Export (0) Print
Expand All
Build Providers for Windows Forms
Draft a Rich UI: Ground Rules for Building Enhanced Windows Forms Support into Your .NET App
A New Grid Control in Windows Forms
Owner-Drawing in .NET
P2P Comm Using Web Services
Smart Clients: Build A Windows Forms Control To Consume And Render WSRP Portlets
Spice It Up: Sprinkle Some Pizzazz on Your Plain Vanilla Windows Forms Apps
Synchronizing Multiple Windows Forms
Text Rendering: Build World-Ready Apps Using Complex Scripts In Windows Forms Controls
Windows Forms Controls: Z-order and Copying Collections
Winning Forms: Practical Tips For Boosting The Performance Of Windows Forms Apps
Code Samples
Expand Minimize

StickOut: A Desktop Sticky Notes Application in the .NET Framework 2.0

 

Omar AL Zabir
OmarAlZabir@gmail.com

March 2006

Applies to:

Microsoft .NET Framework 2.0
Microsoft .NET Remoting
Microsoft Office
Microsoft Visual Studio
Windows Forms

Summary: StickOut is a desktop sticky note with multi-user support and Outlook integration. As a .NET Framework 2.0 Windows Forms application, it uses .NET Remoting to communicate with other StickOut users and exchange sticky notes with them. (85 pages)

Download the associated StickOut.msi code sample.


Note   There is a key file in the source code project that requires a password in order to build the solution. The password is 'janina'.

Contents

Introduction
Features
The Idea
Day 1 – Planning and Designing Architecture
Day 2 – Desktop Sticky Notes
Day 3 – PDC Contest and Working with RichTextBox
Day 4 – Multi-user support
Day 5 – Outlook Integration
Day 6 – Smaller, Better, Faster
Day 7, 8, 9, 10, 11, 12, 13, 14 –> MSDN Article
Conclusion

Introduction

StickOut is a desktop sticky note application with multi-user support and Outlook integration. It is a .NET Framework 2.0 Windows Forms application that uses .NET Remoting to communicate with other StickOut users and exchange sticky notes with them. It uses the new .NET Framework 2.0 IPC Channel for communicating between Microsoft Outlook and the StickOut process. The Outlook Add-in allows you to stick anything out to the desktop from Outlook including e-mails, notes, tasks, appointments, etc., as sticky notes. Care has been taken to reduce the memory footprint of a .NET application significantly and to make a fast and smooth user experience. This article chronicles my first day of planning the application, through subsequent design, development, testing, and deployment days, revealing the evolution of the application, and the complications I dealt with at each step. You will read about lots of .NET tricks, deployment and versioning problems, Visual Studio tricks, and some other non-development–related tricks that might help you to boost your daily development work.

Features

StickOut is a desktop sticky note application that offers the following features:

  • Floating desktop sticky notes
  • Rich text inside sticky notes
  • Multi-user; send/receive sticky notes between other sticky friends
  • Compose Outlook e-mail directly from a sticky note
  • Send any item from Outlook to the desktop as a sticky note using an Outlook add-in
  • Send selected content from Microsoft Word and Microsoft Excel to a desktop sticky note using a Microsoft Word/Excel add-in
  • Create a desktop sticky note from the Visual Studio code editor using the Editor context menu
  • Attach any file to a sticky note and send it to others along with the attachments

    Figure 1. StickOut desktop sticky note application

The Idea

To create my application, I used Stickies freeware, which is the best desktop sticky note application I have seen thus far. It supports rich style, sends and receives stickies over the network, places alarms on stickies, and has many other features. It is also lightweight and fast. However, it lacks some features: it's not open source so I cannot extend it, nor does it feature Outlook integration.

Often I take an e-mail body and put it on a sticky so that I do not forget about the e-mail, and it constantly blinks in front of my eyes. Frequently I copy code from the Visual Studio editor, put it on desktop sticky notes, and send them to others on the network; it is the fastest way to share code with others. Just one click and the code is on my friend's screen. Stickies freeware also does not support attachments or linking files to desktop sticky notes, which is sometimes needed. On a network, you must mail files to others, copy them to a share, or use Instant Messenger (IM) to transfer files. This requires lots of steps. What if we could send sticky notes along with files to others on the network with just a single click? To accomplish this, I started writing my own desktop sticky note application in .NET. When the Stickies application becomes open source, the developer community will move forward and improve it in many ways that I can't comprehend right now. So, on a very hot summer noon, I started sorting out the features for my application.

Day 1 – Planning and Designing Architecture

Aug. 14, 2005 12:50 PM

Planning

In my first draft, I summarized the application as follows:

OutlookNotes is an add-in for Outlook 2003, which allows Outlook notes to be shown as a desktop sticky, and vice versa. You can display e-mails as desktop stickies or any post as a sticky, or you can create custom stickies and place them inside Outlook. You can also right-click on a sticky and send it as mail via Outlook. OutlookNotes also support attachments with stickies so that you can drag files from anywhere and put them on a sticky. It's very convenient for composing notes that eventually become e-mail.

I was thinking of making my application Outlook-based only. But each day, it evolved and eventually became more of a stand-alone desktop sticky notes application. Some of the initial ideas did not make it into final version.

The next steps were to sort out the major features. I wanted the application to have the capability to:

  • Send an Outlook note as a desktop sticky
  • Send Outlook e-mail to a desktop sticky
  • Attach files by dragging and dropping
  • Apply styles
  • Store stickies inside the Outlook Notes folder
  • Allow any Outlook item to become a sticky

After deciding on the features, I defined some usage cases on how users will interact with the application. I documented all of these in a text file. This is extremely low tech, but it gets the job done for play projects. It's not recommended for professional projects.

  • Startup—User starts Outlook and a new button is added to toolbar which sticks out selected item to desktop.
  • Stickout—Get selected items from Outlook and create sticky on desktop.
  • Move sticky—When user drags sticky, try to provide elastic effects. Create regions on desktop that make a sticky snap in.
  • Edit Sticky—When sticky is clicked, create inline editor. Normally, do not show inline editors, as it is costly to create editors.
  • Delete sticky—Delete a sticky from the desktop and also from the Outlook store.
  • Hide sticky—Hide a sticky from desktop but remain in the Outlook store.
  • Set transparency—Make sticky windows transparent.
  • E-mail sticky—Launch Outlook Compose window with sticky content and attachments ready for e-mail.
  • Drag files to stickie—Support file drag-and-drop and store as attachments.
  • Delete attachments—Show attachments at bottom of sticky notes and allow delete.
  • Set reminder on Sticky—Allow user to set reminder for desktop sticky.
  • Sleep sticky—Allow user to make a sticky sleep (vanish) for a period of time and wake up (reappear) on screen.
  • Create styles—Allow user to create styles and save for use on stickies.
  • Assign style—Allow user to apply existing style to sticky.
  • Always on Top—Allow user to toggle always on top by clicking icon on the sticky title bar.

Writing down such usage cases helps you visualize how the application will behave. As developers, by instinct we discover the technical complexities while visualizing how people will be using the application.

Now comes the risk-assessment part. It is clear from these usage cases that there are several technical challenges that need to be overcome before getting started with coding. These are the difficulties that initially came to my mind:

  • Create custom form for storing stickies or use existing PostItem in Outlook.
  • Aug 15: Let's store stickies outside Outlook.
  • Get content of e-mails and create sticky. How to get RTF from body?
  • Apply styles and additional attributes on stickies and persist them in Outlook format.
  • Create Tray icon from Outlook Add-in.
  • Take a screen shot of stickies and show them whenever they lose focus.
  • Set reminders for stickies.
  • Make stickies transparent.
  • Leave memory footprint, no matter what.
  • Editor—use DHTML editor or rich text box?
  • Outlook Add-in loader—see if it works properly.
  • Create flat forms with title that can be dragged and resized.

As I proceed to the development, I will explain how I overcame these difficulties and a lot more difficulties discovered along the way.

Designing

Now it is time to design the architecture. Initially the object model is simple and looks like this:

Figure 2. Object model

_Application is the root class, In order to avoid confusion with System.Windows.Forms.Application, I have prefixed with it a "_" so that you never get confused while reading the article. _Application is a singleton and has the following two roles:

  • Hosts the entire hierarchical object model
  • Provides application-wide services

So, _Application contains the Stickies collection and the Options object. Stickies is a collection of Sticky objects. Each Sticky contains an Attachment collection that represents a file.

Options contains application-wide settings such as whether to start on Windows Startup, the default sticky size when a new sticky is created, the default sticky color, etc.

The idea is to create an object model that supports automation, like most of the Microsoft products including Office and Visual Studio. The automation model is described in my article, Implement a Microsoft Word-like Object Model for Your .NET Application. You first need to understand the idea behind the architecture explained in the article. Otherwise, you will have trouble understanding the ideas presented here. The benefit of having such an object model is that it completely decouples your modules from each other and no module knows of the existence of any other module. Each module can assume that it is the only module in the whole application, and uses the central object model as its central storage and source of notifications. As a result, your application's complexity does not increase exponentially or in order of magnitudes of major features. Without having such an object model, complexity increases almost exponentially as you add more major features that result in increased coupling between modules of your application.

Figure 3. Without Automation-supported object model

Figure 4. With Automation-supported object model

Sometimes you can introduce a lot of major features without making major changes in the object model. This is why I have shown a drop in complexity in the graph. In this article, I will show you how easy it was to add full-fledged multi-user features in a simple single-user application.

Back to Planning

It was time to decide whether this Sticky note application has any prospects or not, and whether people will use it at all. If there's not much prospect, there's no point spending time on it. I thought of the following positive sides:

  1. Desktop sticky software is used by thousands of people.
  2. Outlook is used by millions of people.
  3. As an Outlook Add-in, StickOut might reach millions of people eventually.
  4. StickOut can succeed as a stand-alone Sticky program, too, and have a large user base.

It looks, theoretically, like it has good prospects; however, there are some risks that need to be considered which might lead to an unsuccessful product:

  1. Not many people use the .NET Framework 2.0.
  2. People still use Outlook 2002/2000/XP.
  3. Outlook Add-in loader, which will be used to develop my Outlook add-in, might have deployment problems.
  4. People already use some kind of desktop sticky program, so not everyone will be willing to use it. The feature set needs to be very compelling.

Although the risks are pretty high, the feature set and the technical challenges look like a good coding exercise that is worthwhile. So, I will start designing the architecture.

Developing the Architecture

Figure 5. High-level architecture

There are 3 major parts in the high-level architecture:

  • The UI (user interface) is the visible part of the application that receives notification from the object model and acts accordingly. It also receives user actions and converts them to some kind of object model modification. For example, when you change the text in the Title TextBox, the Sticky object's Title property is changed.
  • The Object Model is the central store of all information and also a central source of application-wide events. For example, _Application class contains an event name OnNewSticky. This event is raised whenever someone wants a new sticky to be shown on the screen. So, the concerned module which knows how to show a new sticky subscribes to this event and provides the new sticky creation feature.
  • Engines are the heart of the application that provides various features like creating a new sticky, loading and saving stickies, integration with Outlook, remoting, etc. They are not aware of the UI at all. All they know is there's an object model that contains everything they need to know and subscribes to the events of different objects. Whenever they receive an event, they act upon it.

Let's see how the _Application class looks:

public class _Application 
{
   #region Events & Delegates

   public static event Action<_Application> OnNewSticky;
   public static event Action<_Application> OnQuit;
   public static event Action<_Application> OnStart;
   public static event Action<_Application> OnShowStickies;
   public static event Action<_Application> OnHideStickies;
   public static event Action<Sticky> OnEmail;
   public static event Action<Sticky> OnAcceptSticky;

   #endregion

   #region Collections
   
   public GenericCollection<Sticky> Stickies;
   public Options Options;
   public static IStickyStore Store;

   #endregion

The Action delegate is a new delegate in the .NET Framework 2.0. This is a handy delegate for methods that deal with an object. If you want to create methods/events that deal with a particular class' instance, you can use it. The definition of this delegate is as follows:

// Represents the method that performs an action on the specified object.
public delegate void Action<T>(T obj);

However, the .NET standard way of declaring an event is using the EventHandler delegate that is defined as:

// Represents the method that will handle an event that has no event data.
[Serializable][ComVisibleAttribute(true)]
public delegate void EventHandler(object sender, EventArgs e);

But this limits you to use only those classes that inherit from EventArgs. This means you have to create container classes that inherit from EventArgs for the objects that you want to pass through the event. This results in too many small classes that have no task but contain one or more objects. When your object model is pretty big, you end up with many small classes that you have to create in order to pass through the events. Of course, this is the standard way and this is how you should declare events.

Now in the .NET Framework 2.0, you can use generic types to declare events in a much better way:

public static event EventHandler<_Application> OnNewSticky;

As a result, the event's definition becomes like this:

OnNewSticky( object sender, _Application e )

This is surely a very good thing. But the catch is, the delegate is defined as EventHandler<TEventArgs>:

// Represents the method that will handle an event. 
// The generic type argument
// specifies the type of the event data generated by the event.
[Serializable]
public delegate void EventHandler<TEventArgs>
      (object sender, TEventArgs e);

If you use any of your class, it needs to inherit from EventArgs. As a result, if I pass _Application, I get this error:

The type 'StickOut.ObjectModel._Application' must be convertible to 
'System.EventArgs' in order to use it as parameter 'TEventArgs' in the 
generic type or method 'System.EventHandler<TEventArgs>'   

This is definitely not an option because I cannot make all of my classes which I need to pass with events inherit from EventArgs. So, I ended up using the Action delegate for my events. The only feature I needed to sacrifice is the source parameter that is sometimes very helpful.

There are some public static methods in _Application that raises these events:

public static void New()
{
   if (null != _Application.OnNewSticky) OnNewSticky(_Application.Instance);
}

public static void ShowStickies()
{
   if (null != _Application.OnShowStickies) OnShowStickies(_Application.Instance);
}
...
...

This is all that _Application class does. It has no other responsibility.

All the entity classes inherit from ItemBase that provides the "observable model" feature. If you inherit from this class, you can make your objects observable. It exposes a lot of events and methods to make this possible. It not only makes your objects observable, but also provides the feature for event bubbling. You can receive events raised from very deep in the object model hierarchy just by sitting at the top of the object model. For example, if you subscribe to the Stickies collection OnItemChange event, you can receive the OnChange event of any Sticky object contained in that collection. Both GenericCollection and ItemBase classes make this event propagation possible. For details about this, please read my other article, Implement a Microsoft Word-like Object Model for Your .NET Application.

Now, we have seen the _Application class only raises the events. But how is the actual service provided? Let's see how we can add persistence to our application by using these events:

internal class XmlStickyStore : IStickyStore
{
   private GenericCollection<Sticky> _Stickies;

   void IStickyStore.Initiate(GenericCollection<Sticky> stickyCollection)
   {
      this._Stickies = stickyCollection;

...
...
...

      // Subscribe to events to provide service
      this._Stickies.OnItemCollectionAdd += 
      new CollectionAddHandler(_Stickies_OnItemCollectionAdd);
      this._Stickies.OnItemCollectionRemove += 
      new CollectionRemoveHandler(_Stickies_OnItemCollectionRemove);
      this._Stickies.OnItemChange += new ItemChangeHandler(_Stickies_OnItemChange);

   }

The XmlStickyStore class stores the Sticky as serialized in Xml format. It first subscribes to the Add, Remove, and Change events of the Stickies collection. So, whenever a sticky is added or removed or modified by any part of the application, this class immediately knows it and can save the sticky to an Xml file.

Here's how it saves a Sticky whenever it is modified by anyone:

void _Stickies_OnItemChange(ItemBase item, string changeType)
{
   this.SaveSticky(item as Sticky);
}

private void SaveSticky(Sticky sticky)
{
   if (null == sticky.ID || 0 == sticky.ID.Length)
      sticky.ID = Guid.NewGuid().ToString("D");

   string fileName = GetStorePath(sticky.ID);
   using (XmlTextWriter writer = new XmlTextWriter(fileName,Encoding.ASCII))
   {
      XmlSerializer serializer = new XmlSerializer(typeof(Sticky));

      serializer.Serialize(writer, sticky);

      writer.Close();
   }         
}

This makes development of other modules a lot easier because you never need to call anyone to persist sticky objects. Whenever you change a sticky object from any part of the application, it is automatically saved.

Originally I was planning to provide two types of storage—Outlook and Xml. So, I decided to use the Builder pattern in order to instantiate the proper storage class. Here I learned an interesting trick to make a Generic Builder that can create any type of product and also take required supplies for creating the product, in a fully strongly typed and generic way.

Lord of the Builders, One Code to Build Them All

This little piece of code can throw away all the builder classes in all your projects when you come to the .NET Framework 2.0:

public static class Director<ProductType,Supplies> where
   ProductType : Product<Supplies>, new() 
{
   public static ProductType Build(Supplies supplies)
   {
      // Here you can introduce factories to construct the object
      ProductType product = new ProductType();
      // Here you can make rule based building process
      product.Build(supplies);
      return product;
   }
}

public abstract class Product<Supply>
{
   public abstract void Build(Supply supply);
}

Figure 6. How the GenericFactory works

Director takes two types: the product that you want to create and the type of supplies or raw materials required for creating the product. When you call Build, it takes the supplies and passes it to the product so that it can use them to build itself.

You can create any concrete product by extending the generic Product:

public class ConcreteProduct : Product<string>
{
   public override void Build(string supplies)
   {
      // Prepare yourself using the supplies
   }
}

If you want to use the Director to create this concrete product, you do it this way:

ConcreteProduct product = Director(ConcreteProduct,string>.Make("Hi");

Here's how the XmlStickyStore looks:

internal class XmlStickyStore : Product<GenericCollection<Sticky>>
{
   public override void Build(GenericCollection<Sticky> stickyCollection)
   {

It is created this way:

_Application.Store =
   Director<XmlStickyStore,GenericCollection<Sticky>>
      .Build(_Application.Instance.Stickies);

Here the product is XmlStickyStore which is created inside the Factory, and the required material is passed to the product which is of type GenericCollection<Sticky> (a collection of Sticky objects). Similarly, you can pass a University object as product and supply Students as supplies and get a populated University with lots of students happily attending their classes regularly.

Generic Singleton

You can also use generics to make a Generic Singleton provider. The following code shows how:

class Registry 
{
   static Registry()
   {
   }

   public void DoSomething()
   {
   }
}
public static class Singleton<T> where T:new() 
{
   public static readonly T Instance = new T();
}

The only constraint is, the class that needs to become a singleton, needs to have a static constructor instead of a private constructor. Now, you use it this way:

Singleton<Registry>.Instance.DoSomething();

Using this generic Singleton<> class, any class having a static constructor can become a singleton class without requiring explicit codes to handle single instance problems. There are lots of ways to make a singleton. Some suggest locking on a static object, some suggest double-locking, and some suggest just using the readonly keyword. No matter what you decide to do, you do it in the Singleton<T> class only and all singleton objects in your entire project work the way you want.

On this first day, although no UI was built, I got a lot of things done just because it is a weekend. Unfortunately, for the rest of the project, I will return to a "go to the office, study, eat, sleep" cycle again.

Day 2 – Desktop Sticky Notes

August 15, 2005 6:40 PM

Creating a Stand-alone Desktop Application

I just learned there's a Professional Developers Conference (PDC) contest going on and the winner will get free ticket to PDC. So, I will start on the development, and give up going to my office for a day.

(Well, as it turns out, I did not win the contest but at least it introduced me to Shareware Starter Kit. The application was just too premature to be considered usable when I submitted it. So, please don't download it from the entry list—I will be embarrassed. You can find about the contest winners.)

Now, let's start with the UI. First we need to make the StickyForm that will show the content of sticky notes. The form is a borderless form with no resize areas or title bar.

Figure 7. Design of a StickyForm

  • Title label: Works as title bar for the borderless form. I have used Win32 API to make the form movable by simulating the Label as the title bar.
  • Label with Close Icon: Label is the most lightweight control to show a small picture.
  • TextBox for changing title: This text box is normally invisible, but shown only when user wants to change the title
  • Rich Text Box: This is where the body of the sticky note is shown and drag-and-drop attachments are used.
  • Space for resizing: A 2-pixel space around the form is left which acts as resizing border. User will use these areas to resize the form. I have use Win32 API to simulate this area as the form resize border. Setting these 2-pixel borders is a breeze in the .NET Framework 2.0. Now each control including the Form has a property Padding, which can be set to implement Padding inside the area. So, you can just put controls taking up the whole space and when you are done, set the Padding to 2 and that's all.
  • Split Container: the .NET Framework 2.0 offers a wonderful container control that provides 2 panels and a splitter between them. You can put controls inside those panels and they are automatically resized using the splitter. In .NET 1.1, it was really difficult. You had to put Panels in a given order, set Dock property very carefully, place the Splitter control at the right time and the right place, otherwise nothing worked. All these problems are solved by this SplitContainer control.
  • Attachment List View: This list view will show the attachments.

    The layout of the Form is like this:

    Figure 8. Control hierarchy

The StickyForm takes a Sticky in its constructor and listens to all changes made to the sticky.

private Sticky _MySticky;

public Sticky MySticky
{
   get { return _MySticky; }
   set { _MySticky = value; }
}
   
public StickyForm(Sticky sticky)
{
   InitializeComponent();

   this.MySticky = sticky.
   this.Subscribe();
}

private void StickyForm_Load(object sender, EventArgs e)
{
   this.RefreshUI();
}

private void Subscribe()
{
   _MySticky.OnChange += new ItemChangeHandler(_MySticky_OnChange);
}

private void Unsubscribe()
{
   _MySticky.OnChange -= new ItemChangeHandler(_MySticky_OnChange);
}

When the OnChange event is fired, it calls the RefreshUI method in order to reflect changes to the UI:

private void RefreshUI()
{
   if (base.Disposing || null == this.MySticky) return;
   
   this.contentTextBox.BackColor = this.MySticky.BackColor;
   this.attachmentListView.BackColor = this.MySticky.BackColor;
   ...
   ...
   base.TopMost = this.MySticky.AlwaysOnTop;
}

After this, changes on the UI need to be reflected on the sticky. The PopulateSticky method takes care of this and is called from variety of events in order to make sure the changes are properly reflected on the object model:

private void PopulateSticky()
{
   if (null == this.MySticky) return;

   this.MySticky.BeginUpdate("Populate");

   this.MySticky.Rtf = this.contentTextBox.Rtf;
   this.MySticky.Body = this.contentTextBox.Text;
   this.MySticky.Title = this.titleLabel.Text;
   this.MySticky.Bounds = base.DesktopBounds;

   this.MySticky.EndUpdate();
}

This method is called on Form resize, deactivate, closing, RichTextBox lost focus etc., in order to make sure we don't miss saving the changes to the Sticky object in any case. Also note, as soon as EndUpdate is called, the OnChange event is fired on the object. This event propagates to the Sticky collection's OnItemChange event and XmlStickyStore captures the event and saves the sticky.

Now we have a sticky form that can show Sticky notes and also allows user to make modifications to the sticky notes.

Now we need a controller type class that will be supervising all the Sticky forms. The UIServiceProvider class does this job that includes:

  • _Application.Stickies.OnItemAdd—A new sticky is added, so create a new sticky form
  • _Application.Stickies.OnItemRemove—A sticky has been removed from the collection, so close the sticky form
  • _Application.Stickies.OnItemChange—See if a Sticky has just been closed and if so, remove the sticky form. If a sticky has been reopened, then recreate the sticky form.

Here's how it detects changes made in any of the Sticky object and reacts:

static void Stickies_OnItemChange(ItemBase item, string changeType)
{
   Sticky sticky = item as Sticky;

   if (!sticky.Closed)
   {
      if (!_StickyForms.ContainsKey(sticky))
      {
         // The sticky has been changed to Not Closed. So, we need
         // to create a sticky form from for this.
         CreateStickyForm(sticky);
      }
   }
}

UIServiceProvider maintains a Dictionary of Sticky and its associated Form so that it can easily detect the form given a Sticky. When events are raised, a Sticky object is passed as the parameter of the event. So, it needs to find out what is the associated Form for the Sticky object. This is why it maintains a mapping between Sticky object and its representative Form.

/// <summary>
/// Maintains a map between the sticky objects and 
/// the Sticky form on the desktop
/// </summary>
private static Dictionary<Sticky, DummyForm> 
   _StickyForms = new Dictionary<Sticky, DummyForm>();

Having this UIServiceProvider, we have the features to add/remove sticky forms, edit the content inside the form and then save stickies in XML files. So far, we have a nice desktop sticky application that is more or less usable, but you can't move or resize any sticky.

Moving Form Using Anything

Using Win32 API, you can make any control act as the title bar for a form. This is done by 2 API calls:

[DllImportAttribute("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, 
   int wParam, int lParam);

[DllImportAttribute("user32.dll")]
public static extern bool ReleaseCapture();

On the MouseDown event of the Label, we need to first call ReleaseCapture in order to release the mouse control and give it back to Windows. Then call the SendMessage API to send a message to the Form that the mouse button is in fact pressed on the Title bar of the Form.

private void titleLabel_MouseDown(object sender, MouseEventArgs e)
{
   Win32.ReleaseCapture();
   Win32.SendMessage(Handle, Win32.WM.WM_NCLBUTTONDOWN, 
      HTCAPTION, lparam);
}

This is all that is needed to make a control act as a title bar. WM_NCLBUTTONDOWN is a constant that tells Windows that the mouse has been pressed on a non-client area of a Form that is not the area where we put controls. Instead, the area, such as the title bar, is beyond our reach. When we pass the HTCAPTION constant, we simulate what happens when the mouse is pressed on the title bar in Windows. The Windows default behavior for moving a form activates and moves the form.

Borderless Form with Custom Resize and Move

If you set a Form's FormBorderStyle = None, you will see that there is no title bar and no resize border. So, your form becomes a fixed size and the user cannot resize it. But in Sticky forms, we need to remove the default borders and title bar for a flat, slick look and feel, but also to provide the resize feature. This is done in two steps:

  1. When the mouse moves on the border areas, we try to find out on which corner of the border area the mouse is at this moment and set the Cursor icon accordingly. For example, when the mouse moves to the top left corner, we set the Cursor to SizeNWSE.
  2. When the user presses the left mouse button, we need to call the Win32 API to tell Windows that a resize session needs to be initiated by calling the SendMessage API similar to the one we have seen before, and passing the direction of resize.

Here's the code on MouseMove that sets the proper Cursor icon according to the position where the mouse is now moving:

private void StickyForm_MouseMove(object sender, MouseEventArgs e)
{
   const int space = 10;

   if (e.Button == MouseButtons.None)
   {
      if (e.X < this.bodyArea.Left)
      {
         // Cursor left side
         if (e.Y <= this.titlePanel.Top + space)
         {
            // Cursor top left
            base.Cursor = Cursors.SizeNWSE;
         }
         else if (e.Y >= this.bodyArea.Bottom - space)
         {
            // Cursor bottom left
            base.Cursor = Cursors.SizeNESW;
         }
...
...

In the same way, we will detect where the user presses the mouse, and then call SendMessage accordingly:

private void StickyForm_MouseDown(object sender, MouseEventArgs e)
{
   const int space = 10;

   if (e.X <= this.bodyArea.Left)
   {
      // Cursor left side
      if (e.Y <= this.titlePanel.Top + space)
      {
         // Cursor top left
         StartResize(Win32.HT.HTTOPLEFT, 0);
      }
      else if (e.Y >= this.bodyArea.Bottom - space)
      {
         // Cursor bottom left
         StartResize(Win32.HT.HTBOTTOMLEFT, 0);
      }
...
...

private void StartResize(int ht, int lparam)
{
   Win32.ReleaseCapture();
   Win32.SendMessage(Handle, Win32.WM.WM_NCLBUTTONDOWN, ht, lparam);
}

Now we have a borderless resizable form. Remember to leave some space on the form around it. Otherwise the Form's MouseMove and MouseDown events will not fire; instead, the control that is on the border area will get the mouse events.

More Information on Mouse Capture

Options Form as Central Form for UI Services

In order to show a Tray icon, we need a form. So, I used the OptionsForm to host the NotifyIcon and the context menu for the tray icon. However, this form needs to always be open and must not be closed or the application will terminate. To do this, cancel the FormClosing event when the user tries to close the Form manually:

private void OptionsForm_FormClosing(object sender, FormClosingEventArgs e)
{
   this.ProcessOptions();

   if (e.CloseReason == CloseReason.UserClosing)
   {
      base.Hide();
      e.Cancel = true;
   }         
}

We will prevent the user from closing the form by hiding it. But on other events like System shutdown, we need to let the application close.

public enum CloseReason
{
   None = 0,
   WindowsShutDown = 1,
   MdiFormClosing = 2,
   UserClosing = 3,
   TaskManagerClosing = 4,
   FormOwnerClosing = 5,
   ApplicationExitCall = 6,
}

OptionsForm uses a PropertyGrid to attach the _Application.Instance.Options object so that the user can edit the object directly, although you should never do this because PropertyGrid is really difficult for regular users. I was just too lazy to make a customized editor. So, the end result is as shown in Figure 9:

Figure 9. Options form

In order to make your object display nicely on PropertyGrid, you need to decorate each property with some designer attributes; otherwise, they look horrible. Here's a sample:

[Serializable]
[XmlRoot("options")]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class Options : ItemBase
{
   public event PropertyChangedEventHandler PropertyChanged;
   private string _MyName = Environment.UserName;

   [Browsable(true)]
   [Category("Myself")]
   [Description(@"Your name that your friends will see")]
   public string MyName
   {
      get { return _MyName; }
      set 
      {
         string oldValue = _MyName;
         _MyName = value;
         if (_MyName != oldValue) OnPropertyChanged(new 
               PropertyChangedEventArgs("MyName"));
      }
   }
  • Browsable(true)—Makes the property visible in PropertyGrid
  • Category("")—Groups properties of the same category together
  • Description("")—When you select a property in property grid, the description is shown at the bottom pane of the property grid.

Also, the standard way to notify changes in an object is to raise the PropertyChanged event specifying the name of the property that is changed. This is a well-known practice.

///<summary>A PropertyChanged event is raised when a property is 
///changed on a component. A PropertyChangedEventArgs object 
///specifies the name of the property that is changed.</summary>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
   if (null != PropertyChanged)
   {
      PropertyChanged(this, e);
   }
   NotifyChange(e.PropertyName);
}

The last line's NotifyChange is mine. It's available in ItemBase. This method fires the OnChange event of ItemBase so that all others listening to the Options object get the OnChange event. In the case of Options, there's an OptionStore class that listens to changes made in the Options object and saves in the Settings object.

Store User-Level Information in .NET Framework 2.0 Settings

The .NET Framework 2.0 offers a new feature called Settings. Go to Project Properties and click the Settings tab. The Settings window opens so you can create strongly typed Settings properties.

Figure 10. Settings

There are two modes for Settings: Application and User. When you set it to User, .NET automatically creates a config file in the "Documents And Settings\Your User Name\Local Settings\Application Data\Company Name\Your App Name" folder. This is an Xml file that contains a serialized version of the Settings object. The setting is loaded automatically for you whenever you first access it. So, you can access the properties in a strongly typed fashion:

Settings.Default.Options

When you declare Scope as User, the properties become read and write. So, you can change the property values programmatically and call Settings.Default.Save() to save the changes.

The OptionsStore class uses this feature and stores the Options object as serialized into Xml in the Settings.Default.Options.

Here's how loading and saving works:

private static void LoadOptions()
{
   if (null != Settings.Default.Options && Settings.Default.Options.Length > 0)
   {
      _Application.Instance.Options = 
         (Options)SerializationHelper.Deserialize(
         new StringReader(Settings.Default.Options), 
            typeof(Options));
   }
   else
   {
      _Application.Instance.Options = new Options();
   }
}

static void _Options_OnChange(ItemBase item, string changeType)
{
   SaveOptions();
}

private static void SaveOptions()
{
   StringBuilder buffer = new StringBuilder(1024);

   SerializationHelper.Serialize(new StringWriter(buffer), 
      _Application.Instance.Options);

   Settings.Default.Options = buffer.ToString();
   Settings.Default.Save();
}

Serializing Color Using Hidden Properties

You cannot serialize Color in .NET. I discovered this when I was trying to serialize the StickyColor property of Options. If you open the serialized Xml you will see the node is an empty node having no value inside it, although you have set it. As a result, I had to bypass this using a different property that stores the numeric value of the Color property.

private int _StickyColor = SystemColors.Info.ToArgb();
[Browsable(false)]
[XmlElement("StickyColor")]
public int XmlStickyColor
{
   get
   {
      return this._StickyColor;
   }
   set
   {
      this._StickyColor = value;
   }
}

[Browsable(true)][XmlIgnore]
[Category("Sticky")]
[Description("Default back color of sticky")]
public Color StickyColor
{
   get { return Color.FromArgb( XmlStickyColor ); }
   set
   {
      int oldValue = XmlStickyColor;
      XmlStickyColor = value.ToArgb(); 
      if (oldValue != XmlStickyColor) 
         OnPropertyChanged(new PropertyChangedEventArgs("StickyColor"));
   }
}

XmlStickyColor is the invisible property that stores the numeric value of the StickyColor property. StickyColor reads and writes directly to the XmlStickyColor without using any private variable to store its own value. XmlStickyColor is serialized/deserialized perfectly, as it is an Int32 type.

One important observation: after deserializing a Color, it is neither empty nor transparent. It's black, having all components set to 0 (0,0,0).

Making Forms Stick to Each Other and the Screen Border

Desktop sticky notes are very sticky in nature. Whenever you try to move them or resize them, they either try to stick to each other or try to snap to screen boundaries. This is a very handy feature for organizing sticky forms properly so that they are not randomly scattered on the screen. The idea of this snap feature is to detect when a window is moving or resizing and then calculate the position of all other sticky forms on the screen. Whenever the boundary of the moving or resizing form goes near to another form's boundary, eliminate the distance between them instantly. Thus the moving/resizing form suddenly gets attracted to another form and snaps itself with that form.

The calculation is long and boring to explain, but it is perfectly explained in this article.

I have used the nice StickyWindow class that takes a form as a constructor parameter and provides the sticky behavior automatically. This class extends from the NativeWindow class of the .NET Framework that allows you to go under the hood of Win32 APIs and subclass any Form or Control. You can study the code of this StickyWindow class and learn a lot about subclassing forms. Also search Google with the term "NativeWindow" and you will find a lot of resources.

File Drag-and-Drop for Attachment

There are two types of drag-and-drop support I have added:

  • Embedding dropped images directly inside the Rich Text Box
  • Linking to files that are not images as external attachments

In order to enable drag-and-drop for RichTextBox, you will have to turn on the AllowDrop property and subscribe to DragEnter and DragDrop events. However in Visual Studio 2005 Beta 2, I was not able to see the AllowDrop property in the Property Grid nor the DragEnter and DragDrop events in the event list. So, I had to manually write the code in the Form Load event:

this.contentTextBox.AllowDrop = true;
this.contentTextBox.DragEnter += new DragEventHandler(contentTextBox_DragEnter);
this.contentTextBox.DragDrop += new DragEventHandler(contentTextBox_DragDrop);

The DragEnter event is fired when the user drags an item on the control/form and the mouse enters the boundary for the first time. In this event, you need to set the cursor to either Allow or Deny according to the type of content that has been dragged.

Here's the DragEnter event I have written which only allows files to be dragged into:

/// <summary>
/// These are the files which are not linked instead copied inside the RichTextBox
/// when dragged & dropped
/// </summary>
private static readonly string[] 
   _FileExtensionsForCopyOnDragDrop = { ".jpeg", ".jpg", ".gif", 
      ".png", ".tiff" };

void contentTextBox_DragEnter(object sender, DragEventArgs e)
{
   // A file is being dragged
   if (e.Data.GetDataPresent(DataFormats.FileDrop))
   {
      bool canCopy = true;
      // Get the file names and check whether the files are
      // Pictures. We can directly copy the pictures
      string[] fileNames = e.Data.GetData(DataFormats.FileDrop) as string[];
      foreach (string fileName in fileNames)
      {
         string extension = Path.GetExtension(fileName);
         if (Array.IndexOf<string>(_FileExtensionsForCopyOnDragDrop, extension) < 0)
         {
            // Unsupported picture
            canCopy = false;
         }
      }

      if (canCopy) e.Effect = DragDropEffects.Copy;
      else e.Effect = DragDropEffects.Link;
   }
}

First we look at what is being dragged by calling GetDataPresent on the Data Property.

// Summary:
// Determines whether data stored in this instance is associated 
// with, or can be converted to, the specified format.
// 
// Parameters:
// format: 
//    The format for which to check. See 
// System.Windows.Forms.DataFormats for predefined formats. 
// 
// Returns:
// true if data stored in this instance is associated with, or can be converted
// to, the specified format; otherwise false.
bool GetDataPresent(string format);

You can get the list of formats from the DataFormats class. Some of the interesting formats are:

public class DataFormats
{
   public static readonly string Bitmap;
   public static readonly string CommaSeparatedValue;
...
   // Specifies the Windows file drop format, which Windows Forms does not
   // directly use. This static field is read-only.
   public static readonly string FileDrop;
...
   // Specifies a format that encapsulates any type of Windows Forms object. This
   // static field is read-only.
   public static readonly string Serializable;
...
   // Specifies the standard ANSI text format. This static field is read-only.
   public static readonly string Text;
...
   // Specifies the wave audio format, which Windows Forms does not directly use.
   // This static field is read-only.
   public static readonly string WaveAudio;

You can see from the format list that there are so many things that can be dragged and dropped between applications. Unfortunately, not all applications support such a wide variety of drag-and-drop, and I am also one of those lazy people who will only support file drag-and-drop. First I check the extension of the file that is being dragged. If it is one of the picture formats that can be embedded inside RTF, it shows the Copy cursor; otherwise, it shows the Link cursor.

The DragDrop event performs the actual linking or embedding of dragged files.

void contentTextBox_DragDrop(object sender, DragEventArgs e)
{
   if (e.Data.GetDataPresent(DataFormats.FileDrop))
   {
      string[] fileNames = e.Data.GetData(DataFormats.FileDrop) as string[];
      foreach (string fileName in fileNames)
      {
         FileInfo file = new FileInfo(fileName);
         bool isAttachment = (this.attachmentListView == sender) 
         || (Array.IndexOf<string>(_FileExtensionsForCopyOnDragDrop, 
            file.Extension) < 0);

         if (isAttachment)
         {
            // Unsupported picture
            Attachment attachment = new Attachment(file.Name, 
               file.FullName, (int)file.Length, null, null);
            this.MySticky.Attachments.Add(attachment);
         }
         else
         {
            // Insert the picture inside the editor
            using (Bitmap bmp = new Bitmap(file.FullName))
            {
               try
               {
                  Clipboard.Clear();
                  Clipboard.SetDataObject(bmp, true);
                  this.contentTextBox.Paste();

There is no method available in RichTextBox that can insert pictures directly inside the RTF. So, I have first loaded the pictures in a Bitmap object and then copied the bitmap to the Clipboard. When done, the Paste method is called on the RichTextBox so that it picks up the bitmap from the clipboard. For non-picture file types, an attachment is created and added in the Attachments collection of the current Sticky.

Attachment Listview with System File Icon

Figure 11. Attachment List view

When a user drags items to a sticky note, it is added as an attachment to the note. We have seen in the previous section how to support drag-and-drop, and there is no complexity in adding items to the Listview, either. I just subscribe to the Attachments collection whenever an item is added, I add it in the Listview, and whenever an attachment object is removed, I remove it from the Listview. However, the complication is in loading the icon of the file that Windows Explorer shows. Here's an API that does this work:

[StructLayout(LayoutKind.Sequential)]
private struct SHFILEINFO
{
   public IntPtr hIcon;
   public IntPtr iIcon;
   public uint dwAttributes;
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
   public string szDisplayName;
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
   public string szTypeName;
};

private class Win32
{
   public const uint SHGFI_ICON = 0x100;
   public const uint SHGFI_LARGEICON = 0x0;    // 'Large icon
   public const uint SHGFI_SMALLICON = 0x1;    // 'Small icon

   [DllImport("shell32.dll")]
   public static extern IntPtr SHGetFileInfo(string pszPath,
                        uint dwFileAttributes,
                        ref SHFILEINFO psfi,
                        uint cbSizeFileInfo,
                        uint uFlags);
}

internal static Icon GetIcon(string fileName, bool smallIcon)
{
   #region Win32 way
   /*
   IntPtr hImg;    //the handle to the system image list
   SHFILEINFO shinfo = new SHFILEINFO();

   //Use this to get the small Icon
   hImg = Win32.SHGetFileInfo(fileName, 0, ref shinfo,
      (uint)Marshal.SizeOf(shinfo), Win32.SHGFI_ICON |
      (smallIcon ? Win32.SHGFI_SMALLICON : Win32.SHGFI_LARGEICON) );

   Icon myIcon = Icon.FromHandle(shinfo.hIcon);

   return myIcon;
   */
   #endregion

   #region .NET 2.0 way
   return Icon.ExtractAssociatedIcon(fileName);
   #endregion
}

In the .NET Framework 2.0, you need none of the above APIs. The Icon class has a static method ExtractAssociatedIcon that does it all:

Icon.ExtractAssociatedIcon("C:\Test.bmp");

The nice thing about this function is, for bitmaps, it creates a small thumbnail instead of the default bitmap file icon.

List View Context Menu

The attachment list view context menu allows you to switch list view mode:

Figure 12. List View context menu

There are several ways to switch list view mode. You can write a click handler that checks which menu item is clicked and then sets the right value to the View property of ListView. But I tried a shortcut. In the Tag of each menu item I stored the Enum name, e.g., SmallIcon. And then I selected all the menu items and went to the event list and entered a function named SetAttachmentListViewMode. This made all the menu-item clicks to go to one function. In this function I have this code:

private void SetAttachmentListViewMode(object sender, EventArgs e)
{
   ToolStripMenuItem menuitem = sender as ToolStripMenuItem;
   string viewName = menuitem.Tag as string;
   View viewEnum = (View)Enum.Parse(typeof(View), viewName);
   this.attachmentListView.View = viewEnum;
}

This might sound unnecessarily complicated, but I don't like writing repetitive code even if that means repeating one line of code 5 times.

Day 3 – PDC Contest and Working with RichTextBox

August 16, 2005 7:15 AM

Today I need to submit the application to the PDC Contest and there are only four hours left. I had three hours of sleep last night. As a requirement of the contest, Shareware SDK must be implemented.

Shareware SDK

First I download the Shareware Starter Kit from the Web site because it is a prerequisite for all entries.

This is a wonderful SDK for .NET Framework 2.0 applications that you want to become shareware. It comes with registration, activation, and license key management built-in, and you only need to call some Web services.

When you install it, it creates a folder in this location:

C:\Documents and Settings\Omar Al Zabir\My Documents\MSDN\Shareware Starter Kit (C#)

Copy the "Client" and "ClientForms" folder to your application's folder and add a reference to these projects. Remember to sign the "ClientForms" project with the same key that you are using to sign your other projects, if you are signing any of your assemblies at all.

From your project, add a reference to the "ClientForms" project. Now copy and paste the following code:

private static void SharewareStartup()
{
   int splashTimeout = 0;
   double fadeConstant = 0.05;
   int fadeTimer = 35;

   SplashForm.Showasyncsplash(splashTimeout, fadeConstant, fadeTimer);
   SplashForm.StatusText = "Initializing Data Store";
   DataStore ds = new DataStore(SplashForm.FormRef);
   ds.DataStoreFailed += new 
      EventHandler<ProcessingFailedEvent>(DataStore_ProccessingFailed);

   //wait until the web service if finished processing
   while (ds.GetProductState == ProductState.WebServiceInProgress)
   {
      System.Threading.Thread.Sleep(1000);
   }

   if (ds.GetProductState == ProductState.NoDataStore)
   {
      //notify the user that there was a problem in 
      //initializing the app, and verify that they have 
      //internet connectivity
   }

   if (ds.GetProductState != ProductState.NoDataStore)
   {
      SplashForm.StatusText = "Data store initialized";
      System.Threading.Thread.Sleep(2500);
      //start the splash screen closing
      SplashForm.closeAsyncSplash();

      if (ds.GetProductState != ProductState.Activated)
      {
         MessageBox.Show("This product is not activated yet.", 
            "Registration Pending",MessageBoxButtons.OK, 
            MessageBoxIcon.Information);
      }
   }
   else
   {
      SplashForm.StatusText = "Error initializing datastore";
      System.Threading.Thread.Sleep(2500);
   }
}

static void Application_ThreadException(object sender, 
         System.Threading.ThreadExceptionEventArgs e)
{
   Exception ex = e.Exception;

   try
   {
      ExceptionForm exForm = new ExceptionForm(ex.Message,
                            ex.Source,
                            ex.StackTrace,
                            "Unhandled Exception");
      exForm.ShowDialog();
   }
   catch (DataStoreAppConfigException datStoreAppConfigException)
   {
      // Form cannot be used until the datstore has been initialized.
   }
}

static void DataStore_ProccessingFailed(object sender, ProcessingFailedEvent e)
{
   SplashForm.ShowDialogBox(e.FailureInformation.Message);
}

In the previous code block, the highlighted code indicates where to disable features when purchasing a license does not activate the application. Remember to call this SharewareStartup function at application startup.

You need to put some features in your application that allow users to load the registration form. In StickOut there's a menu item in the tray icon context menu named "Registration..." which loads the License Management form supplied with the Shareware Starter Kit:

Figure 13. Shareware license management form

Here's the code to load this form:

StickOut.Shareware.SharewareForm form = new StickOut.Shareware.SharewareForm();
form.Show();
form.Refresh();

I have changed the namespace of the form. In the original code of the Shareware Starter Kit sample application, the form was named "Form1". I hope it changes in a later version. This form takes care of the complete registration process and you don't have to worry about it.

The DataStore class can be used to enable features and functionality based on the state of the product. For example suppose that you want to display a reminder for your user to register upon loading the application:

//create an instance of the DataStore class
DataStore ds = new DataStore();

//if the user is not registered then display a reminder
If (!ds.UserRegistered)
{
   //display a reminder to your user here
}

Another example would be enabling advanced features on a per-license level basis. Assuming we have the DataStore instance already created, the only line of code necessary is the following:

//if the product is activated at a premium level as 
// configured in the server database
If (ds.GetProductState == ProductState.Activated && 

ds.GetLicenseLevel.LicenseLevel == "Premium")
{
   //enable your premium features here
}

Using the same logic, features can be restricted when the trial length set in the Configuration settings elapsed.

//detect the trial period has expired
If (ds.GetProductState == ProductState.ExpiredTrial)
{
   Int numberOfDaysInstalled = ds.DaysElapsedSinceInstall;
   //kindly ask the user to register or activate your product here
}

That's all. It's really a marvelous SDK, the easiest to use that I have seen.

RichTextBox Formatting Shortcuts Dilemma

I just discovered formatting shortcuts like Ctrl+B (Bold), Ctrl+I (Italic), etc., are not working in RichTextBox even though the RichTextShortcutsEnabled property is set to true. So, I wrote the code myself in the KeyDown event of the RichTextBox control:

private void contentTextBox_KeyDown(object sender, KeyEventArgs e)
{
   if (e.Control)
   {
      e.Handled = true;
      if (e.KeyCode == Keys.B)
      {
         this.contentTextBox.SelectionFont = new 
            Font(this.contentTextBox.SelectionFont,
            this.contentTextBox.SelectionFont.Style ^ FontStyle.Bold);
      }
      else if (e.KeyCode == Keys.U)
      {
         this.contentTextBox.SelectionFont = new 
            Font(this.contentTextBox.SelectionFont,
            this.contentTextBox.SelectionFont.Style ^ FontStyle.Underline);
      }
      else if (e.KeyCode == Keys.N && e.Shift)
      {
         this.contentTextBox.SelectionFont = base.Font;
      }
      else if (e.KeyCode == Keys.I)
      {
         e.SuppressKeyPress = true;
      }
      else
      {
         e.Handled = false;
      }            
   }
}

However, one additional shortcut I added here is the Ctrl+Shift+N shortcut of Microsoft Word that clears all formatting of the selected text.

At runtime you will notice Ctrl+I is also not always causing this event to be fired, or sometimes the text disappears when you press Ctrl+I. The disappearing text problem was solved by e.SuppressKeyPress = true;.

So, I moved the Ctrl+I KeyUp event and it worked!

private void contentTextBox_KeyUp(object sender, KeyEventArgs e)
{
   if (e.Control)
   {
      if (e.KeyCode == Keys.I)
      {
         this.contentTextBox.SelectionFont = new 
            Font(this.contentTextBox.SelectionFont,
            this.contentTextBox.SelectionFont.Style ^ FontStyle.Italic);
      }
   }
}

I know what you are thinking: why not move the whole code to the KeyUp event and do nothing in KeyDown? I did that and the result is that, when I press Ctrl+B, the text becomes bold automatically, but then it becomes normal. This can only happen when RichTextBox is doing the formatting itself on KeyDown. It does this only when I write code in KeyDown.

Auto-Resize

In Stickies, when you type and the number of lines increase, the sticky window is resized automatically so that you can see the entire text on screen. This is a very handy feature because you don't need to resize the window yourself and scroll to see the text. To do this, use the new RichTextBox PreferredHeight property. This property returns the height of the control that can show the entire text. So, the code for auto-resize is as follows:

private void AutoResizeFormAccordingToContentSize()
{
   int windowHeight = + this.titlePanel.Height
      + this.Padding.Top 
      + this.contentTextBox.PreferredHeight
      + (   this.bodyArea.Panel2Collapsed ? 0 :
         + this.bodyArea.SplitterDistance
         + this.bodyArea.Panel2.Height )
      + this.Padding.Bottom + this.Font.Height;
   if (this.MySticky.AutoSize && base.Height != windowHeight)
   {
      base.Height = windowHeight;

      // The rich box has already scrolled and the top line 
      // is out of the visible area. So, we need to bring
      // that top line back by simulating a scroll to first line
      int selPos = this.contentTextBox.SelectionStart;
      this.contentTextBox.SelectionStart = 0;
      this.contentTextBox.ScrollToCaret();
      this.contentTextBox.SelectionStart = selPos;
   }
}

PreferredHeight is accurate when there is only one font in the text box. If you apply bold and change text sizes within the text box, it will not return the accurate size. Moreover, it is only correct when the user breaks a line by pressing ENTER whenever the text reaches the boundary of the RichTextBox. If the user types continuously, the text wraps into multiple lines but the PreferredHeight does not increase. The PreferredSize property is of type Size and gives you Width and Height that can fit the text. This means the form needs to grow both horizontally and vertically.

Hot Key

I wanted to add Ctrl+Alt+S as a shortcut key to create a new sticky no matter where I am. In order to do this, I must install a global Key hook.

First we collect the hotkey from the user via the property grid:

Figure 14. Get hotkey from user

Now the following function registers the hot key:

private void SetHotKey()
{
   WiredPrairie.HotKey.HotKeyModifiers modifiers = HotKey.HotKeyModifiers.None;

   Keys shortcutKey = _Application.Instance.Options.ShotcutKey;
   // Strip out all modifiers from the shortcutkey
   if ((shortcutKey & Keys.Alt)>0)
   {
      modifiers |= HotKey.HotKeyModifiers.Alt;
      shortcutKey &= ~Keys.Alt;
   }
   if ((shortcutKey & Keys.Control)>0)
   {
      modifiers |= HotKey.HotKeyModifiers.Control;
      shortcutKey &= ~Keys.Control;
   }
   if ((shortcutKey & Keys.Shift)>0)
   {
      modifiers |= HotKey.HotKeyModifiers.Shift;
      shortcutKey &= ~Keys.Shift;
   }

First we break the hot key into two parts, where one part is the actual key, e.g., 'S,' and the other part includes the modifiers (Control, Alt, Shift). This is done by AND in the complement of the bit we need to remove from the shortcutKey bits. Now comes the registration part:

   this.ClearHotkey();
   
   _HotKey = new HotKey(shortcutKey, modifiers);
   _HotKey.Enable = true;
   _HotKey.HotKeyPressed += new EventHandler(_HotKey_HotKeyPressed);
}

void _HotKey_HotKeyPressed(object sender, EventArgs e)
{
   _Application.New();
}

HotKey is a nice class that is available from http://www.wiredprairie.us/static/HotKey.cs.html.

It takes the key and the modifiers and installs a global keyboard hook. Whenever the hot key is pressed, the HotKeyPressed event is fired and we call _Application.New() to create a new sticky.

Day 4 – Multi-User Support

August 17, 2005 7:30 PM

Friends

Exchanging desktop sticky notes between friends on a network is a very convenient way to share things. For example, say you need a piece of code quickly from your friend. Instead of e-mailing him or calling him on the phone, you can send a sticky note to your friend's desktop and your friend can copy the code inside the received sticky and hit reply. There's no hassle of e-mailing, IM transfer, or copying files to a shared network location. Here's an illustration of how a sticky can be an effective way of sharing texts between your sticky friends:

Figure 15. Sharing text between sticky friends

Sticky Friends:
Friends who don't mind sticking to each other without permission

Now, if you can send sticky notes with file attachments, that's a great convenience. We have already added drag-and-drop attachment support in sticky forms. So, we just need to write code to serialize those attachments and send to another friend's computer.

Figure 16. Transferring files using StickOut

StickOut allows you to add Friends from the context menu on the tray icon.

Figure 17. Friends menu

Add as many friends as you like. You will see my computer's IP is already there. You can directly send a sticky note to me whenever I am online. But I may not be able to reply because your computer might be behind a router or assigned to a private IP (10.x.x.x or 172.16.x.x to 172.31.x.x or 192.168.x.x) and port 54321 might be blocked, which StickOut uses. If you want to exchange a sticky with me when your computer is behind a firewall or router, on the Options menu, set "MyAddress" as the router's or Firewall's IP address and configure the router/firewall to forward port 54321 to your computer's port 54321.

Remoting

Transmitting Sticky works by utilizing .NET Remoting. First a TCPServerChannel is established:

public static class RemotingHelper
{
   public static void Initiate()
   {
      try
      {
         TcpServerChannel tcpChannel = new
             TcpServerChannel("StickOutServerChannel",
                         _Application.Instance.Options.Port);
         ChannelServices.RegisterChannel(tcpChannel);
      }
      catch { }

      try
      {
         TcpClientChannel tcpClientChannel = new TcpClientChannel();
         ChannelServices.RegisterChannel(tcpClientChannel);
      }
      catch { }

      try
      {
         RemotingConfiguration.RegisterWellKnownServiceType(
            typeof(StickOutServer), typeof(IStickOutServer).Name,
            WellKnownObjectMode.SingleCall);
      }
      catch { }      
   }

Don't forget to specify a name in the server channel. Normally, all code samples will show you to use the constructor that takes only the port number. But if you do that, you will not be able to register TcpClientChannel. You will get an error like "The same channel is already registered..." This is why you need to specify a name. We need both server and client channels because the server channel allows us to receive the incoming sticky and the client channel allows us to send the outgoing sticky.

The StickOutServer class acts as a server for receiving commands from TcpServerChannel. Here's the class:

public class StickOutServer : MarshalByRefObject, IStickOutServer
{
   bool IStickOutServer.AcceptSticky(string stickyXml)
   {
      Sticky sticky = (Sticky)SerializationHelper.Deserialize(
            new StringReader(stickyXml), typeof(Sticky));

      // Make this a new sticky by clearing its ID so that, StickyStore creates
      // a new file for it.
      sticky.ID = string.Empty;

      Sticky mySticky = StickyHelper.CreateNewSticky();

      // Copy properties from default sticky
      if (sticky.Bounds.IsEmpty)
         sticky.Bounds = mySticky.Bounds;

      if( null != sticky.FromFriend )
         sticky.Title = sticky.Title + " - " + sticky.FromFriend.Name;

      _Application.AcceptExternalSticky(sticky);
      return true;
   }

   bool IStickOutServer.NewSticky()
   {
      Sticky mySticky = StickyHelper.CreateNewSticky();
      _Application.AcceptExternalSticky(mySticky);
      return true;
   }

}

The _Application.AcceptExternalSticky method takes a sticky and raises the _Application.OnAcceptSticky event. Now, you might be wondering why don't I just call _Application.Instance.Stickies.Add( sticky ), because this will raise the OnItemCollectionAdd event and UIServiceProvider will pick it up and create a new StickyForm from this new sticky object. The reason is because TcpServerChannel works in a different thread. As a result, the methods on StickOutServer are called on a different thread than the main UI thread. You can access UI elements, such as Form or any Control inside a Form, only from the main UI thread.

Figure 18. Cross-thread calls are not allowed in the .NET Framework 2.0.

You see that the form creation is taking place on a background thread, which is illegal. The .NET Framework 2.0 will immediately raise an exception; however, in the .NET Framework version 1.1, sometimes you might be able to get away with it.

The solution is to somehow slipstream to the UI thread before creating the form. The only way we can do it is to receive the event from a Form so that we can call the Invoke method in order to execute code on the UI thread. So, we will get help from the OptionsForm, which will be listening to the OnAcceptSticky event instead of the UIServiceProvider class. This is an exception because all UI-related services are supposed to be provided by the UIServiceProvider. However, due to the multithreading issue, we have no option but to get help from a Form.

So, here's the way it works now:

Figure 19. Using Invoke to go to UI thread and create form

The code in the OptionsForm is as follows:

void _Application_OnAcceptSticky(Sticky obj)
{
   // This event is fired from background thread, so we need to slip stream to
   // main UI thread and call the same funtion
   if (base.InvokeRequired)
   {
      this.Invoke(new Action<Sticky>(this._Application_OnAcceptSticky),
         new object[] { obj });
   }
   else
   {
      _Application.Instance.Stickies.Add(obj);
   }
}

When the Add method is called, UIServiceProvider picks up the event and creates a new StickyForm. The whole application behaves the same way, as if the new sticky has been created locally by the current user. This is the beauty of having an Application Automation object model. Without making major changes in the object model, we can introduce major features in the application, which reuses everything that is already working properly.

The next step is to send a sticky to another user. In order to send a sticky, we need to spawn a background thread so that the UI is not blocked while the sticky is being transmitted. This transmission sometimes takes time if you have a 5 MB attachment with the sticky. So, we will be using the new BackgroundWorker component in the .NET Framework 2.0. Let's first see how we use the TcpClientChannel to call a method on another computer via Remoting:

public static void SendSticky(Sticky sticky, Friend friend)
{
   // Specify who am I, so that friends can reply to me
   sticky.FromFriend = new Friend(
      _Application.Instance.Options.MyName,
      _Application.Instance.Options.MyAddress,
      _Application.Instance.Options.Port);

   // Get the remoting object from the friend's computer
   IStickOutServer server = GetServer(friend.Url);

The URL needs to be in this format:

tcp://server name/object name

Here, the server name is the name of the server or IP, and the object name is the name we used to register the StickOutServer class, which is typeof(IStickoutServer).Name.

When we have the URL, we get to hold on to the Remoting object on another computer by using:

IStickOutServer server = (IStickOutServer)Activator.GetObject( typeof(IStickOutServer), url);

The remaining work is simple—serialize the Sticky object and call the AcceptSticky method:

// Serialize the sticky as Xml string 
StringWriter writer = new StringWriter();
SerializationHelper.Serialize(writer,sticky);

// Send the Xml via remoting call
server.AcceptSticky(writer.ToString());

However, the call is a blocking call, which means that until the entire sticky is transmitted over the wire along with its attachments, the UI is stuck. This is why we need to call this from a background thread. In the .NET Framework 2.0, such multithreading has been simplified by introducing the BackgroundWorker component.

The BackgroundWorker Component

This component offers a very convenient way to provide multithreading in your UI. When you put this control in your Form or UserControl you get 3 events:

  • DoWork—Where you do the actual work. This method is executed in a different thread and you cannot access any UI element from this event or from any function that somehow originates a call from this event. In order to show some progress on the UI, you need to call the ReportProgress method.
  • ProgressChanged—This event is fired when you call the ReportProgress method on the worker component. This event is slipstreamed to the UI thread, so you are free to use the UI components. However, what you do here blocks the main UI thread, so you need to do a small amount of work. Also remember not to call ReportProgress too frequently because the thread switching is a complex process.
  • RunWorkerCompleted—This is fired when you are done with the execution in the DoWork event. This function is called from the UI thread, so you are free to use UI controls from this function.

Here's the DoWorker method definition:

private void worker_DoWork(object sender, DoWorkEventArgs e)
{
   // This method will run on a thread other than the UI thread.
   // Be sure not to manipulate any Windows Forms controls created
   // on the UI thread from this method.

   this.BackgroundThreadWork();
}

I generally include the "Background" word in the methods that are called from here. This gives me a visual reminder that I am not in the UI thread anymore. Also, any other function called from such a function is also marked with the "Background" word.

Let's see how the whole process works in StickOut. In the Sticky class we add a method, void Send(List<Friend> friends). When it is called, it raises the OnSend event. The StickyForm representing that stick object captures this event and launches a BackgroundWorker component to execute the Remoting call in order to transmit the sticky.

void _MySticky_OnSend(object sender, SendStickyEventArgs e)
{
   BackgroundWorker worker = new BackgroundWorker();
   // Create a new worker
   this.components.Add(worker);

   this.tooltipProvider.Show("Sending...", this);
   
   worker.DoWork += new DoWorkEventHandler(worker_DoWork);
   worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
   worker.RunWorkerAsync(e);
}

After the RunWorkerAsync call, the DoWork event is fired:

private void worker_DoWork(object sender, DoWorkEventArgs e)
{
   // This method will run on a thread other than the UI thread.
   // Be sure not to manipulate any Windows Forms controls created
   // on the UI thread from this method.

   e.Result = SendStickyToFriendsInBackground(e.Argument as SendStickyEventArgs);
}
// Note: Code from this function disappears everytime I make changes
// in the design of the form. So, incase you see "Send To..." feature
// not working, copy this code inside the function:
// e.Result = SendStickyToFriendsInBackground(e.Argument as SendStickyEventArgs);

Whether this method executes properly or an exception is raised, the RunWorkerComplete event is always fired. From there you can update the UI according to the success or failure. If there is any exception, you will get the exception from e.Error.

private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
   if (null == e.Result)
   {
      this.tickLabel.Visible = true;
      this.tooltipProvider.Show("Sticky sent successfully", this);
   }
   else
   {
      /// Show error message for each failed friend.
      StringBuilder resultBuffer = new StringBuilder();

      /// Get the error mesage for each friend and show as friend-error pair
      Dictionary<Friend, string> errors = e.Result as Dictionary<Friend, string>;
      Dictionary<Friend, string>.Enumerator enu = errors.GetEnumerator();
      while (enu.MoveNext())
      {
         KeyValuePair<Friend, string> item = enu.Current;

         resultBuffer.AppendFormat("Friend: {0}, Error: {1}", item.Key.Name, item.Value);
         resultBuffer.Append(Environment.NewLine);
      }

      this.errorIcon.Visible = true;
      MessageBox.Show(this, resultBuffer.ToString(), "Error while sending",
         MessageBoxButtons.OK, MessageBoxIcon.Error);
   }

   // Dispose the background worker because we need to occupy as less
   // resources as possible.
   BackgroundWorker worker = (sender as BackgroundWorker);
   worker.DoWork -= new DoWorkEventHandler(worker_DoWork);
   worker.RunWorkerCompleted -= new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
   worker.Dispose();
   this.components.Remove(worker);
}

A normal way of using BackgroundWorker is to drag a BackgroundWorker component on your form from the Toolbox. However, I wanted to save resources as much as possible, so I did not keep the BackgroundWorker component alive after using it. This is why I have created the component at runtime and manually disposed of it from the code in order to save as many resources as possible. This is not always necessary.

Adding the Application to Windows Startup

If you want an application to be run during Windows Startup, create a registry key in this place:

Software\\Microsoft\\Windows\\CurrentVersion\\Run

Your application's name is the name of the key, and the value is the path of the application.

There is a RegistryHelper class in the attached source code that offers this feature.

.NET Startup Application Loader

Loading an application at Windows startup, especially a .NET application, is a serious pain for Windows. Normally if you try to load a Windows Forms application for the first time, you will see how slow it is and how much hard disk activity it takes. Loading such an application as this at startup, when Windows is doing a lot of others tasks, is going to make overall booting up much slower and make users lose patience.

That's why I have made an application loader in Visual Basic 6 that takes only 20KB and loads in the blink of an eye. This Visual Basic 6 application is put at startup instead of the .NET application. When StickOut is set to load at startup by the user, the Visual Basic 6 application waits five minutes for Windows to complete its other startup tasks before launching the .NET application.

Here's the Visual Basic 6 code of the loader:

Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Sub Main()
    
    On Error GoTo ExitDoor
    
    ' The first command line argument specifies the delay and the
    ' second argument specifies the exe to launch
    Dim strArguments() As String
    strArguments = Split(Command$, " ")
    
    Dim lngDelay As Long
    lngDelay = Val(strArguments(0))
    
    Dim strFile As String
    strFile = Trim(strArguments(1))
    
    ' Pause for the amount of time
    Call Sleep(lngDelay * 1000)
    
    ' Run the program
    Shell strFile
    
ExitDoor:
    
    
End Sub

Day 5 – Outlook Integration

August 18, 2005 6:30 PM

Outlook Add-in Development

Microsoft has released Outlook Add-in Loader for Visual Studio Tools for Office 2005, which significantly reduces Outlook Add-in post-deployment trauma. You no longer have Outlook stuck in memory and unable to close properly. This frequent and annoying problem has been solved by the introduction of a brilliant approach to the Outlook Add-in loading process. You can find details about this in the following articles:

Introducing Outlook Add-in Support in Visual Studio 2005 Tools for Office

Creating an Outlook Task Add-in Solution with Visual Studio 2005 Tools for Office

Architecture of the Outlook Add-in Support in Visual Studio 2005 Tools for Office

First you need to download and install Outlook Add-in Loader. To create a new Outlook Add-in project, in the New Project dialog box, under Office, select Outlook Add-in. Once you create a new project, you will find a ThisApplication class in your project, which will have the following code:

private void ThisApplication_Startup(object sender, System.EventArgs e)
{
   this.SetupToolbar();
}
private void ThisApplication_Shutdown(object sender, System.EventArgs e)
{
}

You no longer use the IDTExtensibility2 interface and write code in OnConnection or OnStartupComplete. This new approach is much simpler to use.

Let's look at the StickOut Outlook Add-in features:

  • You can stick anything from Outlook to the desktop as a sticky note.

Once installed, the Add-in adds a new toolbar:

Figure 20. StickOut Outlook add-in

First we set up the toolbar:

private void SetupToolbar()
{
   // First, get access to the CommandBars on the active explorer. 
   CommandBars commandBars =
     this.ActiveExplorer().CommandBars;

   // Check if the bar is already there. This will happen if the Add-in is deactivated
   // and loaded again without closing Outlook.
   bool commandBarExists = false;
   foreach (CommandBar bar in commandBars)
      if (bar.Name == COMMAND_BAR_NAME)
      {
         commandBarExists = true;
         this._MyCommandbar = bar;
      }

   // If bar is not there, then add the bar.
   if (!commandBarExists)
      this._MyCommandbar = commandBars.Add(COMMAND_BAR_NAME,
         MsoBarPosition.msoBarTop,
         System.Reflection.Missing.Value,
         true); // Temporary = True, means the command bar is not permanent 

   this._MyCommandbar.Visible = true;

   // Create the buttons on the command bar

   this._StickOutButton = this.CreateButton(this._MyCommandbar,
       STICK_OUT_BUTTON_NAME, STICK_OUT_BUTTON_TEXT, STICK_OUT_BUTTON_TEXT);
   this._StickOutButton.Click += 
      new _CommandBarButtonEvents_ClickEventHandler(
            _StickOutButton_Click);
   this.SetPicture(this._StickOutButton, "ImportTasks.bmp", "ImportTasksMask.bmp");

   this._NewStickyButton = this.CreateButton(this._MyCommandbar, 
   NEW_STICKY_BUTTON_NAME, NEW_STICKY_BUTTON_TEXT, NEW_STICKY_BUTTON_TEXT);
   this._NewStickyButton.Click += 
   new _CommandBarButtonEvents_ClickEventHandler(_NewStickyButton_Click);
   this.SetPicture(this._NewStickyButton, "PublishTasks.bmp", "PublishTasksMask.bmp");
}

Here's one catch: if you want to add transparent icons in the CommandButton, you will soon discover that .ico or .gif file formats with transparency do not work. You can only add bitmaps. The only way to do it is to provide a bitmap for the icon and a mask bitmap that shows the transparent parts.

Another catch to the button.Picture type is IPictureDisp, which is not the regular Image or Bitmap class of the .NET Framework. So, you cannot feed the regular Bitmap class to this property. You need to convert it to IPictureDisp first. In order to do this, you need to have a class that converts Image to IPictureDisp.

using System;
using System.Drawing;
using System.Windows.Forms;
using stdole;

public class AxHost2 : AxHost
{
   public AxHost2()
      : base(null)
   {
   }
   public new static IPictureDisp GetIPictureDispFromPicture(Image image)
   {
      return (IPictureDisp)AxHost.GetIPictureDispFromPicture(image);
   }
}

Set button.Picture as follows:

using (Bitmap picture = new Bitmap(
   Assembly.GetExecutingAssembly().GetManifestResourceStream(
      "StickOut.OutlookAddin." + pictureName)))
{
   button.Picture = (IPictureDisp)AxHost2.GetIPictureDispFromPicture(picture);
}
using (Bitmap mask = new Bitmap(
   Assembly.GetExecutingAssembly().GetManifestResourceStream(
      "StickOut.OutlookAddin." + pictureMaskName)))
{
   button.Mask = (IPictureDisp)AxHost2.GetIPictureDispFromPicture(mask);
}

Next design the icon:

Make one bitmap where the transparent area is white RGB (255,255,255).

Make another mask bitmap where the transparent area is white, but the visible area is filled with RGB (0,255,0), or pure green.

Now we call the StickOut stand-alone application from the Outlook Add-in. We need to use Remoting for this call. First we get the server's reference:

private IStickOutServer GetServer()
{
   if (0 == ChannelServices.RegisteredChannels.Length)
   {
      ChannelServices.RegisterChannel(new TcpChannel());
   }
   
   IStickOutServer server = (IStickOutServer)Activator.GetObject(
   typeof(IStickOutServer), string.Format("tcp://localhost:{0}/{1}",
       54321, typeof(IStickOutServer).Name) );

   return server;
}

Then we call GetServer().NewSticky(); to show a new sticky.

Sticking Items from Outlook to the Desktop

You can select e-mails, notes, and tasks from Outlook and then click Stick Out on the toolbar to create desktop sticky notes from them. This is done as follows:

  1. For each selected item, get the body and subject of the content.
  2. If the item contains attachments, save them to temporary files.
  3. Create a new Sticky object and fill it with Body and Subject.
  4. For each attachment, create a new Sticky Attachment and attach the temporary file with it.
  5. Send the sticky via Remoting.

When the user clicks the Stick Out button, we get the selected items:

foreach (object obj in this.ActiveExplorer().Selection)
{
   using (Item item = (Item)COMWrapper.Wrap(obj, typeof(Item)))
   {   
      try
      {

I will be explaining the COMWrapper class shortly; the next step is to check for attachments:

GenericCollection<StickOut.ObjectModel.Attachment> stickyAttachments = 
   new GenericCollection<StickOut.ObjectModel.Attachment>();

try
{
   /// Let's try getting the "Attachments" property of the item. If failed, then this type
   /// of item does not support attachments. For example, Outlook Note
   using (Attachments attachments = item.Attachments)
   {   
      System.Collections.IEnumerator enumerator = attachments.GetEnumerator();
      
      while( enumerator.MoveNext() )
      {

Now for each Outlook Attachment, we create one Sticky Attachment:

using (StickOut.ManagedOffice.Attachment attachment = (StickOut.ManagedOffice.Attachment)
   COMWrapper.Wrap(enumerator.Current, typeof(StickOut.ManagedOffice.Attachment)))
{
   /// Create a temporary file and write the content of the attachment to it
   string tempFile = Path.Combine(Path.GetTempPath(), attachment.FileName);
   attachment.SaveAsFile(tempFile);

   /// Create a new embedded attachment which will contain the binary data 
   StickOut.ObjectModel.Attachment newAttachment =
      new StickOut.ObjectModel.Attachment(attachment.DisplayName, tempFile,
      data.Length, null, null);

   /// Embed the data from the file inside it
   newAttachment.Embed();

Now, there is an issue with the file icons. StickOut will receive the Attachment as a binary stream and the temporary files will be deleted as soon as the Sticky is sent, so there's no way StickOut can find the associated icons for the files. So, we need to get the icons and send them as an embedded byte stream inside the attachment.

/// Get the system icon for this type of file and store the icon data
/// inside the attachment. 
using (Icon icon = Icon.ExtractAssociatedIcon(tempFile))
{
   using (MemoryStream stream = new MemoryStream())
   {
      icon.Save(stream);
      newAttachment.IconData = stream.ToArray();
   }
}

The remaining work is easy, we just need to serialize the Sticky object and send it via Remoting:

Sticky mySticky = new Sticky();
mySticky.Title = item.Subject;
mySticky.Body = item.Body;
// mySticky.Rtf = item.RTFBody; <- Not supported
mySticky.Attachments = stickyAttachments;

/// Serialize the sticky as an Xml string and pass it to 
/// the remoting server.
using (StringWriter stringWriter = new StringWriter())
{
   Serialize(stringWriter, mySticky);
   server.AcceptSticky(stringWriter.ToString());
}

Making COM Objects Disposable

In the previous code, you see I am using all COM objects inside the using(...) block. This is done with a technique I explain in my article located on Codeproject.com.

There are several problems using COM from .NET:

  • You cannot implement the Dispose pattern by using the "using" block in order to safely dispose of the COM reference.
  • You cannot guarantee that the COM references are finalized. There's no way to implement ~Destructor() for COM references.
  • A COM reference is not released when the call to Marshal.ReleaseComObject is skipped due to an exception.
  • When you Add Reference... to a COM library, the reference is version-specific. So, if you add a reference to the Office 2003 COM library, it does not work properly when deployed to Office 2000.
  • The only solution to version-independent COM is to use Late Bound operations, but you miss all the features of a strongly typed language.

All these problems are resolved in my article located on CodeProject.com. You can safely use COM objects inside the using block, ensuring that the reference to the underlying COM object will be released as soon as the using block is complete.

So, with this technique, you can write code such as this:

using( Application app = 
             ( Application)DisposableCOMProxy.Create( "Outlook.Application", 
               typeof( Application ) ) )
        {
               Debug.WriteLine( app.Name );

               using( Explorer explorer = app.ActiveExplorer() )
               {
                       Debug.WriteLine( explorer.Caption );
               }
        }

Even if you forget to call Dispose on COM objects, the destructor will release the reference. It's an advanced topic and you need to read my article located on CodeProject.com in order to understand the details.

E-mail Sticky via COM Automation

We can now send items from Outlook to StickOut. Next we will be doing the opposite, creating e-mail in Outlook directly from a Sticky note so that you can just right-click on a sticky note, select E-mail, and the Outlook e-mail compose dialog box opens. This is done using COM automation:

using (StickOut.ManagedOffice.Application app = OutlookApplication.Create())
{
   // Compose an email
   using (MailItem mail = (MailItem)COMWrapper.Wrap(
      app.CreateItem(OlItemType.olMailItem),
      typeof(MailItem)))
   {
      mail.Display(Missing.Value);

      mail.Body = sticky.Body;
      mail.Subject = sticky.Title;
      
      using (Attachments mailAttachments = mail.Attachments)
      {
         foreach (StickOut.ObjectModel.Attachment attachment in sticky.Attachments)
         {
            if (attachment.IsEmbedded)
            {
               // Write content of the attachment in a temporary file
               string tempFileName = attachment.Save();

               // Add the temporary file as attachment
               mailAttachments.Add(tempFileName, OlAttachmentType.olByValue,
                  Missing.Value, attachment.Name);

               File.Delete(tempFileName);
            }
            else
            {
               mailAttachments.Add(attachment.FullName, OlAttachmentType.olByValue,
                  Missing.Value, attachment.Name);
            }
         }
      }
   }
}

Again, here you see the use of disposable COM references, which you can find in my article located on CodeProject.com.

Outlook to StickOut Calls Using IPC Channel

The .NET Framework 2.0 comes with a new Remoting channel called IpcChannel. IPC stands for "Inter-process Communication". It works by using Named Pipes. This is a super fast communication channel for communication between two different processes within a computer. It's almost 100 times faster than TcpChannel. As Outlook and StickOut are two different processes that run within the same computer, IpcChannel is the better choice for inter-process communication than TcpChannel.

Setting up IpcChannel is the same as TcpChannel and its usage is also the same except for the portion of the URL where you need to mention "ipc://" instead of "tcp://".

IpcChannel ipcChannel = new IpcChannel("StickOut");
ChannelServices.RegisterChannel(ipcChannel);

On the client side, you connect using the following code:

ChannelServices.RegisterChannel(new IpcClientChannel());
IStickOutServer server = (IStickOutServer)Activator.GetObject(
            typeof(IStickOutServer), string.Format("ipc://StickOut/{0}", 
            typeof(IStickOutServer).Name) );

Please note that you cannot connect to a different computer using IpcChannel. Also the format of the URL is:

ipc://the name server used to create IPC Channel/name of the object

Deploy Outlook Add-in

Once you have built the Outlook Add-in setup project, if you run it in a new computer that has Microsoft Office 2005 installed, you will see that your add-in does not load. This is because it does not have the Outlook Add-in Loader for Visual Studio Tools 2005 installed. First you need make sure that you have the .NET Framework 2.0 installed. Then you need to install the Outlook Add-in Loader for Visual Studio Tools 2005 and run setup.

Day 6 – Smaller, Better, Faster

August 19, 2005 11:30 AM

Today is the day to make StickOut less resource intensive, more user friendly, and squeeze out every drop of performance from it. First we start with reducing resource consumption by reducing the number of GDI and USER handles it occupies.

Reduce Resource Consumption by Removing All Controls

StickOut uses too many resources. With Process Explorer from SysInternals, it takes hundreds of GDI handles for 4 or 5 sticky forms. Also it takes around 25MB physical memory, which is absolutely unacceptable for a tiny application like this. So, the first step is to reduce resource consumption by eliminating unnecessary controls.

Each Sticky form comes with a RichTextBox, a ListView, and an ImageList control. All these are resource-intensive controls. If you have 10 sticky notes on your desktop, you have 10 copies of each of these controls. It is not appropriate to keep them in the user's machine memory when StickOut is rarely used. So, when StickOut is not being used, if we take a screen shot of the Sticky Form, remove all controls, and then put the screen shot as the Form's background, nobody will notice that it's a screenshot, not the actual form. When a user clicks on any of the forms, we recreate those controls only on that form and the user can keep working without noticing what just happened behind the scene. Again, when the user leaves the form and goes to another application, we can again take a new screenshot and remove all controls from the form and put the captured picture inside the form.

The form's Deactivate event is fired whenever a Form loses focus. So, this is the right place to take a screenshot of the Form and remove all controls from it. The screenshot is taken using Win32 API:

internal static class ScreenCaptureHelper
{
   [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
   private static extern bool BitBlt(
      IntPtr hdcDest, // handle to destination DC
      int nXDest, // x-coord of destination upper-left corner
      int nYDest, // y-coord of destination upper-left corner
      int nWidth, // width of destination rectangle
      int nHeight, // height of destination rectangle
      IntPtr hdcSrc, // handle to source DC
      int nXSrc, // x-coordinate of source upper-left corner
      int nYSrc, // y-coordinate of source upper-left corner
      System.Int32 dwRop // raster operation code
      );

   public static Bitmap CaptureForm(Form form)
   {
      Graphics g1 = form.CreateGraphics();         
      
      Bitmap destImage = new Bitmap(form.ClientRectangle.Width,
          form.ClientRectangle.Height, g1);
      Graphics g2 = Graphics.FromImage(destImage);

      IntPtr dc1 = g1.GetHdc();
      IntPtr dc2 = g2.GetHdc();
      
      BitBlt(dc2, 0, 0, form.ClientRectangle.Width,
          form.ClientRectangle.Height, dc1, 0, 0, 13369376);
      
      g1.ReleaseHdc(dc1);
      g2.ReleaseHdc(dc2);

      return destImage;
   }

CaptureForm takes a Form and captures its current state and returns a Bitmap. You can then set the Bitmap as a Form's BackgroundImage. So, the Form_Deactivate event looks like this:

private void StickyForm_Deactivate(object sender, EventArgs e)
{
   base.BackgroundImage = ScreenCaptureHelper.CaptureForm(this);
   this.DisposeAllControls();
}
private void DisposeAllControls()
{
   while (base.Controls.Count > 0)
   {
      Control c = base.Controls[0];
      base.Controls.RemoveAt(0);
      c.Dispose();
   }
}

As a result, when you are not using StickOut, there's nothing but some empty forms with a background picture. This frees up a lot of GDI and USER handles.

Now we need to bring back all the Form controls on the Activate event. This is done by calling the InitializeControls() function that the Windows Forms Designer already generated for you.

However, this results in a poor user experience because disposing of all controls takes time, and creating them back on the Form also takes time. As a result, every time a user activates a Sticky form or leaves it, there's a considerable delay and screen flicker. So, this solution is not appropriate.

Lightweight DummyForm

The final solution is to have only one StickyForm with all the controls and everything intact, and create blank dummy forms with no controls inside them to work as placeholders for the captured StickyForm image for each sticky. When a form gets activated, the singleton StickyForm moves to that sticky's place and shapes itself according to the sticky. The dummy form on that place is hidden from view. When the user is done with working on the form, a screenshot is taken and put on the dummy form, and the StickyForm becomes invisible. Now the Dummy Form with the latest screenshot of the StickyForm becomes visible on the sticky area, giving the user an impression that the StickyForm is still there.

This requires no control addition or removal, and thus eliminates screen flicker. Also, we have only one active form that does not require more memory as you create more sticky notes.

Drag-and-Drop Code Is Broken

This approach broke the drag-and-drop code because when the user drags items on the DummyForm, it's not the StickyForm that is receiving the DragEnter event; instead, it's the DummyForm. So, we need to somehow pass the DragEnter event to the StickyForm. Luckily this works out without much complication. First, the DummyForm is set to AllowDrop=true so that it receives the DragEnter event. Whenever the DragEnter event is fired, the singleton StickyForm is brought to its place and made visible. As a result, the StickyForm starts receiving the drag events and the DragEnter and DragDrop event works perfectly.

Memory Helper

The MemoryHelper class is the last weapon in my arsenal that reduces physical memory consumption significantly by reducing the application's Process WorkingSet. WorkingSet is a property of the Process class that contains how much memory the process is consuming. It is not a read-only variable; you can increase or decrease it. If you try to decrease it, you will notice the memory usage goes down significantly. Although it does not always work, we can give it a try whenever it might help.

internal static void ReduceMemory() 
{ 
   try
   {
      Process loProcess = Process.GetCurrentProcess(); 
      if(_Toggle) 
      { 
         loProcess.MaxWorkingSet = (IntPtr)((int)loProcess.MaxWorkingSet - 1); 
         loProcess.MinWorkingSet = (IntPtr)((int)loProcess.MinWorkingSet - 1); 
      } 
      else 
      { 
         loProcess.MaxWorkingSet = (IntPtr)((int)loProcess.MaxWorkingSet + 1); 
         loProcess.MinWorkingSet = (IntPtr)((int)loProcess.MinWorkingSet + 1); 
      } 
      _Toggle = !_Toggle; 
   }
   catch( Exception x )
   {
      Debug.WriteLine( x );
   }
}

You need to have Administrator privileges in order to make this code work. This gives us significant memory reduction, from 25 MB down to 2 MB!

Figure 21. StickOut memory usage

Make the UI More Responsive with Delayed Method Calls

Sometimes a user does something that results in an event repeatedly firing in a short time. For example, when a user is resizing a form, the user is firing the Resize event frequently. If you try to update your object model or do something that takes a bit of time during these events, your UI will get stuck frequently and the user will not be able to resize the form smoothly. For example, in StickOut, whenever you resize the form, the Bounds property of the Sticky object is changed. As soon as it is changed, the OnChange event fires and this triggers XmlStickyStore to save the Sticky. As a result, when the user is resizing the form, the Sticky object is being continuously saved and the user is getting interrupted frequently.

It would be nice if we could call methods like this:

[WaitFor(1000)] DoSomething( p1, p2, p3 );

This would queue the method call for 1000 milliseconds and fire it when the time is elapsed.

But unfortunately we can't. However, there are many approaches to work around this. A simple approach is to turn on a Timer in such repeated events and mark the function that needs to be called when the timer fires. The timer will tick after one second and call that function.

So, first we create a private delegate on the Form:

// Set the method that needs to be called by the timer after a while
private MethodInvoker _DelayedMethodCall;

Then on the Resize event, we start the Timer:

// Call this method after 1 second
this._DelayedMethodCall = new MethodInvoker(this.PopulateSticky);
timer.Stop();
timer.Start();

We need to call the Stop method in order to initialize the timer, which sets its count to zero. Otherwise it ticks while the user is resizing the form and that cancels the resize action.

On the timer_tick event, we call the delegate:

private void timer_Tick(object sender, EventArgs e)
{
   timer.Stop();

   if( null != this._DelayedMethodCall ) this._DelayedMethodCall();
   this._DelayedMethodCall = null;         
}

This gives the user a smooth resize experience.

Better Window Resize

I just realized my Dad can't resize sticky forms, and if he can't, then 90% of the people will have the same trouble. The solution we have seen before for resizing the borderless sticky form is too difficult for him because the resize border around the form is just too thin for most people.

Figure 22. Difficult-to-resize sticky forms

The 2-pixel–wide border is too difficult for people because not everyone has precise control over the mouse. So, more space around the form is needed. But increasing the size of the border results in this:

Figure 23. Non-glamorous sticky form

It does not look good at all, especially in medium resolution screens like 1024X768. The solution needs to have 2 characteristics:

  • The form border will be physically 1 or 2 pixels wide.
  • The border will be logically 8 pixels wide around the form.

In order to do this, one solution is to capture mouse events on the controls inside the form and propagate to the form's mouse event handlers. But this results in a complicated calculation based on the location of the mouse. The solution I have finally made is to use the IMessageFilter interface. This interface allows you to put a hook into Global Mouse events using the System.Windows.Forms.Application.AddMessageFilter(IMessageFilter) function. You can then use any class to receive mouse events even if the mouse is in another application. This is a global hook for all mouse events no matter where the mouse is currently located. So, this solution can be used to solve the thin border problem in this way:

  • Capture the mouse and see whether the mouse is on the sticky form.
  • If the mouse is inside the sticky form, then see if the mouse is on the 8-pixel logical border around the form.
  • If it is on the logical border, set the mouse cursor to one of the resize cursors and allow resize.

Here's the code that does it all:

class WindowResizeHelper : IMessageFilter
{
   private Form _TheWindow;

   public Form TheWindow
   {
      get { return _TheWindow; }
      set { _TheWindow = value; }
   }

   public WindowResizeHelper(Form form)
   {
      this._TheWindow = form;
      Application.AddMessageFilter(this);
   }

The constructor takes a form for which it will provide the resize service and then hooks into the application-wide message filter. Whenever there's a message, the IMessageFilter.PreFilterMessage is called.

   #region IMessageFilter Members

   bool IMessageFilter.PreFilterMessage(ref Message m)
   {
      if (!this._TheWindow.IsDisposed && this._TheWindow.Visible)
      { // Warning, don't access anything on the form when it is disposed
         Point pt = this._TheWindow.PointToClient(Control.MousePosition);
         Rectangle formBounds = this._TheWindow.ClientRectangle;
         if (formBounds.Contains(pt))
         {

The cursor is not inside the form area for which we are providing resizing. Now, we need to find out whether the cursor is on the 8-pixel-wide virtual border around the form:

            // Create a rectangular area which is 5 pix smaller than windows's size
            // This is the area beyond which we need to simulate non client area
            Rectangle bounds = this._TheWindow.ClientRectangle;
            bounds.Inflate(-7, -7);

            int htValue = Win32.HT.HTNOWHERE;
            Cursor cursor = Cursors.Default;

            // If the cursor is outside this inner rectangle, we need to
            // check for resize
            if (!bounds.Contains(pt))
            {

Yes, the cursor is now on the virtual border. So, we need to find which corner the mouse is on now and then set the resize cursor accordingly:

               if (pt.X < bounds.Left) // Cursor left side
               {
                  if (pt.Y < bounds.Top)
                  {
                     // Cursor top left
                     cursor = Cursors.SizeNWSE;
                     htValue = Win32.HT.HTTOPLEFT;
                  }
                  else if (pt.Y > bounds.Bottom)
                  {
                     // Cursor bottom left
                     cursor = Cursors.SizeNESW;
                     htValue = Win32.HT.HTBOTTOMLEFT;
                  }
                  ...

This way we check for all 8 corners. Now, we are interested in doing something only when the mouse is either moving or the left mouse button is pressed. We must ignore all other messages so that the default operations are not hampered.

               if (m.Msg == Win32.WM.WM_MOUSEMOVE)
               {
                  // the cursor is already set, we have nothing to do
                  this._TheWindow.Cursor = cursor;
                  return true; // The message is handled
               }
               else if (m.Msg == Win32.WM.WM_LBUTTONDOWN)
               {
                  // Start resizing
                  Win32.ReleaseCapture();
                  Win32.SendMessage(this._TheWindow.Handle, Win32.WM.WM_NCLBUTTONDOWN,
                     htValue, 0);
                  return true; // The message is handled               
               }
               else
               {
                  return false; // The message is NOT handled               
               }
            }
         }
      }
      return false;
   }
   #endregion

On a borderless form's constructor, create an instance of this class and pass the form as a reference to the constructor's parameter. The form will become resizable.

Word and Excel Add-in

A lonely desktop application does not have much market value unless you integrate your product with Microsoft Office applications. If you have a competitor who makes the same product but you make one a little better, your competitor will do better business than you if they can provide integration with Microsoft Office applications. People love to have their favorite applications accessible from Word/Excel/PowerPoint/FrontPage or Outlook. So, in order for me to do better business with StickOut in the desktop sticky note application market, I need to integrate it with Office applications. Only then can I say to others, "See? That product doesn't integrate with Office applications, but mine does. So you should use my product, not that one."

On the File menu, click New Project, select Extensibility from the Project types, and then select Shared Add-in. In the Shared Add-in Wizard, I selected Word and Excel only.

Figure 24. Office shared Add-in

On the Choose Add-in Options screen, select the I would like my Add-in to load when the host application loads check box. This makes debugging a lot easier.

Figure 25. Load add-in at startup

A project is created along with a setup project to install the add-in. On the Connect.cs write the following code inside the OnConnection method:

public void OnConnection(object application, 
   Extensibility.ext_ConnectMode connectMode, object addInInst, 
      ref System.Array custom)
{
   applicationObject = application;
   addInInstance = addInInst;

   if (connectMode != Extensibility.ext_ConnectMode.ext_cm_Startup)
   {
      OnStartupComplete(ref custom);
   }
}

Similarly, OnDisconnect:

public void OnDisconnection(Extensibility.ext_DisconnectMode
    disconnectMode, ref System.Array custom)
{
   if (disconnectMode != Extensibility.ext_DisconnectMode.ext_dm_HostShutdown)
   {
      OnBeginShutdown(ref custom);
   }
}

The OnStartupComplete method does the work to make the add-in functional:

public void OnStartupComplete(ref System.Array custom)
{
   if (applicationObject is Microsoft.Office.Interop.Excel.Application)
   {
      this._Addin = Director<ExcelAddin, Microsoft.Office.Interop.Excel.Application>
         .Build(applicationObject as Microsoft.Office.Interop.Excel.Application);
   }
   else if (applicationObject is Microsoft.Office.Interop.Word.Application)
   {
      this._Addin = Director<WordAddin, Microsoft.Office.Interop.Word.Application>
         .Build(applicationObject as Microsoft.Office.Interop.Word.Application);
   }      
}

Here we are using the Generic Builder we discussed earlier in order to instantiate a Word- or Excel-specific add-in. Remember, this add-in is loaded by both Microsoft Word and Microsoft Excel. So, we need to know which is our host and then act accordingly. Although our work is 98% identical for both add-ins, due to different types of object models for the host applications, there is some difference in the way we use them.

First we create the CommandBar with two buttons, just like the Outlook Add-in:

public override void Build(Application app)
{
   this._Application = app;

   CommandBar bar = app.CommandBars.Add(COMMAND_BAR_NAME,
       MsoBarType.msoBarTypeMenuBar, Missing.Value, true);
   bar.Visible = true;

   this._NewStickyButton = CommandBarHelper.CreateButton(bar,
       COMMAND_BUTTON_NEW_STICKY_NAME, COMMAND_BUTTON_NEW_STICKY_TEXT,
          COMMAND_BUTTON_NEW_STICKY_TEXT);
   this._StickOutButton = CommandBarHelper.CreateButton(bar,
    COMMAND_BUTTON_STICKOUT_NAME, COMMAND_BUTTON_STICKOUT_TEXT, 
      COMMAND_BUTTON_STICKOUT_TEXT);

   CommandBarHelper.SetPicture(this._NewStickyButton, "ImportTasks.bmp",
       "ImportTasksMask.bmp");
   CommandBarHelper.SetPicture(this._StickOutButton, "PublishTasks.bmp",
       "PublishTasksMask.bmp");

   this.SubcribeToButtonClick();
}

Now we need to subscribe to the button-click handlers:

private void SubcribeToButtonClick()
{
   this._NewStickyButton.Click += 
   new _CommandBarButtonEvents_ClickEventHandler(newStickyButton_Click);
   this._StickOutButton.Click += 
   new _CommandBarButtonEvents_ClickEventHandler(stickOutButton_Click);
}

private void UnsubcribeToButtonClick()
{
   try
   {
      this._NewStickyButton.Click -= 
      new _CommandBarButtonEvents_ClickEventHandler(newStickyButton_Click);
      this._StickOutButton.Click -= 
      new _CommandBarButtonEvents_ClickEventHandler(stickOutButton_Click);
   }
   catch { }
}

On the button click, do the following:

  • Copy the selected text to the clipboard. This copies the text with all the styles and also the pictures as RTF to the clipboard.
  • Get the clipboard content as RTF.
  • Create a new sticky and set the RTF.
  • Use Remoting to call the StickOut application to add this new sticky.
void stickOutButton_Click(CommandBarButton Ctrl, ref bool CancelDefault)
{
   Range selection = this._Application.Selection as Range;
   if (null == selection) return;
   System.Windows.Forms.Clipboard.Clear();
   selection.Copy(Missing.Value);
   string selectedRtf = System.Windows.Forms.Clipboard.GetData(
      System.Windows.Forms.DataFormats.Rtf) as string;
   string selectedText = System.Windows.Forms.Clipboard.GetData(
      System.Windows.Forms.DataFormats.Text) as string;

   Sticky newSticky = new Sticky(this._Application.ActiveWorkbook.Name, 
      selectedText, System.Drawing.Rectangle.Empty,
      Color.Empty, null);
   newSticky.Rtf = selectedRtf;

   StringWriter writer = new StringWriter();
   Helper.Serialize(writer, newSticky);
   this.GetServer().AcceptSticky(writer.ToString());

   // Click event handler gets detached everytime
   this.UnsubcribeToButtonClick();
   this.SubcribeToButtonClick();
}

It is important to notice that the Click handlers get detached every time we do the Remoting call. In fact, I have noticed from prior experience that, even if I show a dialog window from within the add-in, the handlers get detached and no longer receive the Click event. So on every click, I have to detach the handlers (if it is not detached already) and then reattach them to the buttons.

In order to run it from within Visual Studio, on the Project Properties Debug tab, select Start External Program and set the value to:

C:\Program Files\Microsoft Office\OFFICE11\EXCEL.EXE

Now you can click Run and you will see the add-in does not load at all.

Microsoft Office 2003 comes with strict security, so you cannot see the add-in loaded in Word or Excel. To allow add-ins, open Word, and on the Tools menu, click Macro, and then click Security. On the Security Level tab, select Medium. On the Trusted Publishers tab, clear the Trust all installed Add-ins and Templates check box. Close Word. If you have Outlook running, you will have to close Outlook, too, because it keeps an instance of Word in memory. Better to open the Task Manager (Ctrl+Shift+ESC) and on the Process tab, ensure there's no Winword.exe in the list. If there is any instance of Word that still appears after exiting Word and Outlook, delete it. Now you can either open Word again or click Run from within Visual Studio and this time, the add-in will load. If the add-in still does not load (in my case, it did not), then you need to do some registry tweaking using Regedit.exe. You will have to create a registry entry in the following location:

HKEY_CURRENT_USER\Software\Microsoft\Office\OfficeApp\Addins\ProgID

Here OfficeApp = Word and ProgID = StickOutOfficeAddin. You can find the ProgID from the Connect.cs:

[GuidAttribute("02AE500C-F52E-4EB0-8273-2C511BEA2BA4"), ProgId("StickOutOfficeAddin.Connect")]
public class Connect : Object, Extensibility.IDTExtensibility2
{

Create a new key using a DWORD value called LoadBehavior. This value determines how the add-in is loaded by the host application, and is made up of a combination of the following values:

  • ·0 = Disconnect—Is not loaded.
  • ·1 = Connected—Is loaded.
  • ·2 = Bootload—Load on application startup.
  • ·8 = DemandLoad—Load only when requested by user.
  • ·16 = ConnectFirstTime—Load only once (on next startup)

The typical value specified is 0x03 (Connected | Bootload).

According to the Knowledge Base article, How To Build an Office COM Add-in by Using Visual C# .NET:

Add-ins that implement IDTExtensibility2 should also specify a DWORD value called CommandLineSafe to indicate whether the add-ins are safe for operations that do not support a user interface. A value of 0x00 indicates False, and a value of 0x01 indicates True.

However, you can skip it. Now launch Word or click Run in the Visual Studio Integrated Development Environment (IDE) and the add-in will load.

Remember, if you have installed the setup project that is created for the add-in, these registry entries get created. So, an easy way to skip all this hassle is to just install the setup project. On the other hand, if you ever uninstall the setup, then the registry entries get deleted. As a result, you no longer see the add-in loaded even if you start it from Visual Studio.

Visual Studio Add-in

Visual Studio 2005 introduces a big change in the way add-ins are created and deployed. Let's start form the beginning:

Figure 26. Creating a Visual Studio add-in

Complete the Wizard and you will have a Connect.cs in your project. However, there's no setup project.

We will be adding a menu item to the code editor context menu so that you can select a block of text, make a sticky out of it, and then send it quickly to your friends.

Figure 27. Code editor context menu

Visual Studio maintains a list of commands that execute some operations. For example, commands such as File.NewProject, Edit.Copy, and Build.Build, etc., carry out the respective operations. This is a different approach than Microsoft Office products. In Microsoft Office, you add command buttons and subscribe to Click events. But in Visual Studio, we need to register a command, and then implement an interface to act upon the command execution. So, we have to register our own command such as "StickOut.StickOut" and handle the execution of this command. You can see the available commands from the Command Window inside Visual Studio:

Figure 28. Visual Studio command window

In order to receive notification about anyone calling a command, you need to implement the IDTCommandTarget interface. We will see how it is done, but first, let's get our own command registered and create a menu item in the code window context menu:

public void OnConnection(object application, 
   ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
   _Application = (DTE2)application;
   _AddinInstance = (AddIn)addInInst;

   if (connectMode == ext_ConnectMode.ext_cm_Startup)
   {
      Array contextGUIDS = new object[] { };
      Commands2 commands = (Commands2)_Application.Commands;
      CommandBars commandBars = _Application.CommandBars as CommandBars;

      try
      {
         // Delete the existing command incase if it is still there
         this.CleanupExistingCommand();

         //Add this to the context sensitive menu and not the main menu bar.
         CommandBar commandBar = (CommandBar)commandBars[CODE_VIEW_COMMAND_BAR_NAME];

         //Add a command to the Commands collection:
         Command command = commands.AddNamedCommand2(_AddinInstance,
            COMMAND_STICK_OUT, COMMAND_STICK_OUT_TEXT,
            COMMAND_STICK_OUT_DESCRIPTION, true, 59, ref contextGUIDS,
            (int)vsCommandStatus.vsCommandStatusSupported + 
               (int)vsCommandStatus.vsCommandStatusEnabled,
            (int)vsCommandStyle.vsCommandStylePictAndText, 
               vsCommandControlType.vsCommandControlTypeButton);

         command.AddControl(commandBar, 1);

         // Add a separator line between the commands added here and the other 
         // commands on the context (popup) menu.  
         commandBar.Controls[2].BeginGroup = true;

The process is similar to what we have seen in Microsoft Word or an Outlook add-in. The difference between Visual Studio .NET 2003 and Visual Studio 2005 is the new interface, Commands2, and the new method, AddNamedCommand2. In order to receive the notification, we need to implement the IDTCommandTarget interface, which has two methods:

  • QueryStatus—Lets Visual Studio know whether your command is applicable within the current context. For example, we can check whether there is any text selected in the code window. If there's nothing selected, we should disable the StickOut menu item.
  • Exec—Executes the command and does the job we as developers are supposed to do. Remember, the command can be executed by selecting the StickOut menu item or it can be executed from the Command window.

On QueryStatus, we give the Visual Studio IDE the green signal to go ahead and enable the menu item:

public void QueryStatus(string commandName, vsCommandStatusTextWanted 
   neededText, ref vsCommandStatus status, ref object commandText)
{
   if(neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
   {
      if(commandName == VS_COMMAND)
      {
         status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported
         |vsCommandStatus.vsCommandStatusEnabled;
      }
   }
}

On Exec, we do the actual work:

public void Exec(string commandName, vsCommandExecOption executeOption, 
   ref object varIn, ref object varOut, ref bool handled)
{
   handled = false;
   if(executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault)
   {
      if(commandName == VS_COMMAND)
      {
         this.StickThingsOut();

         handled = true;
         return;
      }
   }
}

Getting selected text from the code editor is the same as what we have done in the Microsoft Word add-in:

private void StickThingsOut()
{
   if (null == _Application.ActiveDocument) return;
   EnvDTE.TextSelection selection = _Application.ActiveDocument.Selection 
      as EnvDTE.TextSelection;

   if (null != selection && selection.Text.Length > 0)
   {            
      // Get the text and show in the output window. Just for debugging
      string text = selection.Text;
      this.DisplayInOutputWindow(text);

      // Copy the selected data to clipboard so that we get the RTF
      System.Windows.Forms.Clipboard.Clear();
      selection.Copy();

The attached code explains the details of how things work. You can also learn from the code how to create your own Output window and write text there.

To deploy your add-in you will have to copy the StickOutVisualStudioAddin.AddIn file (available in the solution) to a specific folder:

  • Visual Studio 2005 Beta 2 – The folder is:
    C:\Documents and Settings\Omar Al Zabir\Application Data\Microsoft\MSEnvShared\Addins\
  • Visual Studio 2005 July CTP
    C:\Documents and Settings\Omar AL Zabir\My Documents\Visual Studio 2005\Addins

This file describes the load behavior and the location of the add-in DLL. If you find that the add-in is not being loaded, there's a good chance that the DLL path is wrong.

Deploying and Testing Using Virtual Machine

Before deploying, you should test your product on different platforms, especially with different versions of Windows having different combinations of Office and Visual Studio and their service packs. Here's a list of combinations I have found useful:

  • Fresh Windows XP, 2000 and 2003
  • Windows XP SP2
  • Windows 2003 SP1
  • Windows 2000 SP4
  • Windows + Office 2000 / 2002 / XP / 2003 without Office Service Pack
  • Windows + Office + Latest version-specific service pack
  • Windows + Office + One earlier version of service pack for Office
  • Windows + Visual Studio 2003
  • Windows + Visual Studio 2003 + Office 2003 / 2002 / XP
  • Windows + .NET Framework 2.0 only
  • Windows + Visual Studio 2005 Beta 2
  • Windows + Visual Studio 2005 Latest CTP
  • Windows + Visual Studio + Office + Anti-Spyware + Antivirus + Latest media player + latest Internet Explorer Service Pack

It is difficult to arrange so many computer configurations. So, I have an 80GB hard drive with lots of virtual machines containing all such combinations. You can use either VMWare or Microsoft Virtual PC. Both offers incremental cloning of an existing virtual machine, which means, first you can create a virtual machine and install only Windows on it. Then clone that computer maintaining a link to the original virtual machine. This means the entire virtual machine is not duplicated; instead, a reference to the original VM is maintained. The new clone contains all data of the original VM, but whatever you do in the clone VM does not affect the original VM. So, you can create a root VM with only the operating system installed, and then create a tree of clones with different products installed. This saves hard drive space and installation time significantly because you do not need to create a brand new VM and start from Windows every time you create a new VM.

Whenever you want to deploy your application, create a temporary clone of one of the clones and check whether your deployment works or not. This way, you don't pollute the original clone.

Figure 29. Testing in Virtual Machines

Also, don't forget to test your applications in different resolutions, different windows themes, and Large Font Size. Moreover, preserve the VM where you have deployed a final release of a major version of your application that you send to your customers and do not modify it in anyway. In the future, when you release a new version, try installing the new version over the old version on that VM and see whether the update works seamlessly. All your existing customers will be going through this experience. So, it is important that you try installing the new version over the exact version that your customer is currently using.

Day 7, 8, 9, 10, 11, 12, 13, 14 –> MSDN Article

It took me more days to write this article than to actually develop the product. I also furnished the code while writing the article. One interesting thing was to deliver the source code excluding the binaries. In the source code folder you will find bin, obj, debug, and release folders that contains the binary files. Moreover if you use version control, then there will be additional files. In my case, I use Subversion that creates an .svn folder under each directory that contains lots of files. In order to deliver a clean source code pack, I need to copy these and then manually delete all the unnecessary folders. This is difficult to do frequently. One easy solution is to use the xcopy command to copy files skipping some predefined file extensions and subdirectories.

First I created a batch file xcopysrc.bat at the root folder of source codes, e.g., D:\StickOut. The batch file contains the following command:

xcopy /S/E *.* d:\temp\StickOut\ /exclude:excludeinxcopy.txt

/S/E indicates to copy subdirectories including empty subdirectories. Then we specify to copy all files by *.* wildcards. Next comes the destination folder. A trailing \ indicates that it is a folder and xcopy won't ask you whether the destination is a folder or a file. The last argument is /exclude, which specifies a file where we include all the extensions to skip. The file contains the following content:

\obj\
\bin\
\cvs\
\debug\
\release\
\_svn\
\.svn\
.zip
.cvsignore

This file specifies which subdirectories and file extensions to skip while copying. As a result, every time you run this batch file, you get a nice clean source code folder.

Conclusion

This simple project turned out to be a really good exercise for using many features in the .NET Framework 2.0. Here we learned how to create a multithreaded Windows Forms application, make it look slick, reduce memory consumption, and improve UI responsiveness. Then we learned about Office Add-ins, the best way to create Outlook Add-ins, and then hook into Visual Studio IDE. We learned about COM Automation and a safer way to use COM from .NET. We also covered inter-process communication using the new IpcChannel. Finally we learned how to deploy and test our product in such a way that we can simulate the exact environment of real users. I hope this article gives you end-to-end guidance of many aspects of .NET Framework 2.0 application development.

 

About the author

Omar AL Zabir is a graduate of Computer Science in American International University—Bangladesh, (www.aiub.edu). He developed the Web-based automation and collaboration system for his university using the .NET Framework almost four years ago when it was in the Beta 1 stage. He has also worked for seven years (beginning when he was in high school) at Orion Technologies as Lead Developer, developing solutions for large banks in the United States. His latest creation is www.pageflakes.com. His love for Microsoft technologies can be seen on his Web site, www.oazabir.com.

Show:
© 2014 Microsoft