Click here to Skip to main content
Click here to Skip to main content

Customize the built-in Outlook Select Names dialog (or any other)

, 10 Mar 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
In this article, you will learn how to customize the built-in Select Names dialog and use different external data sources for your own dialog.

Screenshot - CustomAddressDialogResult.png

Introduction

In this article, you will learn how to intercept an Outlook dialog and replace it with your own .NET form. The Outlook Object Model itself exposes no events and objects to replace the built-in dialogs with your own. However, by combining VSTO, P/Invoke, and .NET technologies, you have the ability to replace any kind of Outlook built-in dialog you can imagine.

Benefits

  • Simple use of external data for address selection: CRM, SQL, Web Services, XML files, Outlook tables, etc.
  • Don't have to implement a complicated COM address book provider
  • No use of COM components which have to be registered at deployment
  • Your own designer user interface
  • Your own business logic in address selection, such as searching and resolving names

Drawbacks

  • Use of complicated unmanaged code
  • Depends on the version and language of your installed Office

Background

The Microsoft Outlook Object Model (OOM) is powerful, and provides access to many features that use and manipulate the data stored in Outlook and on the Exchange Server. Here are some common options to use external data in Microsoft Outlook:

  • Importing the data into Outlook Items (Contacts)
  • Importing data into the Exchange store using WebDAV or CDOSys
  • Creating your own address book provider

Importing / Exporting data is time consuming and a reason for synchronization conflicts. The data is outdated and out of sync. Here is a new scenario that shows how to intercept an Outlook built-in dialog and replace it with your own.

The idea

What you definitely can get from the Outlook Object Model are Explorer and Inspector objects which represent application and data item windows. Luckily, whenever such a window is activated by the user (when someone clicks on it), or when it is deactivated (when another window comes to the front), you will receive events from these objects. You can use these events to get notified when windows are activated or deactivated. This is also true when a user clicks on the "To" or "Cc" button to select a recipient from the Recipient dialog. Whenever the Address / Recipient dialog is shown, your inspector window is deactivated. You can intercept and search for the opened Recipient dialog in the Deactivate event handler, close the Recipient dialog, and open your own .NET form instead.

Set up the solution

Before you can start hacking into Outlook, you have to install the minimum requirements on your development machine.

Prerequisites

Create a solution

To demonstrate this technique, start an Outlook AddIn project. Here in my case, I have Outlook 2007 (German) and Visual Studio 2008 Beta 2 running on Vista 64 bit.

Screenshot - CustomAddressDialogSolution.png

After you have created the project, you will find a skeleton class called ThisAddIn with two methods where the application is started and terminated. Here, the journey begins, and we will start to code the application.

namespace CustomAddressDialog
{
    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

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

The Outlook InspectorWrapper template

As you can read in many articles, one of the most common problems when programming Outlook add-ins is the fact that you can have multiple Explorers and Inspectors opened and closed at any time during the lifetime of your Outlook session. One method of handling this situation correctly is by using an Explorer/Inspector wrapper which encapsulates each of these windows and traps the events and states of the different windows during their lifetime, and which does a proper cleanup to avoid ghost instances and crashes of your Outlook application. This technique is also common for IExtensibility add-ins. Reference: InspectorWrapper Sample(H. Obertanner).

The Inspector/Explorer wrapper template is basically a wrapper class which has a unique ID, holds a reference to the wrapped object inside the class, monitors the object state, and informs the application when the object has been closed. Here it goes:

public partial class ThisAddIn
{
    // the Outlook Inspectors collection
    Outlook.Inspectors _Inspectors;
    // the Outlook Explorers collection
    Outlook.Explorers _Explorers;

    // a collection of wrapped objects
    Dictionary<guid,WrappedObject> _WrappedObjects;

    /// <span class="code-SummaryComment"><summary></span>
    /// The entrypoint for the application
    /// <span class="code-SummaryComment"></summary></span>
    private void ThisAddIn_Startup(object sender, System.EventArgs e)
    {

        _WrappedObjects = new Dictionary<guid,WrappedObject>();

        // Inspectors stuff
        _Inspectors = this.Application.Inspectors;
        // Any open Inspectors after startup ?
        for (int i = _Inspectors.Count; i >= 1; i--)
        {
            // wrap the Inspector
            WrapInspector(_Inspectors[i]);
        }
        // get notified for new inspectors
        _Inspectors.NewInspector += new 
          Outlook.InspectorsEvents_NewInspectorEventHandler(_Inspectors_NewInspector); 
        

        // Explorer stuff
        _Explorers = this.Application.Explorers;
        // Are there any open Explorers after Startup ?
        for (int i = _Explorers.Count; i >= 1; i--)
        {
            // Wrap the Explorer and do something useful with it
            WrapExplorer(_Explorers[i]);
        }
        // get notified for new application windows
        _Explorers.NewExplorer += new 
          Outlook.ExplorersEvents_NewExplorerEventHandler(_Explorers_NewExplorer);

    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Event sink for the NewExplorer event.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""Explorer"""" />The new Explorer instance</param /></span>
    void _Explorers_NewExplorer(Outlook.Explorer Explorer)
    {
        WrapExplorer(Explorer);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// The Explorer is "wrapped" and used in the application.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""explorer"""" />The new Explorer instance</param /></span>
    void WrapExplorer(Outlook.Explorer explorer)
    {
        ExplorerWrapper wrappedExplorer = new ExplorerWrapper(explorer);
        wrappedExplorer.Closed += new WrapperClosedDelegate(wrappedObject_Closed);
        _WrappedObjects[wrappedExplorer.Id] = wrappedExplorer;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Event sink for the NewInspector event.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""Inspector"""" />The new Inspector instance</param /></span>
    void _Inspectors_NewInspector(Outlook.Inspector Inspector)
    {
        WrapInspector(Inspector);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// The Inspector is "wrapped" and used in the application.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""inspector"""" />The new Inspector instance</param /></span>
    void WrapInspector(Outlook.Inspector inspector)
    {
        InspectorWrapper wrappedInspector = new InspectorWrapper(inspector);
        wrappedInspector.Closed += new WrapperClosedDelegate(wrappedObject_Closed);
        _WrappedObjects[wrappedInspector.Id] = wrappedInspector; 
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Event sink for the WrappedInstanceClosed event.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""id"""" />The unique ID of the closed object</param /></span>
    void wrappedObject_Closed(Guid id)
    {
        _WrappedObjects.Remove(id); 
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Exitpoint for the application, do the cleanup here. 
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""sender"""" /></param /></span>
    /// <span class="code-SummaryComment"><param name=""""e"""" /></param /></span>
    private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
    {
        _WrappedObjects.Clear();
        _Inspectors.NewInspector -= new 
          Outlook.InspectorsEvents_NewInspectorEventHandler(_Inspectors_NewInspector); 
        _Inspectors = null;
        _Explorers.NewExplorer -= new 
          Outlook.ExplorersEvents_NewExplorerEventHandler(_Explorers_NewExplorer);
        _Explorers = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    #region VSTO generated code

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

The abstract WrapperClass:

/// <span class="code-SummaryComment"><summary></span>
/// Delegate signature to inform the application about closed objects.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name=""""id"""" />The unique ID of the closed object.</param /></span>
public delegate void WrapperClosedDelegate(Guid id);

/// <span class="code-SummaryComment"><summary></span>
/// The Wrapperclass itself has a unique ID and a closed event.
/// <span class="code-SummaryComment"></summary></span>
internal abstract class WrapperClass
{
    /// <span class="code-SummaryComment"><summary></span>
    /// The event occurs when the monitored item has been closed.
    /// <span class="code-SummaryComment"></summary></span>
    public event WrapperClosedDelegate Closed;

    /// <span class="code-SummaryComment"><summary></span>
    /// The unique ID of the wrapped object.
    /// <span class="code-SummaryComment"></summary></span>
    public Guid Id { get; private set; }

    protected void OnClosed()
    {
        if (Closed != null) Closed(Id);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// The constructor creates a new unique ID.
    /// <span class="code-SummaryComment"></summary></span>
    public WrapperClass()
    {
        Id = Guid.NewGuid();
    }
}

The Inspector wrapper class:

/// <span class="code-SummaryComment"><summary></span>
/// The InspectorWrapper used to monitor the state of an Inspector during its lifetime.
/// <span class="code-SummaryComment"></summary></span>
internal class InspectorWrapper : WrapperClass
{

    /// <span class="code-SummaryComment"><summary></span>
    /// The Outlook Inspector Instance.
    /// <span class="code-SummaryComment"></summary></span>
    public Outlook.Inspector Inspector { get; private set; }

    /// <span class="code-SummaryComment"><summary></span>
    /// Construction code. 
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""inspector"""" />The Inspector Object</param /></span>
    public InspectorWrapper(Outlook.Inspector inspector)
    {
        Inspector = inspector;
        ConnectEvents();
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Register the events to get notified of Inspector statechanges within the application.
    /// <span class="code-SummaryComment"></summary></span>
    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);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Unregister the events / cleanup.
    /// <span class="code-SummaryComment"></summary></span>
    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);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Event sink for the Close event. Memory Cleanup and inform the application.
    /// <span class="code-SummaryComment"></summary></span>
    void InspectorWrapper_Close()
    {
        DisconnectEvents();

        Inspector = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();

        // inform the application to release al references.
        OnClosed();
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Event sink for the Activate event
    /// <span class="code-SummaryComment"></summary></span>
    void InspectorWrapper_Activate()
    {

    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Event sink for the deactivate event
    /// <span class="code-SummaryComment"></summary></span>
    void InspectorWrapper_Deactivate()
    {
    }
}

The Explorer wrapper class is similar to the Inspector wrapper class. Refer to the sample solution to see additional details. In fact, what you now have is a small framework which could be used to successfully build your VSTO add-ins.

Search the built-in Recipient dialog

Now that you have arranged to be informed when your Inspector window becomes inactive (because you will receive the Deactivate event), you can search for the Recipient dialog now. You can't do it with .NET managed code - you have to use the good old Windows API for it. This technique is called P/Invoke, and it's the way to access unmanaged API DLL functions, methods, and callbacks from your managed code. The best online resources for information about P/Invoke are the MSDN Windows API documentation and a website called pinvoke.net.

Before you can search for the dialog/window, you have to know what to search for. Luckily, with Visual Studio, you get a small tool called Spy++. You can use this tool to search for windows, messages, and even to find the parent and child windows of any window. Start Microsoft Outlook, create a new mail, and select a recipient. When the built-in Recipient dialog is shown, start the Spy++ tool. It's usually located under "C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\Tools folder" on your hard disk. Now, you can use the "Find Window" function and drag the target onto your Recipient dialog.

Using Spy++ to get information about the Recipient dialog:

Screenshot - CustomAddressDialogSpy.png

What you will get is some information about the window you selected. You will get information about the window text, the class name, and the handle. The handle is a dynamically assigned unique address of the window in your system. Because it's dynamically assigned, it changes every time the dialog is opened, and therefore isn't helpful here. The caption (title or window text) changes depending on the application context, and doesn't help us here either. How can you identify the window? The answer is not 42 - it's by the class name and by its child windows.

Here now is a small challenge for you: Since I coded this sample with a localized version of Outlook, you have to modify the code to suit your needs and locality. All controls on the Recipient dialog are windows too, they are child windows of the Recipient dialog. The next snippet demonstrates how to use some Windows API functions to:

  • search for a window handle with the class name #32770
  • enumerate all child windows of the dialog
  • retrieve the window text of all the child windows
  • see if all the required children are there to successfully identify the Recipient dialog

The method to retrieve a list of all child windows and their window text is encapsulated in a managed method to keep all API calls inside of a class.

Let's roll - here's the code for the WinApiProvider class:

/// <span class="code-SummaryComment"><summary></span>
/// This class encapsulates all P/Invoke unmanaged functions.
/// <span class="code-SummaryComment"></summary></span>
[SuppressUnmanagedCodeSecurity]
internal class WinApiProvider
{
    /// <span class="code-SummaryComment"><summary></span>
    /// The FindWindow method finds a window by it's classname and caption. 
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""lpClassName"""" />The classname</span>
    ///       of the window (use Spy++)<span class="code-SummaryComment"></param /></span>
    /// <span class="code-SummaryComment"><param name=""""lpWindowName"""" />The Caption of the window.</param /></span>
    /// <span class="code-SummaryComment"><returns>Returns a valid window handle or 0.</returns></span>
    [DllImport("user32", CharSet = CharSet.Auto)]
    public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

    /// <span class="code-SummaryComment"><summary></span>
    /// Retrieves the Windowtext of the window given by the handle.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""hWnd"""" />The windows handle</param /></span>
    /// <span class="code-SummaryComment"><param name=""""lpString"""" />A StringBuilder object</span>
    ///       which receives the window text<span class="code-SummaryComment"></param /></span>
    /// <span class="code-SummaryComment"><param name=""""nMaxCount"""" />The max length</span>
    ///       of the text to retrieve, usually 260<span class="code-SummaryComment"></param /></span>
    /// <span class="code-SummaryComment"><returns>Returns the length of chars received.</returns></span>
    [DllImport("user32", CharSet = CharSet.Auto)]
    public static extern int GetWindowText(IntPtr hWnd, 
                  StringBuilder lpString, int nMaxCount);

    /// <span class="code-SummaryComment"><summary></span>
    /// Returns a list of windowtext of the given list of window handles..
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""windowHandles"""" />A list of window handles.</param /></span>
    /// <span class="code-SummaryComment"><returns>Returns a list with the corresponding</span>
    ///           window text for each window.<span class="code-SummaryComment"></returns /></span>
    public static List<string> GetWindowNames(List<IntPtr><intptr /> windowHandles)
    {
        List<string> windowNameList = new List<string>();
        
        // A Stringbuilder will receive our windownames...
        StringBuilder windowName = new StringBuilder(260);
        foreach (IntPtr hWnd in windowHandles)
        {
            int textLen = GetWindowText(hWnd, windowName, 260);

            // get the windowtext
            windowNameList.Add(windowName.ToString());
        }
        return windowNameList;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Returns a list of all child window handles for the given window handle.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""hParentWnd"""" />Handle of the parent window.</param /></span>
    /// <span class="code-SummaryComment"><returns>A list of all child window handles recursively.</returns></span>
    public static List<IntPtr> EnumChildWindows(IntPtr hParentWnd)
    {
        // The list will hold all child handles. 
        List<intptr /> childWindowHandles = new List<intptr />();

        // We will allocate an unmanaged handle
        // and pass a pointer to the EnumWindow method.
        GCHandle hChilds = GCHandle.Alloc(childWindowHandles);
        try
        {
            // Define the callback method.
            EnumWindowProc childProc = new EnumWindowProc(EnumWindow);
            // Call the unmanaged function to enum all child windows
            EnumChildWindows(hParentWnd, childProc, GCHandle.ToIntPtr(hChilds));
        }
        finally
        {
            // Free unmanaged resources.
            if (hChilds.IsAllocated)
                hChilds.Free();
        }

        return childWindowHandles;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// A method to enummerate all child windows of the given window handle.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""hWnd"""" />The parent window handle.</param /></span>
    /// <span class="code-SummaryComment"><param name=""""callback"""" />The callback method</span>
    ///       which is called for each child window.<span class="code-SummaryComment"></param /></span>
    /// <span class="code-SummaryComment"><param name=""""userObject"""" />A pointer </span>
    ///       to a userdefined object, e.g a list.<span class="code-SummaryComment"></param /></span>
    [DllImport("user32")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool EnumChildWindows(IntPtr hWnd, 
            EnumWindowProc callback, IntPtr userObject);

    /// <span class="code-SummaryComment"><summary></span>
    /// Callback method to be used when enumerating windows.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""hChildWindow"""" />Handle of the next window</param /></span>
    /// <span class="code-SummaryComment"><param name=""""pointer"""" />Pointer to a GCHandle that holds</span>
    /// a reference to the dictionary for our windowHandles.<span class="code-SummaryComment"></param /></span>
    /// <span class="code-SummaryComment"><returns>True to continue the enumeration, false to bail</returns></span>
    private static bool EnumWindow(IntPtr hChildWindow, IntPtr pointer)
    {
        GCHandle hChilds = GCHandle.FromIntPtr(pointer);
        ((List<intptr />)hChilds.Target).Add(hChildWindow);

        return true;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Delegate for the EnumChildWindows method
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""hWnd"""" />Window handle</param /></span>
    /// <span class="code-SummaryComment"><param name=""""parameter"""" />Caller-defined variable</param /></span>
    /// <span class="code-SummaryComment"><returns>True to continue enumerating, false to exit the search.</returns></span>
    public delegate bool EnumWindowProc(IntPtr hWnd, IntPtr parameter);
}

The interesting thing here is how to pass a managed generic list to an unmanaged API function by allocating an unmanaged handle to your managed object.

Now, you want to use it in your InspectorWrapper to identify the window. Every time your Inspector is deactivated, let's go and search for the dialog.

The Event sink for the Inspector Deactivate method will look like this:

/// <span class="code-SummaryComment"><summary></span>
/// Event sink for the Deactivate event
/// <span class="code-SummaryComment"></summary></span>
void InspectorWrapper_Deactivate()
{
    // check for a Dialog class
    IntPtr hBuiltInDialog = WinApiProvider.FindWindow("#32770", "");
    if (hBuiltInDialog != IntPtr.Zero)
    {
        // ok, found one
        // let's see what child windows there are
        List<intptr /> childWindows = WinApiProvider.EnumChildWindows(hBuiltInDialog); 
        // Let's get a list of captions for the child windows
        List<string> childWindowsText = WinApiProvider.GetWindowNames(childWindows);

        // now check some criteria to identify the built-in dialog..
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // !!! This part is only valid for German Outlook 2007 Version !!!
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        
        if (!childWindowNames.Contains("Nur N&ame")) return;
        if (!childWindowNames.Contains("&Mehr Spalten")) return;
        if (!childWindowNames.Contains("A&dressbuch")) return;
        // you can even check more criteria

        // OK - we have the built-in Select Names Dialog

    }
}

Closing the built-in dialog

You have mastered the first exercise - identify the built-in dialog. You have the handle to it, and now you have to close the dialog. When you have a managed .NET form, this is easy - but if not, it's a little trickier. In the Windows API, two methods are documented:

You can't use either of them. Why? When you are receiving this event, the built-in dialog is not initialized completely and it runs in another thread. But, the whole Windows system is based on a message loop where windows exchange messages to interact together. So, you simply send the built-in dialog a Close message. This is the same effect as pressing ESC on the visible window. The window frees all used resources and closes properly. When the window has been closed, your Inspector window will become active again and you will receive an Inspector_Activated event.

In the next code block, you will see how to close the window and the activate method that is used to display our own dialog:

/// <span class="code-SummaryComment"><summary></span>
/// Event sink for the Deactivate event
/// <span class="code-SummaryComment"></summary></span>
void InspectorWrapper_Deactivate()
{
    _showOwnDialogOnActivate = false;

    // check for a Dialog class
    IntPtr hBuiltInDialog = WinApiProvider.FindWindow("#32770", "");
    if (hBuiltInDialog != IntPtr.Zero)
    {
        // ok, found one
        // let's see what childwindows are there
        List<intptr /> childWindows = WinApiProvider.EnumChildWindows(hBuiltInDialog); 
        // Let's get a list of captions for the child windows
        List<string> childWindowNames = WinApiProvider.GetWindowNames(childWindows);

        // now check some criteria to identify the built-in dialog..
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // !!! This part is only valid for German Outlook 2007 Version !!!
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        if (!childWindowNames.Contains("Nur N&ame")) return;
        if (!childWindowNames.Contains("&Mehr Spalten")) return;
        if (!childWindowNames.Contains("A&dressbuch")) return;
        // you can even check more criteria

        // OK - we have the built-in Select Names dialog
        WinApiProvider.SendMessage(hBuiltInDialog, 
           WinApiProvider.WM_SYSCOMMAND, WinApiProvider.SC_CLOSE, 0);   
        // When our Inspector becomes active again, we should display our own dialog
        _showOwnDialogOnActivate = true;
    }
}

bool _showOwnDialogOnActivate;

/// <span class="code-SummaryComment"><summary></span>
/// Eventsink for the Activate event
/// <span class="code-SummaryComment"></summary></span>
void InspectorWrapper_Activate()
{
    if (_showOwnDialogOnActivate)
    {
        RecipientDialog customDialog = new RecipientDialog();
        customDialog.ShowDialog(); 
    }
}

The picture below shows the design of the .NET form used to replace the built-in dialog.

Screenshot - CustomAddressDialogDesign.png

Basically, it has a DataGridView, To, Cc, and Bcc buttons with corresponding textboxes, and a Search button with a combo box. You want to have cool looking modern functionality, and so we want to filter the Recipient list while typing into the combo box. The combo box should display the last used search phrases.

To realize the filtering, you can use a DataView with an attached DataSet. The DataSet can be easily designed with Visual Studio and used as the DataSource for the DataView. So, go ahead and add a new DataSet to the application with the specific fields for the recipients.

Screenshot - CustomAddressDialogDataSet.png

Different data sources

The implementation of this dialog is whatever you can imagine - it depends on what your requirements are. Just to give you a start, you will use three different data sources to fill up your new Recipients dialog in this sample.

  • Internal Outlook data by using the Table object (Outlook 2007 only)
  • An external XML file
  • SQL data using LINQ to SQL with a corresponding database

First, you will access the data of the Contacts folder. In the past, you had one of these options to access the Outlook internal data:

  • Loop over the folder Items (very slow and problematic with 250 RPC connections normally allowed)
  • Like option 1, but caching the data (synchronization required)
  • Use of CDO (not supported, security violations)
  • Use of a third party DLL such as Redemption from Dmitry Streblechenko

New in Outlook 2007 is a Table object which provides fast access to an Outlook folder's contents. You will use it as shown below to get the contents of the personal Contacts folder and populate the custom dialog. The helper methods are in a class called OutlookUtility. You have to pass the name of the columns you want to retrieve, and you can apply a filter on the table items.

The implementation looks like this:

/// <span class="code-SummaryComment"><summary></span>
/// Class with helpermethods for Outlookspecific functionality
/// <span class="code-SummaryComment"></summary></span>
internal class OutlookUtility
{
    /// <span class="code-SummaryComment"><summary></span>
    /// Returns the Table Object for the given default folder.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""defaultFolder"""" />The Default</param /></span>
    /// <span class="code-SummaryComment"><param name=""""filter"""" />A filter that</span>
    ///       could be passed to filter items.<span class="code-SummaryComment"></param /></span>
    /// <span class="code-SummaryComment"><returns>Returns the folder Table object.</returns></span>
    public static Outlook.Table GetFolderTable(Outlook.OlDefaultFolders defaultFolder, 
                                               string filter)
    {
        Outlook.MAPIFolder folder = 
          Globals.ThisAddIn.Application.Session.GetDefaultFolder(defaultFolder);
        return GetFolderTable(folder, filter);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Returns the Table Object for the passed folder.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""defaultFolder"""" />The Default</param /></span>
    /// <span class="code-SummaryComment"><param name=""""filter"""" />A filter that</span>
    ///       could be passed to filter items.<span class="code-SummaryComment"></param /></span>
    /// <span class="code-SummaryComment"><returns>Returns the folder Table object.</returns></span>
    public static Outlook.Table GetFolderTable(Outlook.MAPIFolder folder, string filter)
    {
        return folder.GetTable(filter, Missing.Value);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Prepares the Table object for setting what data to retrieve.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""table"""" />The Table object</param /></span>
    /// <span class="code-SummaryComment"><param name=""""columnNames"""" />An arry of columnnames</param /></span>
    public static void SetTableColumns(Outlook.Table table, string[] columnNames)
    {
        table.Columns.RemoveAll();
        foreach (string columnName in columnNames)
        {
            table.Columns.Add(columnName);
        }
    }
}

Now, take a closer look at the implementation of the .NET form. As mentioned earlier, you will use a backgroundworker to pump the data into your new dialog. Also, you have to connect the dialog to your Inspector's data, so that the recipients that you have selected shows up in the mail somehow and vice versa.

You can achieve this by passing the Inspector's CurrentItem object to the form and by modifying the item directly within the Recipient dialog. The corresponding code is shown below:

/// <span class="code-SummaryComment"><summary></span>
/// The custom Recipient Dialog
/// <span class="code-SummaryComment"></summary></span>
public partial class RecipientDialog : Form
{
    /// <span class="code-SummaryComment"><summary></span>
    /// Reference to the Outlook Item Object that should be modified here.
    /// <span class="code-SummaryComment"></summary></span>
    object _item;

    /// <span class="code-SummaryComment"><summary></span>
    /// Construction code.
    /// The Outlook item will be injected here.
    /// <span class="code-SummaryComment"></summary></span>
    public RecipientDialog(object item)
    {
        InitializeComponent();
        _item = item;

        // Read current data from item and set it into the user interface.
        ProcessPropertyTags(false);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Loop over all Controls.
    /// The name of the Outlook property to use is in the Tag of the UserControl. 
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""write"""" />If false, </span>
    ///          read the value from Item - if true write it back.<span class="code-SummaryComment"></param /></span>
    private void ProcessPropertyTags(bool write)
    {
        foreach (Control c in this.Controls)
        {
            if (!string.IsNullOrEmpty(c.Tag as string))
            // do we have a Tag value in the Control ? means bound to the Outlook Item
            {
                if (write)
                {
                    OutlookUtility.PropertySet(ref _item, (string)c.Tag, c.Text);
                }
                else
                {
                    c.Text = OutlookUtility.PropertyGet(ref _item, 
                                        (string)c.Tag).ToString();
                }
            }
        }
    }

    private void OKButton_Click(object sender, EventArgs e)
    {
        // Read all Data and write it back to the Outlook Item
        ProcessPropertyTags(true);
        DialogResult = DialogResult.OK;
        this.Close();
    }

    private void Cancel_Click(object sender, EventArgs e)
    {
        // Close without accepting the data change
        DialogResult = DialogResult.Cancel;
        this.Close();
    }

    private void Form_FormClosed(object sender, FormClosedEventArgs e)
    {
        _item = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

Now, you have the dialog connected to your Inspector and you should fill it with data. You want to maintain a responsive application, so the decision is to use a backgroundworker for your application. You start with Outlook Contacts Folder data. The theory says: create a background thread, get the folder table, loop over the data, and add it to your dataset. While looping over the data, show the progressbar. When finished, enable all user-elements.

Study the more advanced code of the backgroundworker process:

    DataView _dvContacts;

    /// <span class="code-SummaryComment"><summary></span>
    /// Indicates that the background process has been completed.
    /// <span class="code-SummaryComment"></summary></span>
    bool _outlookLoaderFinished;

    /// <span class="code-SummaryComment"><summary></span>
    /// This method is executed asynchronously in a separated thread
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""sender"""" />The backgroundworker instance.</param /></span>
    /// <span class="code-SummaryComment"><param name=""""e"""" />Parameter object</span>
    ///          that could be passed at initialization.<span class="code-SummaryComment"></param /></span>
    private void _outlookContactLoader_DoWork(object sender, DoWorkEventArgs e)
    {
        // get the folder table object and filter
        // only IPM.Contact items (no distributionlist items)
        Outlook.Table contactsTable = 
           OutlookUtility.GetFolderTable(Outlook.OlDefaultFolders.olFolderContacts, 
           "[MessageClass] = 'IPM.Contact'");
        // we're interrested only in some of the columns
        OutlookUtility.SetTableColumns(ref contactsTable, new string[] 
          { "EntryID", "FirstName", "LastName", 
            "CompanyName", "User1", "Email1Address" });

        // the itemCount is used for the progressbar
        int itemCount = contactsTable.GetRowCount();
        int count = 0;
        // access the table data and add it to our DataSet
        while (!contactsTable.EndOfTable && !e.Cancel)
        {
            count++;
            Outlook.Row row = contactsTable.GetNextRow();

            string entryId = row[1] as string;
            string firstName = row[2] as string;
            string lastName = row[3] as string;
            string company = row[4] as string;
            string customerId = row[5] as string;
            string email = row[6] as string;

            _dsContacts.ContactTable.AddContactTableRow(entryId, firstName, 
                                     lastName, email, company, customerId);

            _outlookContactLoader.ReportProgress(((int)count * 100 / itemCount));

        }
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Event sink for the RunWorkerCompleted event.
    /// Is called when the backgroundworker has been finnished.
    /// <span class="code-SummaryComment"></summary></span>
    private void _outlookContactLoader_RunWorkerCompleted(object sender, 
                                       RunWorkerCompletedEventArgs e)
    {
        _outlookLoaderFinished = true;
        RefreshUI();
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Eventsink for the ProgressChanged event.
    /// <span class="code-SummaryComment"></summary></span>
    private void _outlookContactLoader_ProgressChanged(object sender, 
                                       ProgressChangedEventArgs e)
    {
        UpdateProgress(e.ProgressPercentage);
    }

    private delegate void UpdateProgressDelegate(int progress);

    private void UpdateProgress(int progress)
    {
        if (ProgressBarStatus.InvokeRequired)
        {
            this.Invoke(new UpdateProgressDelegate(UpdateProgress), progress);
        }
        ProgressBarStatus.Value = progress;
        ProgressBarStatus.Update();
        ResultGrid.DataSource = _dvContacts;
    }

    private void RefreshUI()
    {
        bool allLoadersfinished = (_outlookLoaderFinished);
        ToButton.Enabled = CcButton.Enabled = BccButton.Enabled = allLoadersfinished;
        ProgressBarStatus.Visible = !allLoadersfinished;
        _dvContacts.RowFilter = GetRowFilterText(SearchTextComboBox.Text);
        ResultGrid.DataSource = _dvContacts;
    }

    private string GetRowFilterText(string searchText)
    {
        if (string.IsNullOrEmpty(searchText)) searchText = "*";
        return "[FirstName] LIKE '*" + searchText +
            "*' OR  [LastName] LIKE  '*" + searchText +
            "*' OR  [CompanyName] LIKE  '*" + searchText +
            "*' OR  [EmailAddress] LIKE  '*" + searchText + "*'";
    }

Take a break now. You should have an initial functional add-in now, and the design goals are reached to this point (with minor bugs). You can download this solution as Part 1 from here now and study the code.

Resume - In the first part, we discussed:

  • Create a VSTO Outlook application add-in
  • The Inspector/Explorer wrapper template
  • Use unmanaged API calls to deal with Outlook windows beyond the Outlook Object Model
  • The Outlook 2007 Table object
  • Use of a backgroundworker to keep a responsive user interface

Using LINQ to SQL to query external data

You want to use an external SQL database for your addresses. Fine - let's create one. In an enterprise, usually, you would use a central SQL Server. Here you use a local database, created by yourself with the SQL Server Express Edition and the new LINQ language extensions.

First, you need to add a reference to the System.Data.Linq DLL.

Screenshot - CustomAddressDialogRefLINQ.png

In this scenario, you have just one simple entity, so call it "Customer". You will create a fresh database if one doesn't exist already and add some customers to it. Also, two methods to retrieve the data back from the database into entities would be helpful for the application. Note: All DB related classes are placed in a subfolder/namespace called "Database".

The Customer class:

/// <span class="code-SummaryComment"><summary></span>
/// Represents a Customer Entity.
/// <span class="code-SummaryComment"></summary></span>
[Table(Name = "Customers")]
public class Customer
{
    [Column(IsPrimaryKey = true, IsDbGenerated = true)]
    public int CustomerId { get; set; }

    [Column(CanBeNull = false)]
    public string Firstname { get; set; }

    [Column(CanBeNull = false)]
    public string Lastname { get; set; }

    [Column (CanBeNull = false)]
    public string Emailaddress { get; set; }

    public string Companyname { get; set; }

}

The CustomAddressDialogDB class, inherited from DataContext:

/// <span class="code-SummaryComment"><summary></span>
/// Represents the Databasecontext for our Contact Database
/// <span class="code-SummaryComment"></summary></span>
public class CustomAddressDialogDB : DataContext
{

    /// <span class="code-SummaryComment"><summary></span>
    /// Construction code.
    /// Checks if the Database exists and if not, create a fresh DB from scratch.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""fileOrServerConnection"""" />The full Path to the desired </span>
    ///            Database or a valid connectionstring.<span class="code-SummaryComment"></param /></span>
    public CustomAddressDialogDB(string fileOrServerConnection)
        : base(fileOrServerConnection)
    {

// In Debugmode always create a fresh DB
#if DEBUG
        if (DatabaseExists())
        {
            DeleteDatabase();
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
#endif

        if (!DatabaseExists())
        {
           CreateDatabase();
           AddCustomer("Ken", "Slovak", "some.address@somedomain.com", "Slovaktech");
           AddCustomer("Sue", "Mosher", "some.address@otherdomain.com", "Turtleflock");
           AddCustomer("Dmitry", "Streblechenko", 
             "another.address@some.otherdomain.com", "Streblechenko");
           AddCustomer("Randy", "Byrne", "unknown@address.com", "Microsoft");
        }
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Defines the Customer Table object
    /// <span class="code-SummaryComment"></summary></span>
    public Table<Customer> _customerTable;

    /// <span class="code-SummaryComment"><summary></span>
    /// Adds a new customer to the customers table
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><returns>Returns the new ID of the customer instance.</returns></span>
    public int AddCustomer(string firstname, string lastname, 
                           string emailaddress, string companyname)
    {
        Customer customer = new Customer();
        customer.Firstname = firstname;
        customer.Lastname = lastname;
        customer.Emailaddress = emailaddress;
        customer.Companyname = companyname;
        _customerTable.InsertOnSubmit(customer); 
        SubmitChanges();
        return customer.CustomerId ;
    }


    /// <span class="code-SummaryComment"><summary></span>
    /// Search for all customers with the query in Lastname, Firstname or emailaddress
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name=""""query"""" />The criteria</param /></span>
    /// <span class="code-SummaryComment"><param name=""""maxItems"""" />Maximum Items to return</param /></span>
    /// <span class="code-SummaryComment"><returns>Returns a generic List of Customer objects.</returns></span>
    public List<Customer> FindCustomers(string query, int maxItems)
    {
        var q = from customer in _customerTable
                where customer.Lastname.Contains(query) 
                || customer.Firstname.Contains(query) 
                || customer.Emailaddress.Contains(query)
                orderby customer.Lastname, customer.Firstname
                select customer;
        return q.Take(maxItems).ToList<Customer>();
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Returns a collection of all Customers in the customers table
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><returns></returns></span>
    public List<Customer> GetCustomers()
    {
        var q = from customer in _customerTable
                orderby customer.Lastname, customer.Firstname
                select customer;
        return q.ToList<Customer>();
    }
}

In your customized dialog, you will use the new data source and create a new backgroundworker that will load the data from the database and fill up your Address dialog with data.
What you are missing is the connection string for your database. You have to save the database somehow where you have the possibility to write files. Where depends on how restricted your account on your system is - at the minimum, you can write to the MyDocuments folder. How can you get it? In .NET 3.5, this is easy with:

    /// <span class="code-SummaryComment"><summary></span>
    /// Returns the Path to MyDocuments
    /// <span class="code-SummaryComment"></summary></span>
    public static string GetMyDocumentsFolder(){
        return Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments );  
    }

The corresponding directory for your application documents would be something like:

    // The directory for the Databasefile
    string dataPath = Path.Combine(OutlookUtility.GetMyDocumentsFolder (), 
                                   "CustomAddressDialog");
    if (!Directory.Exists(dataPath)) Directory.CreateDirectory(dataPath);

Caution! There is a problem here when you try to create your database on the fly - you will receive a no access exception. This is because the SQLEXPRESS instance has no rights to access your personal directory, by default. But, you are smart and give the service the rights to do something in this directory.

Just to make it easy, you give control to the Networkservice here (in German, Netzwerkdienst):

    // Give the SQL Server instance access to this path
    //    - other wise you can't create a database on the fly
    DirectoryInfo di = new DirectoryInfo(dataPath);
    DirectorySecurity acl = di.GetAccessControl();
    acl.AddAccessRule (new FileSystemAccessRule  ("Networkservice", 
                FileSystemRights.FullControl, AccessControlType.Allow ));
    di.SetAccessControl(acl);
    
    dataPath = Path.Combine(dataPath, "CustomAddressDialogDB.mdf");

    // The Database will be created if there doesn't exist here
    Database.CustomAddressDialogDB db = 
      new CustomAddressDialog.Database.CustomAddressDialogDB(dataPath );

Now, the first time when your customized dialog is opened, a fresh database with some sample data is created and is ready for use.

The next line shows you how to get the data out of the server:

List<database.Customer> customers = db.GetCustomers ();

That was easy? The rest of this backgroundworker is very similar to the Outlook ones, you can refer to the sample code if you like. Resume for Part 2:

  • Create a simple business entity using the new LINQ language features
  • Add correct user credentials for the Networkservice to the application data folder
  • Create a SQL database from scratch
  • Retrieve SQL data using LINQ language features
  • Extend the custom select Recipient dialog with a second data source

Download the solution, part 2, with source code below:

Outlook specific tweaks

As you can see, I'm a German guy, and I'm using a localized version of Microsoft Outlook. However, how do you find out if you have a German or an English version currently running? In the Outlook 2007 Object Model, there is a property called Application.LanguageSettings. A language ID of 1031 (0x407) stands for German - an ID of 1033 (0x409), for the English version of Outlook. Many thanks to Ken Slovak here for providing me with the English screenshots of the Select Recipients dialog. The following code snippet will give you an idea of how you can retrieve the local language:

    int languageId = Inspector.Application.LanguageSettings.get_LanguageID(
                       Microsoft.Office.Core.MsoAppLanguageID.msoLanguageIDUI);
    switch (languageId)
    {
        case 1031:
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // !!! This part is only valid for German Outlook 2007 Version !!!
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            if (!childWindowNames.Contains("Nur N&ame")) return;
            if (!childWindowNames.Contains("&Mehr Spalten")) return;
            if (!childWindowNames.Contains("A&dressbuch")) return;
            // you can even check more criteria
            break;

        case 1033:
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // !!! This part is only valid for English Outlook 2007 Version !!!
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            if (!childWindowNames.Contains("N&ame only")) return;
            if (!childWindowNames.Contains("Mo&re Columns")) return;
            if (!childWindowNames.Contains("A&ddress Book")) return;
            break;

        // TODO: place your language here....

        default:
            return;
    }

Now, whenever you click the "To", "Cc", or "Bcc" button on an Inspector window, you will notice that before your own dialog shows up, you will see the original Select Recipient dialog flashing up in the background. The only way to suppress this window showing up is by using a bad trick. We have to create a new invisible window and make the original window a child of it. You need two additional API calls - one for creating a new window and one for changing the parent of a window.

Here are the needed API calls:

/// <span class="code-SummaryComment"><summary></span>
/// Set a new parent for the given window handle
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name=""""hWndChild"""" />The handle of the target window</param /></span>
/// <span class="code-SummaryComment"><param name=""""hWndNewParent"""" />The window handle of the parent window</param /></span>
[DllImport("user32")]
public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

/// <span class="code-SummaryComment"><summary></span>
/// Create a new window.
/// Description see http://msdn2.microsoft.com/en-us/library/ms632680.aspx
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name=""""dwExStyle"""" />Specifies the extended</span>
///       window style of the window being created<span class="code-SummaryComment"></param /></span>
/// <span class="code-SummaryComment"><param name=""""lpClassName"""" />A class name - </span>
///       see http://msdn2.microsoft.com/en-us/library/ms633574.aspx<span class="code-SummaryComment"></param /></span>
/// <span class="code-SummaryComment"><param name=""""lpWindowName"""" />Pointer to a null-terminated</span>
///       string that specifies the window name<span class="code-SummaryComment"></param /></span>
/// <span class="code-SummaryComment"><param name=""""dwStyle"""" />Specifies the style</span>
///       of the window being created<span class="code-SummaryComment"></param /></span>
/// <span class="code-SummaryComment"><param name=""""x"""" />The window startposition X</param /></span>
/// <span class="code-SummaryComment"><param name=""""y"""" />The window startposition Y</param /></span>
/// <span class="code-SummaryComment"><param name=""""nWidth"""" />Width</param /></span>
/// <span class="code-SummaryComment"><param name=""""nHeight"""" />Height</param /></span>
/// <span class="code-SummaryComment"><param name=""""hWndParent"""" />Parent window handle</param /></span>
/// <span class="code-SummaryComment"><param name=""""hMenu"""" />Handle to a menu</param /></span>
/// <span class="code-SummaryComment"><param name=""""hInstance"""" />Handle to the instance</span>
///       of the module to be associated with the window<span class="code-SummaryComment"></param /></span>
/// <span class="code-SummaryComment"><param name=""""lpParam"""" />Pointer to a value</span>
///       to be passed to the window through the CREATESTRUCT structure <span class="code-SummaryComment"></param /></span>
/// <span class="code-SummaryComment"><returns>If the function succeeds,</span>
///       the return value is a handle to the new window<span class="code-SummaryComment"></returns></span>
[DllImport("user32.dll")]
public static extern IntPtr CreateWindowEx(
   uint dwExStyle,
   string lpClassName,
   string lpWindowName,
   uint dwStyle,
   int x,
   int y,
   int nWidth,
   int nHeight,
   IntPtr hWndParent,
   IntPtr hMenu,
   IntPtr hInstance,
   IntPtr lpParam);
}

And, here you can see the modified InspectorWrapper class:

        // OK - we have the built-in Select Names dialog
        // Create a new invisible window
        _hWndInvisibleWindow = WinApiProvider.CreateWindowEx(0, "Static", 
          "X4UTrick", 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); 

        // use this window as the new Parent for the original dialog
        WinApiProvider.SetParent(hBuiltInDialog, _hWndInvisibleWindow);
        
        WinApiProvider.SendMessage(hBuiltInDialog, 
           WinApiProvider.WM_SYSCOMMAND, WinApiProvider.SC_CLOSE, 0);
        // When our Inspector becomes active again, we should display our own dialog
        _showOwnDialogOnActivate = true;
    }
}

/// <span class="code-SummaryComment"><summary></span>
/// A windows handle to an invisible window if we have found the built-in dialog
/// <span class="code-SummaryComment"></summary></span>
IntPtr _hWndInvisibleWindow;

/// <span class="code-SummaryComment"><summary></span>
/// Flag, used to indicate when our dialog should be displayed.
/// <span class="code-SummaryComment"></summary></span>
bool _showOwnDialogOnActivate;

/// <span class="code-SummaryComment"><summary></span>
/// Event sink for the Activate event
/// <span class="code-SummaryComment"></summary></span>
void InspectorWrapper_Activate()
{
    // Should we display our custom select recipients dialog ?
    if (_showOwnDialogOnActivate)
    {
        // Close the invisible window
        WinApiProvider.SendMessage(_hWndInvisibleWindow, 
          WinApiProvider.WM_SYSCOMMAND, WinApiProvider.SC_CLOSE, 0);

        // Display the custom dialog
        RecipientDialog customDialog = new RecipientDialog(Inspector.CurrentItem);
        customDialog.ShowDialog();
    }
}

Now, the annoying original dialog is gone. That's it for now. With this technique, now you have the opportunity to change any dialog within Outlook. This is also true for Print dialogs, etc. Now, go on and extend your Outlook customization with more functionality and added benefit for your customers.

Resume of Part 3:

  • Determining the current UI setting of your Outlook instance
  • Suppress the annoying original Select Recipients dialog flashing up by changing the parent window

Download Part 3 with Outlook tweaks below:

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).

Special thanks to:

History

  • V.1.0 - Initial version (13 November, 2007).
  • V.1.1 - Spelling corrections (Special thanks to Ken Slovak) (16 November, 2007).
  • V.1.2 - Upgraded projects to support Visual Studio 2008 RTM with VSTO (11 March, 2008).

License

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

Share

About the Author

Helmut Obertanner
Software Developer (Senior) X4U electronix
Germany Germany
I'm a 1968 model, made in Germany.
After School transformed into an electronic engineer and started repairing computers in 1986. From PET till now In the IT-world. Currently employed as softwaredeveloper in a company creating solutions for pharmacies.
MCSE2000, MCSD, MCTS - Team Foundation Server, MCTS - Visual Studio Tools for Office.
Specialized in Outlook/Exchange custom development. Languages: german, english, C#, C++, VB.Net, VB6, SQL.
In 2006 received an award as MVP - Visual Developer VSTO by Microsoft.
Homepage: [http://www.x4u.de]

Comments and Discussions

 
GeneralDoes anybody know how to make the ClickOnce MSB3482 error, and all the related aftermath, go away on Windows Server 2008 using Visual Studio 2010? [modified] PinmemberRedDK24-May-11 11:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141216.1 | Last Updated 11 Mar 2008
Article Copyright 2007 by Helmut Obertanner
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid