How to replace built-in Outlook dialogs with custom forms
Years ago I found this article by Helmut Obertanner on CodeProject. In this article Helmut explains how you can replace the built-in Outlook Address book dialog with your own form. I was amazed and intrigued! This approach is a great way to provide your users with a custom address form that is able to retrieve contact address information from literally any source e.g. CRM or customer database.
So, in today’s article I’ll show you how you can use Helmut’s code in your own Add-in Express projects and how to intercept and customize not just the built-in Outlook Address book dialog but also any other Outlook dialog.
Creating the Outlook add-in project
Start by creating a new ADX COM Add-in project in Visual Studio 2012 with Add-in Express for Office and .net.
Select your programming language (it’s C# for this project), minimum supported version of Office (I’m selecting Office 2010 for my plug-in to work in Outlook 2010 and 2013) and Microsoft Outlook as the supported application and finish the wizard.
A closer look at the code
Helmut mentions that a common problem with programming for Microsoft Outlook is that the user can have multiple Explorer and Inspector windows open during the Outlook session’s lifetime. This becomes a challenge when you need to know which Explorer and/or Inspector was created or closed.
A good approach to address this problem is to create wrapper classes that can handle all the tricky bits for you. Both the Explorer and Inspector wrapper classes in the example implement an abstract class called WrapperClass. This class contains an ID property that will contain the unique ID of the Explorer or Inspector object as well as an event delegate to inform the application of closed objects.
The code for WrapperClass.cs follows:
public delegate void WrapperClosedDelegate(Guid id); internal abstract class WrapperClass { public event WrapperClosedDelegate Closed; public Guid Id { get; private set; } protected void OnClosed() { if (Closed != null) Closed(Id); } public WrapperClass() { Id = Guid.NewGuid(); } }
Since we’re not really interested in tracking any Explorers in this example, we’ll omit the ExplorerWrapper class from our sample Outlook project. We will, however, need the InspectorWrapper class.
InspectorWrapper.cs
internal class InspectorWrapper : WrapperClass { bool _showCustomAddressBook; public Outlook.Inspector Inspector { get; private set; } public InspectorWrapper(Outlook.Inspector inspector) { Inspector = inspector; ConnectEvents(); } void ConnectEvents() { ((Outlook.InspectorEvents_10_Event)Inspector).Close += new Outlook.InspectorEvents_10_CloseEventHandler(InspectorWrapper_Close); ((Outlook.InspectorEvents_10_Event)Inspector).Activate += new Outlook.InspectorEvents_10_ActivateEventHandler(InspectorWrapper_Activate); ((Outlook.InspectorEvents_10_Event)Inspector).Deactivate += new Outlook.InspectorEvents_10_DeactivateEventHandler(InspectorWrapper_Deactivate); } void DisconnectEvents() { ((Outlook.InspectorEvents_10_Event)Inspector).Close -= new Outlook.InspectorEvents_10_CloseEventHandler(InspectorWrapper_Close); ((Outlook.InspectorEvents_10_Event)Inspector).Activate -= new Outlook.InspectorEvents_10_ActivateEventHandler(InspectorWrapper_Activate); ((Outlook.InspectorEvents_10_Event)Inspector).Deactivate -= new Outlook.InspectorEvents_10_DeactivateEventHandler(InspectorWrapper_Deactivate); } void InspectorWrapper_Activate() { if (_showCustomAddressBook) { frmMyAddressBook customDialog = new frmMyAddressBook(); customDialog.ShowDialog(); } } void InspectorWrapper_Deactivate() { _showCustomAddressBook = false; IntPtr hBuiltInDialog = WinApiProvider.FindWindow("#32770", ""); if (hBuiltInDialog != IntPtr.Zero) { List<IntPtr> childWindows = WinApiProvider.EnumChildWindows(hBuiltInDialog); List<string> childWindowNames = WinApiProvider.GetWindowNames(childWindows); if (!childWindowNames.Contains("&Name only")) return; if (!childWindowNames.Contains("Mo&re columns")) return; if (!childWindowNames.Contains("A&ddress Book")) return; if (!childWindowNames.Contains("Ad&vanced Find")) return; WinApiProvider.SendMessage( hBuiltInDialog, WinApiProvider.WM_SYSCOMMAND, WinApiProvider.SC_CLOSE, 0); _showCustomAddressBook = true; } } void InspectorWrapper_Close() { DisconnectEvents(); Marshal.ReleaseComObject(Inspector); Inspector = null; GC.Collect(); GC.WaitForPendingFinalizers(); OnClosed(); } }
The code for the InspectorWrapper class is fairly self-explanatory; we’re mostly interested in the Deactivate event handler of the class. In this event we’ll use P/Invoke to find a window by its class name. The WinApiProvider.cs class wraps all the P/Invoke methods for ease of use. You can read more about P/Invoke on www.pinvoke.net
You can also find information about any Windows form by using Spy++, available in your Visual Studio installation folder e.g. C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\Tools\spyxx.exe
The code then loops through the Outlook window’s child controls to determine which child window is being displayed. It then sets a Boolean variable in order to display your own Windows form instead of the built-in Outlook form.
Get your code to interact with Outlook
With all the helper classes added, switch back to the AddinModule’s designer and add events handlers for its AddinBeginShutdown and AddinStartupComplete events by double-clicking next to the event names in the Visual Studio property grid.
Next, declare the following objects in the AddinModule class:
Outlook.Inspectors _Inspectors; Dictionary<Guid, WrapperClass> _WrappedObjects;
Add the following to the AddinStartupComplete event handler:
private void AddinModule_AddinStartupComplete(object sender, EventArgs e) { _WrappedObjects = new Dictionary<Guid, WrapperClass>(); _Inspectors = OutlookApp.Inspectors; for (int i = _Inspectors.Count; i >= 1; i--) { WrapInspector(_Inspectors[i]); } _Inspectors.NewInspector += _Inspectors_NewInspector; }
The code in the event handler creates new instances for the _WrappedObjects and _Inspectors objects and then wraps any open Inspector using the WrapInspector method. Lastly, a new event handler is declared for the _Inspectors object’s NewInspector event.
The NewInspector event simply wraps the Inspector using the WrapInspector method:
The NewInspector event simply wraps the Inspector using the WrapInspector method:
void _Inspectors_NewInspector(Outlook.Inspector Inspector) { WrapInspector(Inspector); }
Lastly the AddinBeginShutdown event is used to release any COM objects and clean up the object references:
private void AddinModule_AddinBeginShutdown(object sender, EventArgs e) { _WrappedObjects.Clear(); if (_Inspectors != null) Marshal.ReleaseComObject(_Inspectors); GC.Collect(); GC.WaitForPendingFinalizers(); }
Creating the custom Outlook address lookup form
We’ll build a light-weight address lookup form that retrieves contacts from 37Signals Highrise. Open the frmMyAddressBook form and design its layout to resemble the following image:
Next, add the following to the form’s Shown event:
private void frmMyAddressBook_Shown(object sender, EventArgs e) { string peopleXml = string.Empty; List<Person> persons = new List<Person>(); using (WebClient client = new WebClient()) { client.Headers.Add( System.Net.HttpRequestHeader.Authorization, "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes("--YOURAPITOKEN--"))); peopleXml = client.DownloadString("https://yoursubdomain.highrisehq.com/people.xml"); } XmlDocument doc = new XmlDocument(); doc.LoadXml(peopleXml); foreach (XmlNode node in doc.DocumentElement.ChildNodes) { Person person = new Person(node.OuterXml); persons.Add(person); ListViewItem lvitemPerson = new ListViewItem(person.FirstName); ListViewItem.ListViewSubItem lvitemLastName = new ListViewItem.ListViewSubItem(lvitemPerson, person.LastName); ListViewItem.ListViewSubItem lvitemCompanyName = new ListViewItem.ListViewSubItem(lvitemPerson, person.CompanyName); ListViewItem.ListViewSubItem lvitemEmail = new ListViewItem.ListViewSubItem(lvitemPerson, person.EmailAddress); lvitemPerson.SubItems.Add(lvitemLastName); lvitemPerson.SubItems.Add(lvitemCompanyName); lvitemPerson.SubItems.Add(lvitemEmail); lvitemPerson.Tag = person; lvContacts.Items.Add(lvitemPerson); } }
The code above will use the Highrise API and retrieve the contacts from the web and load it into the list view on the custom Outlook form. The person class is a wrapper for the Highrise person object and is included in the sample project.
Lastly, we need to add code to add the selected contact’s e-mail address to the open e-mail when the user clicks the OK button. Add the following code to the button’s Click event:
private void btnOK_Click(object sender, EventArgs e) { Outlook.MailItem currentMail = null; try { currentMail = _inspector.CurrentItem as Outlook.MailItem; foreach (ListViewItem item in lvContacts.SelectedItems) { currentMail.To += ((Person)item.Tag).EmailAddress; } this.Dispose(); } finally { if (currentMail != null) Marshal.ReleaseComObject(currentMail); } }
Build, register and run your project and if everything goes according to plan, you should see your custom address lookup dialog when clicking the To button on the e-mail inspector window in Outlook.
Thank you for reading. Until next time, keep coding!
Available downloads:
This sample Outlook add-in was developed using Add-in Express for Office and .net: