Click here to Skip to main content
15,867,771 members
Articles / Productivity Apps and Services / Microsoft Office

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

Rate me:
Please Sign up or sign in to vote.
4.90/5 (35 votes)
14 Jan 2015CPOL14 min read 166.6K   2.6K   67   31
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.

C#
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

        /// <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 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:

C#
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;

    /// <summary>
    /// The entrypoint for the application
    /// </summary>
    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);

    }

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

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

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

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

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

    /// <summary>
    /// Exitpoint for the application, do the cleanup here. 
    /// </summary>
    /// <param name=""""sender"""" /></param />
    /// <param name=""""e"""" /></param />
    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

    /// <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 abstract WrapperClass:

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

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

    /// <summary>
    /// The unique ID of the wrapped object.
    /// </summary>
    public Guid Id { get; private set; }

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

    /// <summary>
    /// The constructor creates a new unique ID.
    /// </summary>
    public WrapperClass()
    {
        Id = Guid.NewGuid();
    }
}

The Inspector wrapper class:

C#
/// <summary>
/// The InspectorWrapper used to monitor the state of an Inspector during its lifetime.
/// </summary>
internal class InspectorWrapper : WrapperClass
{

    /// <summary>
    /// The Outlook Inspector Instance.
    /// </summary>
    public Outlook.Inspector Inspector { get; private set; }

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

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

    /// <summary>
    /// Unregister the events / cleanup.
    /// </summary>
    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);
    }

    /// <summary>
    /// Event sink for the Close event. Memory Cleanup and inform the application.
    /// </summary>
    void InspectorWrapper_Close()
    {
        DisconnectEvents();

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

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

    /// <summary>
    /// Event sink for the Activate event
    /// </summary>
    void InspectorWrapper_Activate()
    {

    }

    /// <summary>
    /// Event sink for the deactivate event
    /// </summary>
    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:

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

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

    /// <summary>
    /// Returns a list of windowtext of the given list of window handles..
    /// </summary>
    /// <param name=""""windowHandles"""" />A list of window handles.</param />
    /// <returns>Returns a list with the corresponding
    ///           window text for each window.</returns />
    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;
    }

    /// <summary>
    /// Returns a list of all child window handles for the given window handle.
    /// </summary>
    /// <param name=""""hParentWnd"""" />Handle of the parent window.</param />
    /// <returns>A list of all child window handles recursively.</returns>
    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;
    }

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

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

        return true;
    }

    /// <summary>
    /// Delegate for the EnumChildWindows method
    /// </summary>
    /// <param name=""""hWnd"""" />Window handle</param />
    /// <param name=""""parameter"""" />Caller-defined variable</param />
    /// <returns>True to continue enumerating, false to exit the search.</returns>
    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:

C#
/// <summary>
/// Event sink for the Deactivate event
/// </summary>
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

    }
}</intptr>

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:

C#
/// <summary>
/// Event sink for the Deactivate event
/// </summary>
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;

/// <summary>
/// Eventsink for the Activate event
/// </summary>
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:

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

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

    /// <summary>
    /// Prepares the Table object for setting what data to retrieve.
    /// </summary>
    /// <param name=""""table"""" />The Table object</param />
    /// <param name=""""columnNames"""" />An arry of columnnames</param />
    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:

C#
/// <summary>
/// The custom Recipient Dialog
/// </summary>
public partial class RecipientDialog : Form
{
    /// <summary>
    /// Reference to the Outlook Item Object that should be modified here.
    /// </summary>
    object _item;

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

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

    /// <summary>
    /// Loop over all Controls.
    /// The name of the Outlook property to use is in the Tag of the UserControl. 
    /// </summary>
    /// <param name=""""write"""" />If false, 
    ///          read the value from Item - if true write it back.</param />
    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:

C#
DataView _dvContacts;

/// <summary>
/// Indicates that the background process has been completed.
/// </summary>
bool _outlookLoaderFinished;

/// <summary>
/// This method is executed asynchronously in a separated thread
/// </summary>
/// <param name=""""sender"""" />The backgroundworker instance.</param />
/// <param name=""""e"""" />Parameter object
///          that could be passed at initialization.</param />
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));

    }
}

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

/// <summary>
/// Eventsink for the ProgressChanged event.
/// </summary>
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:

C#
/// <summary>
/// Represents a Customer Entity.
/// </summary>
[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:

C#
/// <summary>
/// Represents the Databasecontext for our Contact Database
/// </summary>
public class CustomAddressDialogDB : DataContext
{

    /// <summary>
    /// Construction code.
    /// Checks if the Database exists and if not, create a fresh DB from scratch.
    /// </summary>
    /// <param name=""""fileOrServerConnection"""" />The full Path to the desired 
    ///            Database or a valid connectionstring.</param />
    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");
        }
    }

    /// <summary>
    /// Defines the Customer Table object
    /// </summary>
    public Table<Customer> _customerTable;

    /// <summary>
    /// Adds a new customer to the customers table
    /// </summary>
    /// <returns>Returns the new ID of the customer instance.</returns>
    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 ;
    }


    /// <summary>
    /// Search for all customers with the query in Lastname, Firstname or emailaddress
    /// </summary>
    /// <param name=""""query"""" />The criteria</param />
    /// <param name=""""maxItems"""" />Maximum Items to return</param />
    /// <returns>Returns a generic List of Customer objects.</returns>
    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>();
    }

    /// <summary>
    /// Returns a collection of all Customers in the customers table
    /// </summary>
    /// <returns></returns>
    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:

C#
/// <summary>
/// Returns the Path to MyDocuments
/// </summary>
public static string GetMyDocumentsFolder(){
    return Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments );
}

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

C#
// 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):

C#
// 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:

C#
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:

C#
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:

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

/// <summary>
/// Create a new window.
/// Description see http://msdn2.microsoft.com/en-us/library/ms632680.aspx
/// </summary>
/// <param name=""""dwExStyle"""" />Specifies the extended
///       window style of the window being created</param />
/// <param name=""""lpClassName"""" />A class name - 
///       see http://msdn2.microsoft.com/en-us/library/ms633574.aspx</param />
/// <param name=""""lpWindowName"""" />Pointer to a null-terminated
///       string that specifies the window name</param />
/// <param name=""""dwStyle"""" />Specifies the style
///       of the window being created</param />
/// <param name=""""x"""" />The window startposition X</param />
/// <param name=""""y"""" />The window startposition Y</param />
/// <param name=""""nWidth"""" />Width</param />
/// <param name=""""nHeight"""" />Height</param />
/// <param name=""""hWndParent"""" />Parent window handle</param />
/// <param name=""""hMenu"""" />Handle to a menu</param />
/// <param name=""""hInstance"""" />Handle to the instance
///       of the module to be associated with the window</param />
/// <param name=""""lpParam"""" />Pointer to a value
///       to be passed to the window through the CREATESTRUCT structure </param />
/// <returns>If the function succeeds,
///       the return value is a handle to the new window</returns>
[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:

C#
        // 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;
    }
}

/// <summary>
/// A windows handle to an invisible window if we have found the built-in dialog
/// </summary>
IntPtr _hWndInvisibleWindow;

/// <summary>
/// Flag, used to indicate when our dialog should be displayed.
/// </summary>
bool _showOwnDialogOnActivate;

/// <summary>
/// Event sink for the Activate event
/// </summary>
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)


Written By
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

 
QuestionMore than one #32770 window Pin
FlavioBria29-Jul-16 5:53
FlavioBria29-Jul-16 5:53 
GeneralMessage Closed Pin
23-Jul-17 19:33
Member 1332480923-Jul-17 19:33 
QuestionMessage Closed Pin
13-Jan-15 8:58
Member 1137275513-Jan-15 8:58 
AnswerRe: Make it work for outlook 2010? Pin
Helmut Obertanner13-Jan-15 21:09
Helmut Obertanner13-Jan-15 21:09 
GeneralCompiles now, but I'm confused about the downloaded content Pin
RedDk26-May-11 12:36
RedDk26-May-11 12:36 
GeneralRe: Compiles now, but I'm confused about the downloaded content Pin
Helmut Obertanner26-May-11 19:39
Helmut Obertanner26-May-11 19:39 
GeneralRe: Compiles now, but I'm confused about the downloaded content Pin
RedDk28-May-11 7:06
RedDk28-May-11 7:06 
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] Pin
RedDk24-May-11 10:34
RedDk24-May-11 10:34 
GeneralMessage Closed Pin
10-Mar-10 16:25
Jeans.zhou10-Mar-10 16:25 
GeneralRe: outlook sidebar Pin
Helmut Obertanner10-Mar-10 19:19
Helmut Obertanner10-Mar-10 19:19 
GeneralMessage Closed Pin
11-Mar-10 3:08
Jeans.zhou11-Mar-10 3:08 
GeneralRe: outlook sidebar Pin
Jeans.zhou11-Mar-10 3:18
Jeans.zhou11-Mar-10 3:18 
GeneralEditing E-mail Properties Dialog Pin
NiceNaidu19-Nov-09 23:14
NiceNaidu19-Nov-09 23:14 
QuestionHow to create a custom button in Meeting Request Pin
T G Shiva28-Sep-09 20:05
T G Shiva28-Sep-09 20:05 
QuestionProblem to run Customize the built-in Outlook Select Names dialog Pin
zengfuxu27-May-09 17:23
zengfuxu27-May-09 17:23 
AnswerRe: Problem to run Customize the built-in Outlook Select Names dialog Pin
Helmut Obertanner27-May-09 20:00
Helmut Obertanner27-May-09 20:00 
AnswerRe: Problem to run Customize the built-in Outlook Select Names dialog Pin
Helmut Obertanner27-May-09 20:06
Helmut Obertanner27-May-09 20:06 
QuestionCustomise the built-in Outlook Select Names dialog Pin
zengfuxu19-May-09 18:38
zengfuxu19-May-09 18:38 
AnswerRe: Customise the built-in Outlook Select Names dialog Pin
Helmut Obertanner19-May-09 19:13
Helmut Obertanner19-May-09 19:13 
QuestionRe: Customise the built-in Outlook Select Names dialog Pin
zengfuxu25-May-09 22:38
zengfuxu25-May-09 22:38 
GeneralHI i need some help Pin
wasimsharp10-Feb-09 19:06
wasimsharp10-Feb-09 19:06 
GeneralRe: HI i need some help Pin
Helmut Obertanner10-Feb-09 22:50
Helmut Obertanner10-Feb-09 22:50 
GeneralRe: HI i need some help Pin
wasimsharp10-Feb-09 23:16
wasimsharp10-Feb-09 23:16 
GeneralRe: HI i need some help Pin
Helmut Obertanner10-Feb-09 23:22
Helmut Obertanner10-Feb-09 23:22 
hi,

for me it looks that the filter must be:

string sFilter = "[CustomerID] = '" + objId.contactId + "'";

instead of

string sFilter = "[Customer ID] = '" + objId.contactId + "'";

Hope this helps,
greets - Helmut

greets, Helmut
[http://www.x4u.de]

GeneralRe: HI i need some help Pin
wasimsharp12-Feb-09 19:20
wasimsharp12-Feb-09 19:20 

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

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