Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Create an Outlook 2007 Form Region by using VSTO 2008

4.91/5 (22 votes)
1 Jan 2008CPOL8 min read 1   2.3K  
In this article, we will create a form region that shows all contacts within the same category.

Screenshot - MyContactsResult.png

Introduction

In this article, you will learn how to:

  • Build a VSTO 2008 Outlook add-in
  • Create a Form Region for your Outlook Contacts
  • Use the Table, Column, and Row objects to access and filter MAPIFolder content
  • Retrieve the picture of a Contact
  • Programmatically start an IM-session

By using the VSTO features, you can extend / replace Outlook standard forms and extend them with a fancy .NET user interface. In the past, you had to customize the Outlook standard forms with the Outlook Forms Designer. An ugly old-styled form without XP-style was the result. With VSTO, you can use a Visual Studio integrated form regions designer to create an UI with .NET controls.

Benefits

  • Outlook forms will keep the new design with new controls (like the picture control)
  • Integrated with your Visual Studio Designer
  • Code templates and debugging

The Concept

For this sample, I decided to extend the Outlook standard contact form to display an additional page with all contacts within the same category. In Outlook, you can assign categories to contacts, appointments, messages, and tasks. Each of these items can have multiple categories, you can even define your own besides the Mastercategories list. The plan is to get the current contact, get its parent folder, use the table object, and find all contacts which are in the same category as the selected contact. If the selected contact has no categories assigned - simply display all the contacts of the folder.

The Solution

Before you can create this solution, you need the following prerequisites on your development machine.

Prerequisites

Create a Solution

This sample was built on a Vista Ultimate 64bit with Outlook 2007 (German) and Visual Studio 2008 (RTM). Start your Visual Studio and create a new project. Choose Office/2007/Outlook Add-in as project template.

Screenshot - MyContactsCreateProject.png

After you creat the project, you will find a skeleton class called ThisAddIn with two methods where the application is started and terminated. Normally, here I would say - and here our application is loaded... - but this is not true for a form region. Just for reference, see the code below.

C#
namespace MyContacts
{
    public partial class ThisAddIn
    {
        private void ThisAddIn_Startup(object sender, System.EventArgs e)
        {
        }

        private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
        {
        }

        #region VSTO generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }

        #endregion
    }
}

The ThisAddIn class must not be modified. Go on, and let's create a form region by using the Visual Studio wizard.

Create a Forms-Region

Just right-click your project and add a new item. Choose Outlook Form Region as template.

Screenshot - MyContactsAddFormRegion.png

On the next screenshot, you can see how to create a form region. First, select Design a new form region.

Screenshot - MyContactsFormRegion1.png

Choose what type of form region you want to have. For this sample. select the Separate type which means it will show up on an extra page.

Screenshot - MyContactsFormRegion2.png

Give the form region a name.

Screenshot - MyContactsFormRegion3.png

At the end. select what kind of message classes should display this form region. In this sample, only contact forms are supported. The message class is 'IPM.Contact'.

Screenshot - MyContactsFormRegion4.png

The wizard that comes with VSTO now creates a code template for the form region. Every time the form region is loaded, a method FormRegionShowing is called. Before the form region is unloaded, a method FormRegionClosed is executed. See the generated method stubs below.

C#
#region Form Region Factory

[Microsoft.Office.Tools.Outlook.FormRegionMessageClass(
   Microsoft.Office.Tools.Outlook.FormRegionMessageClassAttribute.Contact)]
[Microsoft.Office.Tools.Outlook.FormRegionName("MyContacts.FormRegionMyContacts")]
public partial class FormRegionMyContactsFactory {
    // Occurs before the form region is initialized.
    // To prevent the form region from appearing, set e.Cancel to true.
    // Use e.OutlookItem to get a reference to the current Outlook item.
    private void FormRegionMyContactsFactory_FormRegionInitializing(object sender, 
            Microsoft.Office.Tools.Outlook.FormRegionInitializingEventArgs e) {
    }
}

#endregion

// Occurs before the form region is displayed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionShowing(object sender, System.EventArgs e) {

}

// Occurs when the form region is closed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionClosed(object sender, System.EventArgs e) {

}

Now, when you start to debug the solution and open a contact form, you will notice an extra icon in the Ribbon. With this button, you can access the new form region.

Below, you can see a contact form with the new button.

Screenshot - MyContactsContactForm.png

In this sample, I want to display all contacts which are in the same category as the current contact. To accomplish this goal, you need to access the contact item of the current form region. Within the form region class, you can access the current Outlook item by using this.OutlookItem. You have an item of type ContactItem, and so you explicitly have to cast from the generic Outlook Item type. The categories of the contact can be accessed via the Categories property. The value is a concatenated list of strings, separated by a semicolon. Example: When a contact has assigned the categories "Outlook" and "Friends", the Categories value would be "Outlook; Friends". If you want to show the list of all other contacts with the same category, you need to retrieve the desired information of the contacts and display it in the form region. For the display, you can use a ListView, because it's easy to use here and you can even display the contact pictures.

You want a fast responsive application, and so you can use the Table object to access the information of a MAPIfolder. The content of the table can be filtered by some kind of SQL syntax. Because you want to retrieve all contacts of the same category, you need to build an SQL filter for the Table object. The following lines of code shows you how to build the filter for the categories:

C#
/// <summary>
/// Build the SQL Filter for the  
/// </summary>
/// <param name="messageClass">The desired MessageClass</param>
/// <param name="categoryList">A list of categories for the contact</param>
/// <returns>returns the complete filter for the Table-Object</returns>
string BuildSQLFIlter(string messageClass, string[] categoryList) {

    // We can use a StringBuilder object for 
    StringBuilder filter = new StringBuilder(250);
    // SQL prefix
    filter.Append(@"@SQL=(");
    // only types with a specific messageClass
    filter.Append(string.Format(@"(""http://schemas." + 
           "microsoft.com/mapi/proptag/0x001a001e"" = '{0}')", 
           messageClass ));

    // are there categories ? append an AND conjunction
    if (categoryList.Length > 0) filter.Append(" AND (");
    // all categories of the List are ORed together
    for (int index = 0; index < categoryList.Length; index++) {
        if (index > 0) filter.Append(" OR ");
        filter.Append(string.Format(@"(""urn:schemas-microsoft" + 
          "-com:office:office#Keywords"" LIKE '%{0}%')", 
          categoryList[index].Trim()));
    }
    // end bracket for the AND 
    if (categoryList.Length > 0) filter.Append(")");
    // end bracket for the complete SQL
    filter.Append(")");
    return filter.ToString();
}

The meaning of the filter is "All types with a message class of "IPM.Contact" and Category A or Category B or...."

When you have access to the filtered table items, you have to tell which columns you want to retrieve. It's in the nature of MAPI, the more columns you want to access, the longer it takes to retrieve the data from the store.

You even can't access the picture directly by using the Table object. So, here it's the best option to get the contact data like name, email, homepage etc., as fast as possible, and the long running operations should be processed in the background. Naturally, you would use a Backgroundworker object for this. The job of the Backgroundworker is to get a contact item and pull out the contact picture of it - and pass it to the corresponding item in the ListView.

Below you can see the complete method used to retrieve the desired data.

C#
// Occurs before the form region is displayed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionShowing(
             object sender, System.EventArgs e) {

    Outlook.ContactItem contact;
    Outlook.MAPIFolder folder;
    Outlook.Table table;

    try {
        // get the current Outlook ContactItem
        contact = this.OutlookItem as Outlook.ContactItem;
        // the contact folder object
        folder = contact.Parent as Outlook.MAPIFolder;

        // retrieve the categories for the current contact
        string categories = contact.Categories ?? string.Empty ;
        string[] categoryList = categories.Split(';');

        // build he SQL filter for the categories
        string filter = BuildSQLFIlter ("IPM.Contact", categoryList );
       
        // get the Table, filltered only for items matching
        // the filter and the MessageClass, no HiddenItems
        table = folder.GetTable(filter.ToString (), 
                Outlook.OlTableContents.olUserItems);

        // define wich columns should be retrieved from table
        table.Columns.RemoveAll();
        table.Columns.Add("EntryID");
        table.Columns.Add("FileAs");
        table.Columns.Add(@"urn:schemas:contacts:profession");
        table.Columns.Add("Email1Address");
        table.Columns.Add(@"urn:schemas:contacts:businesshomepage");

        // the propertytag for the Instant-Messenger address
        table.Columns.Add(@"http://schemas.microsoft.com/mapi/" + 
              @"id/{00062004-0000-0000-C000-000000000046}/8062001F");
        table.Columns.Add("Categories");

        // the MAPI propertytag for the 'HasPicture' flag
        table.Columns.Add(@"http://schemas.microsoft.com/mapi/" + 
              @"id/{04200600-0000-0000-C000-000000000046}/8015000B");

        // create an Imagelist and add the 'NoPicture' image to the list
        ImageList imageList = new ImageList();
        imageList.ImageSize = IMAGESIZE;
        imageList.ColorDepth = ColorDepth.Depth24Bit; 

        // assign the ImageList to the ListView in the form region
        listViewContacts.LargeImageList = imageList;
        // listViewContacts.TileSize = IMAGESIZE;
        imageList.Images.Add(string.Empty  , Properties.Resources.NoPicture);

        // build a List of conatcs which have a picture assigned
        List<string> contactsWithPicture = new List<string>();

        // loop over the contacts and add them to the listView
        // initially the businessCard is filled with the current Info
        while (!table.EndOfTable) {
            Outlook.Row row = table.GetNextRow();
            // fill the COntactInfo from row information
            ContactInfo info = new ContactInfo (row);

            if (info.HasPicture) {
                contactsWithPicture.Add(info.EntryId);
            }                    
            if (contact.EntryID != info.EntryId) {

                ListViewItem listViewItem = 
                  this.listViewContacts.Items.Add(info.FileAs, 0);
                listViewItem.Tag = info;
                listViewItem.Name = info.EntryId;
                listViewItem.ToolTipText = info.EmailAddress;

            } else {
                // display the curent data in the businesscard view
                UpdateBusinessCard(info, GetContactPicture(info.EntryId));
            }
        }

        // the long running operation to retrieve
        // the contact pictures is done in a separate thread
        _backgroundWorker = new BackgroundWorker();
        _backgroundWorker.WorkerSupportsCancellation = true;
        _backgroundWorker.DoWork += new DoWorkEventHandler(_backgroundWorker_DoWork);
        // we pass along a list of contacts with attached pictures
       _backgroundWorker.RunWorkerAsync(contactsWithPicture);

    } finally {
        table = null;
        folder = null;
        contact = null;
    }
}

You learned that it isn't possible to access a contact picture by using the Table object. Here is a code snippet that demonstrates how to retrieve a contact picture from an Outlook ContactItem. When a contact has a picture attached, it is accessible as a normal attachment and it has a default name, "ContactPicture.jpg":

C#
/// <summary>
/// retrieves the picture of a contact
/// </summary>
/// <param name="entryId">the entryId of the contact</param>
/// <returns>returns the image or null</returns>
private Image GetContactPicture(string entryId) {
    // retrieve the contact item by it's entryID
    Outlook.ContactItem contact = 
      Globals.ThisAddIn.Application.Session.GetItemFromID(entryId, 
      null) as Outlook.ContactItem;
    Image img = null;
    // a path to temporarily store the attached picture
    string tempPath = Environment.GetEnvironmentVariable("TEMP");
    if (contact != null) {
        // check for the contact picture
        foreach (Outlook.Attachment attachment in contact.Attachments) {
            if (attachment.FileName == "ContactPicture.jpg") {
                // save the file with a unique name
                string fileName = Path.Combine(tempPath, entryId + ".jpg");
                attachment.SaveAsFile(fileName);
                // read the saved image into a Bitmap using a stream
                FileStream stream = new FileStream(fileName,FileMode.Open );
                Bitmap bmp = new Bitmap(Image.FromStream (stream,true ));
                // check the aspect-ratio of the picture
                if (bmp.Width >= bmp.Height) {
                    // scale Image
                    Bitmap tempBmp = new Bitmap(IMAGESIZE.Width ,IMAGESIZE.Height  );
                    Graphics g = Graphics.FromImage (tempBmp );
                    g.FillRectangle(Brushes.White,0, 0, IMAGESIZE.Width, IMAGESIZE.Height);  
                    
                    float ratio = (float)bmp.Height  / bmp.Width  ;
                    int newHeight = (int)(ratio * bmp.Height);

                    // draw the scaled Image onto the empty Bitmap
                    int top = (IMAGESIZE.Height - newHeight) / 2;
                    g.DrawImage (bmp, new Rectangle (0,top, IMAGESIZE.Width , newHeight )); 
                    img = tempBmp;
                    g.Dispose ();
                } else {
                    // resize the picture
                    img = new Bitmap(bmp, IMAGESIZE);
                }
                stream.Dispose ();       
                // delete the temp file
                File.Delete(fileName);
                break;
            }
        }
    }
    contact = null;
    return img;
}

For each contact with a picture, the image is retrieved in the background worker thread. The thread should update the user interface. Usually, you have to use the controls Invoke method when accessing a control from another thread than the UI-thread. In the following code block, you will see how the picture is updated in the form region from the worker thread:

C#
/// <summary>
/// does all the background work
/// in this case, it get's the Images for the contacts and updates the imagelist 
/// </summary>
/// <param name="sender">the backgroundworker-instace</param>
/// <param name="e">a startparameter which has been passed to the workerthread</param>
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {

    // we passed the list of contacts with assigned pictures
    List<string> contactsWithPicture = (List<string>)e.Argument;

    // loop over the list, and retrieve the 
    foreach (string entryId in contactsWithPicture) {
        if (e.Cancel) break;
        Image contactPicture = GetContactPicture(entryId);
        if (contactPicture != null) {
            SetImagePicture(entryId, contactPicture);
        }
    }
}


/// <summary>
/// defines a method signature used by the invoke command 
/// </summary>
/// <param name="enrtyId">entryId of the contact</param>
/// <param name="image">image for the contact</param>
public delegate void SetImagePictureDelegate(string enrtyId, Image image);

/// <summary>
/// updates the contact picture in the listview
/// when this method is called from another thread - it will invoke itself
/// </summary>
/// <param name="enrtyId">entryId of the contact</param>
/// <param name="image">image for the contact</param>
public void SetImagePicture(string entryId, Image image) {

    // if the listview needs invocation, call this
    // method again using the listviews invoke method
    if (listViewContacts.InvokeRequired) {
        listViewContacts.Invoke(new SetImagePictureDelegate(
           SetImagePicture), new object[] { entryId, image });
    } else {

        // if the listview has no item with
        // the scpecific ID, everything is sensless
        if (!listViewContacts.Items.ContainsKey(entryId)) return;

        // get the index of the contactimage from the listview imagelist
        int index = listViewContacts.LargeImageList.Images.IndexOfKey(entryId);

        // when there is no such image, add it to the list
        if (index == -1) {
            listViewContacts.LargeImageList.Images.Add(image);
            index = listViewContacts.LargeImageList.Images.Count-1;
        } else {
            listViewContacts.LargeImageList.Images[index] = image;
        }

        // tell the listviewItem that it should use the given image index
        listViewContacts.Items[entryId].ImageIndex = index;

        // get the index of the listviewitem and force a redraw
        index = listViewContacts.Items[entryId].Index ;
        listViewContacts.RedrawItems(index, index, false);
    }
}

/// <summary>
/// backgroundworker is used to update the pictures of the contacts
/// </summary>
private BackgroundWorker _backgroundWorker;

// Occurs when the form region is closed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionMyContacts_FormRegionClosed(object sender, 
                                  System.EventArgs e) {
    if (_backgroundWorker.IsBusy) {
        _backgroundWorker.CancelAsync();
    }
    _backgroundWorker.Dispose();
}

As you can also see above, the BackgroundWorker has to be stopped when the form region is unloaded. The rest is just straight coding. Just to play around a little, I have implemented a small business card, which is updated with the contact information while hovering over the contacts in the list with your mouse. When you double-click a list item, the corresponding contact is displayed in Outlook:

C#
/// <summary>
/// event sink for the listviewitem doubleclick.
/// used to open the selected contact.
/// </summary>
private void listViewContacts_DoubleClick(object sender, EventArgs e) {
    Point position = listViewContacts.PointToClient(Control.MousePosition);
    ListViewItem listViewItem = 
       listViewContacts.GetItemAt(position.X ,position.Y );
    if (listViewItem == null) return;

    OpenItem(listViewItem.Name); 
}

/// <summary>
/// opens the contact
/// </summary>
/// <param name="entryId">entryId of the contact which shoul be displayed</param>
private void OpenItem(string entryId){
    Outlook.ContactItem contact = 
      Globals.ThisAddIn.Application.Session.GetItemFromID(entryId, null) 
      as Outlook.ContactItem;
    contact.Display(false);
    contact = null;
}

/// <summary>
/// Occurs when the mouse hovers over an Item
/// </summary>
/// <param name="sender">The ListviewItem object</param>
/// <param name="e">The Item where the mouse is hovering</param>
private void listViewContacts_ItemMouseHover(object sender, 
                        ListViewItemMouseHoverEventArgs e) {
    UpdateBusinessCard((ContactInfo)e.Item.Tag, 
       listViewContacts.LargeImageList.Images[e.Item.ImageIndex]);
}

The information passed to the business card is wrapped in a simple value holder class called ContactInfo. Here is the ContactInfo class:

C#
public class ContactInfo {

    /// <summary>
    /// The constructor takes an Outlook Row
    /// and parses the Information into the values
    /// </summary>
    /// <param name="row">the outlook row object</param>
    public ContactInfo(Outlook.Row row) {
        EntryId = (string)row[1];
        FileAs = (string)row[2];
        JobTitle = (string)(row[3] ?? string.Empty);
        EmailAddress = (string)(row[4] ?? string.Empty);
        Homepage  = (string)(row[5] ?? string.Empty);
        MessengerAddress  = (string)(row[6] ?? string.Empty);
        string categories = (string)(row[7] ?? string.Empty);
        Categories = categories.Split(';');
        HasPicture = (bool)(row[8] ?? false);
    }

    public string EntryId {get; private set; }
    public string FileAs { get; private set; }
    public string JobTitle { get; private set; }
    public string EmailAddress { get; private set; }
    public string Homepage { get; private set; }
    public string MessengerAddress { get; private set; }
    public string[] Categories { get; private set; }
    public bool HasPicture { get; private set; }
}

As you can see, you can design the form region with your Visual Studio designer as any other .NET control.

Screenshot - MyContactsFormDesign.png

As a goodie, you can see how to send someone an instant-message by using the Windows Messenger (tested with MSN Live Messenger) by using the native COM API. You can also click on Email and compose a new email with the contact as recipient - or show his homepage in the web browser. You can even double click the business card and display the Outlook-Contact.

See below, the business card snippets:

C#
/// <summary>
/// fills the BusinessCard with the given ContactInfo
/// </summary>
/// <param name="info">the ContactInfo object.</param>
/// <param name="image" >the contact image</param>
public void SetContactInfo( ContactInfo info , Image image){
    EntryId = info.EntryId;  
    LastFirst.Text = info.FileAs;        
    Profession.Text = info.JobTitle;
    Emailaddress.Text = info.EmailAddress;  
    Messenger.Text = info.MessengerAddress;        
    Homepage.Text = info.Homepage;       
    Categories.Text = string.Join("\n", info.Categories);        
    Image.Image = image;    
}


/// <summary>
/// The EntryId identifies the Outlook ContactItem
/// </summary>
public string EntryId { get; set; }

/// <summary>
/// event sink for the business card doubleclick event
/// </summary>
private void FormBusinessCard_MouseDoubleClick(object sender, MouseEventArgs e) {
    OpenItem(EntryId);
}


/// <summary>
/// retrieves the contact from application context and displays it
/// </summary>
/// <param name="entryId">the entryId of the contact</param>
void OpenItem(string entryId) {
    Outlook.ContactItem contact = 
      Globals.ThisAddIn.Application.Session.GetItemFromID (entryId , 
      null) as Outlook.ContactItem ;
    contact.Display(false);
    contact = null;
}


/// <summary>
/// event sink for the homepage link clicked event
/// </summary>
private void Homepage_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
    ShowHomePageInBrowser(Homepage.Text);
}

/// <summary>
/// displays the contacts homepage in a browser
/// </summary>
/// <param name="url">the homepage url</param>
void ShowHomePageInBrowser(string url){
    Process p = new Process();
    p.StartInfo.FileName = "IExplore.exe";
    p.StartInfo.Arguments = url;
    p.Start();
}


/// <summary>
/// event sink for the messenger lnk clicked event.
/// </summary>
private void Messenger_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
    ConnectToMessenger(Messenger.Text);
}

/// <summary>
/// Takes the email from the InstantMessaging partner and opens the InstantMessage Window.
/// </summary>
/// <param name="email">the emailaddress of the partner</param>
void ConnectToMessenger(string email) {
    try{
        // create a COM - instance to the Messenger
        Type messengerType = Type.GetTypeFromProgID ("Messenger.UIAutomation.1");
        object comObject = Activator.CreateInstance (messengerType);
        
        // call the InstantMessage method with the emailaddress of the user.
        object[] arguments = new object[] { email };
        messengerType.InvokeMember ("InstantMessage", 
          BindingFlags.InvokeMethod,null, comObject, arguments);
        Marshal.ReleaseComObject(comObject); 


    } catch(System.Exception ex){
        MessageBox.Show("Please make sure you have installed the" + 
          " latest Windows Messenger Live and  that you are signed-in." );
    }
}

/// <summary>
/// event sink for the email address link clicked event
/// </summary>
private void Emailaddress_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
    SendEmail(LastFirst.Text, Emailaddress.Text);
}

/// <summary>
/// creates a new Email for the recipient using the emailaddress
/// </summary>
/// <param name="emailAddress">the contacts emailaddress</param>
void SendEmail(string name,string emailAddress) {
    Outlook.MailItem mail =  Globals.ThisAddIn.Application.CreateItem(
      Microsoft.Office.Interop.Outlook.OlItemType.olMailItem) as Outlook.MailItem ;
    mail.Recipients.Add (string.Format("{0}<{1}>", name, emailAddress  ));
    mail.To = emailAddress;
    mail.Display(false);
    mail = null;
}

That's it for now. Go and play with form regions and see how easy it is by using the VSTO technology and the visual designers to extend the Outlook forms and user interface. As always - greetings from Munich/Germany, I hope you enjoyed this article.

Resume:

  • Build a VSTO 2008 Outlook add-in
  • Create a Form Region for your Outlook Contacts
  • Use the Table, Column, and Row objects to access and filter MAPIFolder-Content
  • Retrieve the picture of a Contact
  • Programmatically start an IM-Session

Notes

The following notes are valid for this and all VSTO add-ins:

  • The temporary key for this solution was created on my development machine. You have to create and use your own.
  • The solution has no Setup project. For distributing VSTO add-ins, see Deploying VSTO Solutions.
  • For each DLL that is used from your add-in, you have to set the security policy (custom action in MSI Setup package).
  • VSTO 2008 Beta2 and VSTO 2008 RTM solutions are not compatible!!! You have to patch the .csproj file to get it to work.

Special thanks to (You will find lots of information about using and programming Outlook, CDO, and Exchange on their sites. And I would not be what I am without their help):

History

  • V.1.0 - Initial version (23 December, 2007)
  • V.1.1 - Added VB.NET Solution (1 January, 2008)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)