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.
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.
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
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.
On the next screenshot, you can see how to create a form region. First, select Design a new form region.
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.
Give the form region a name.
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
'.
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.
#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 {
private void FormRegionMyContactsFactory_FormRegionInitializing(object sender,
Microsoft.Office.Tools.Outlook.FormRegionInitializingEventArgs e) {
}
}
#endregion
private void FormRegionMyContacts_FormRegionShowing(object sender, System.EventArgs e) {
}
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.
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:
string BuildSQLFIlter(string messageClass, string[] categoryList) {
StringBuilder filter = new StringBuilder(250);
filter.Append(@"@SQL=(");
filter.Append(string.Format(@"(""http://schemas." +
"microsoft.com/mapi/proptag/0x001a001e"" = '{0}')",
messageClass ));
if (categoryList.Length > 0) filter.Append(" AND (");
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()));
}
if (categoryList.Length > 0) filter.Append(")");
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.
private void FormRegionMyContacts_FormRegionShowing(
object sender, System.EventArgs e) {
Outlook.ContactItem contact;
Outlook.MAPIFolder folder;
Outlook.Table table;
try {
contact = this.OutlookItem as Outlook.ContactItem;
folder = contact.Parent as Outlook.MAPIFolder;
string categories = contact.Categories ?? string.Empty ;
string[] categoryList = categories.Split(';');
string filter = BuildSQLFIlter ("IPM.Contact", categoryList );
table = folder.GetTable(filter.ToString (),
Outlook.OlTableContents.olUserItems);
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");
table.Columns.Add(@"http://schemas.microsoft.com/mapi/" +
@"id/{00062004-0000-0000-C000-000000000046}/8062001F");
table.Columns.Add("Categories");
table.Columns.Add(@"http://schemas.microsoft.com/mapi/" +
@"id/{04200600-0000-0000-C000-000000000046}/8015000B");
ImageList imageList = new ImageList();
imageList.ImageSize = IMAGESIZE;
imageList.ColorDepth = ColorDepth.Depth24Bit;
listViewContacts.LargeImageList = imageList;
imageList.Images.Add(string.Empty , Properties.Resources.NoPicture);
List<string> contactsWithPicture = new List<string>();
while (!table.EndOfTable) {
Outlook.Row row = table.GetNextRow();
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 {
UpdateBusinessCard(info, GetContactPicture(info.EntryId));
}
}
_backgroundWorker = new BackgroundWorker();
_backgroundWorker.WorkerSupportsCancellation = true;
_backgroundWorker.DoWork += new DoWorkEventHandler(_backgroundWorker_DoWork);
_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":
private Image GetContactPicture(string entryId) {
Outlook.ContactItem contact =
Globals.ThisAddIn.Application.Session.GetItemFromID(entryId,
null) as Outlook.ContactItem;
Image img = null;
string tempPath = Environment.GetEnvironmentVariable("TEMP");
if (contact != null) {
foreach (Outlook.Attachment attachment in contact.Attachments) {
if (attachment.FileName == "ContactPicture.jpg") {
string fileName = Path.Combine(tempPath, entryId + ".jpg");
attachment.SaveAsFile(fileName);
FileStream stream = new FileStream(fileName,FileMode.Open );
Bitmap bmp = new Bitmap(Image.FromStream (stream,true ));
if (bmp.Width >= bmp.Height) {
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);
int top = (IMAGESIZE.Height - newHeight) / 2;
g.DrawImage (bmp, new Rectangle (0,top, IMAGESIZE.Width , newHeight ));
img = tempBmp;
g.Dispose ();
} else {
img = new Bitmap(bmp, IMAGESIZE);
}
stream.Dispose ();
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:
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {
List<string> contactsWithPicture = (List<string>)e.Argument;
foreach (string entryId in contactsWithPicture) {
if (e.Cancel) break;
Image contactPicture = GetContactPicture(entryId);
if (contactPicture != null) {
SetImagePicture(entryId, contactPicture);
}
}
}
public delegate void SetImagePictureDelegate(string enrtyId, Image image);
public void SetImagePicture(string entryId, Image image) {
if (listViewContacts.InvokeRequired) {
listViewContacts.Invoke(new SetImagePictureDelegate(
SetImagePicture), new object[] { entryId, image });
} else {
if (!listViewContacts.Items.ContainsKey(entryId)) return;
int index = listViewContacts.LargeImageList.Images.IndexOfKey(entryId);
if (index == -1) {
listViewContacts.LargeImageList.Images.Add(image);
index = listViewContacts.LargeImageList.Images.Count-1;
} else {
listViewContacts.LargeImageList.Images[index] = image;
}
listViewContacts.Items[entryId].ImageIndex = index;
index = listViewContacts.Items[entryId].Index ;
listViewContacts.RedrawItems(index, index, false);
}
}
private BackgroundWorker _backgroundWorker;
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:
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);
}
private void OpenItem(string entryId){
Outlook.ContactItem contact =
Globals.ThisAddIn.Application.Session.GetItemFromID(entryId, null)
as Outlook.ContactItem;
contact.Display(false);
contact = null;
}
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:
public class ContactInfo {
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.
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:
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;
}
public string EntryId { get; set; }
private void FormBusinessCard_MouseDoubleClick(object sender, MouseEventArgs e) {
OpenItem(EntryId);
}
void OpenItem(string entryId) {
Outlook.ContactItem contact =
Globals.ThisAddIn.Application.Session.GetItemFromID (entryId ,
null) as Outlook.ContactItem ;
contact.Display(false);
contact = null;
}
private void Homepage_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
ShowHomePageInBrowser(Homepage.Text);
}
void ShowHomePageInBrowser(string url){
Process p = new Process();
p.StartInfo.FileName = "IExplore.exe";
p.StartInfo.Arguments = url;
p.Start();
}
private void Messenger_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
ConnectToMessenger(Messenger.Text);
}
void ConnectToMessenger(string email) {
try{
Type messengerType = Type.GetTypeFromProgID ("Messenger.UIAutomation.1");
object comObject = Activator.CreateInstance (messengerType);
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." );
}
}
private void Emailaddress_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) {
SendEmail(LastFirst.Text, Emailaddress.Text);
}
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)