65.9K
CodeProject 正在变化。 阅读更多。
Home

自定义内置的 Outlook 选择名称对话框(或其他)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (33投票s)

2007 年 11 月 13 日

CPOL

14分钟阅读

viewsIcon

173379

downloadIcon

2692

在本文中,您将学习如何自定义内置的“选择名称”对话框,并使用不同的外部数据源来创建自己的对话框。

Screenshot - CustomAddressDialogResult.png

引言

在本文中,您将学习如何拦截 Outlook 对话框并用您自己的 .NET 窗体替换它。Outlook 对象模型本身不提供任何事件和对象来替换内置对话框。然而,通过结合 VSTO、P/Invoke 和 .NET 技术,您就有能力替换您能想到的任何 Outlook 内置对话框。

优点

  • 简单使用外部数据进行地址选择:CRM、SQL、Web 服务、XML 文件、Outlook 表等。
  • 无需实现复杂的 COM 地址簿提供程序
  • 不使用需要在部署时注册的 COM 组件
  • 您自己的设计器用户界面
  • 您自己的地址选择业务逻辑,例如搜索和解析名称

缺点

  • 使用复杂的非托管代码
  • 取决于您安装的 Office 的版本和语言

背景

Microsoft Outlook 对象模型 (OOM) 功能强大,可访问许多用于使用和操作 Outlook 和 Exchange Server 中存储的数据的功能。以下是在 Microsoft Outlook 中使用外部数据的常见选项:

  • 将数据导入 Outlook 项目(联系人)
  • 使用 WebDAV 或 CDOSys 将数据导入 Exchange 存储
  • 创建您自己的地址簿提供程序

导入/导出数据耗时且容易出现同步冲突。数据会过时且不同步。以下是一个新场景,展示了如何拦截 Outlook 内置对话框并用您自己的对话框替换它。

想法

您可以从 Outlook 对象模型中获得的绝对是 Explorer 和 Inspector 对象,它们分别代表应用程序和数据项窗口。幸运的是,每当用户激活(有人单击它时)或停用(当另一个窗口向前移动时)某个窗口时,您都会收到这些对象的事件。您可以使用这些事件来获知窗口何时被激活或停用。当用户单击“收件人”、“抄送”按钮从收件人对话框中选择收件人时,也是如此。每当显示地址/收件人对话框时,您的 Inspector 窗口就会被停用。您可以在 Deactivate 事件处理程序中拦截并搜索已打开的收件人对话框,关闭收件人对话框,然后打开您自己的 .NET 窗体。

设置解决方案

在开始修改 Outlook 之前,您必须在开发计算机上安装最低要求。

必备组件

创建解决方案

为了演示这项技术,请启动一个 Outlook 加载项项目。在本例中,我使用的是 Outlook 2007(德语版)和 Visual Studio 2008 Beta 2,运行在 Vista 64 位系统上。

Screenshot - CustomAddressDialogSolution.png

创建项目后,您会找到一个名为 ThisAddIn 的骨架类,其中有两个方法:应用程序启动和终止。旅程从这里开始,我们将开始编写应用程序代码。

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
    }
}

Outlook InspectorWrapper 模板

正如您在许多文章中读到的,在编写 Outlook 加载项时,一个最常见的问题是,在 Outlook 会话的整个生命周期中,您可能随时打开和关闭多个 Explorer 和 Inspector。正确处理这种情况的一种方法是使用 Explorer/Inspector 包装器,它封装了每个窗口,并捕获不同窗口在其生命周期内的事件和状态,并进行适当的清理以避免内存泄漏和 Outlook 应用程序崩溃。这项技术也常用于 IExtensibility 加载项。参考:InspectorWrapper 示例(作者:H. Obertanner)

Inspector/Explorer 包装器模板基本上是一个具有唯一 ID 的包装类,它在类内部持有对被包装对象的引用,监控对象状态,并在对象关闭时通知应用程序。代码如下:

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
}

抽象的 WrapperClass

/// <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();
    }
}

Inspector 包装类

/// <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()
    {
    }
}

Explorer 包装类与 Inspector 包装类类似。有关更多详细信息,请参阅示例解决方案。事实上,您现在拥有了一个小型框架,可以用于成功构建您的 VSTO 加载项。

搜索内置的收件人对话框

既然您已经能够获知 Inspector 窗口何时变得不活跃(因为您会收到 Deactivate 事件),您现在就可以搜索收件人对话框了。您无法使用 .NET 托管代码做到这一点——您必须使用老旧的 Windows API。这项技术称为 P/Invoke,它是从托管代码访问非托管 API DLL 函数、方法和回调的方式。有关 P/Invoke 信息最好的在线资源是 MSDN Windows API 文档和一个名为 pinvoke.net 的网站。

在搜索对话框/窗口之前,您必须知道要搜索什么。幸运的是,Visual Studio 提供了一个名为 **Spy++** 的小工具。您可以使用此工具来搜索窗口、消息,甚至查找任何窗口的父窗口和子窗口。启动 Microsoft Outlook,创建一个新邮件,然后选择一个收件人。当显示内置的收件人对话框时,启动 **Spy++** 工具。它通常位于硬盘上的“C:\Program Files (x86)\Microsoft Visual Studio 9.0\Common7\Tools 文件夹”下。现在,您可以使用“查找窗口”功能并将目标拖到您的收件人对话框上。

使用 Spy++ 获取有关收件人对话框的信息

Screenshot - CustomAddressDialogSpy.png

您将获得有关所选窗口的一些信息。您将获得有关窗口文本、类名和句柄的信息。句柄是窗口在系统中的动态分配的唯一地址。因为它是由动态分配的,所以每次打开对话框时它都会发生变化,因此在这里没有帮助。标题(标题或窗口文本)会根据应用程序上下文而变化,在此处也没有帮助。您如何识别窗口?答案不是 42——而是通过类名及其子窗口。

现在有一个小挑战:由于我用 Outlook 的本地化版本编写了这个示例,您必须修改代码以满足您的需求和本地化。收件人对话框上的所有控件也是窗口,它们是收件人对话框的子窗口。下一个代码片段演示了如何使用一些 Windows API 函数来:

  • 使用类名 #32770 搜索窗口句柄
  • 枚举对话框的所有子窗口
  • 检索所有子窗口的窗口文本
  • 查看所需的所有子窗口是否都存在,以成功识别收件人对话框

检索所有子窗口及其窗口文本列表的方法封装在一个托管方法中,以将所有 API 调用保留在一个类中。

让我们开始吧——这是 WinApiProvider 类的代码。

/// <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);
}

这里有趣的是如何通过为您的托管对象分配非托管句柄来将托管泛型列表传递给非托管 API 函数。

现在,您想在 InspectorWrapper 中使用它来识别窗口。每次 Inspector 被停用时,我们都会去搜索对话框。

Inspector Deactivate 方法的事件接收器将如下所示:

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

    }
}

关闭内置对话框

您已经完成了第一个练习——识别内置对话框。您已经有了它的句柄,现在您需要关闭它。当您有一个托管的 .NET 窗体时,这很简单——但如果没有,它会有点棘手。在 Windows API 中,记录了两个方法:

您不能使用其中任何一个。为什么?当您收到此事件时,内置对话框尚未完全初始化,并且它运行在另一个线程中。但是,整个 Windows 系统都基于消息循环,窗口通过消息进行交互。所以,您只需向内置对话框发送一个 Close 消息。这与按下可见窗口上的 ESC 键效果相同。窗口会释放所有使用的资源并正常关闭。当窗口关闭后,您的 Inspector 窗口将再次激活,您将收到一个 Inspector_Activated 事件。

下一个代码块将展示如何关闭窗口以及用于显示我们自己的对话框的 activate 方法。

/// <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(); 
    }
}

下图显示了用于替换内置对话框的 .NET 窗体的设计。

Screenshot - CustomAddressDialogDesign.png

基本上,它有一个 DataGridView、“收件人”、“抄送”和“密送”按钮,以及相应的文本框,还有一个带组合框的“搜索”按钮。您想要现代化的酷炫功能,所以我们希望在键入组合框时过滤收件人列表。组合框应显示上次使用的搜索短语。

为了实现过滤,您可以使用带有附加 DataSetDataViewDataSet 可以轻松地使用 Visual Studio 设计,并用作 DataViewDataSource。所以,继续添加一个新的 DataSet 到应用程序中,并为收件人指定特定的字段。

Screenshot - CustomAddressDialogDataSet.png

不同的数据源

此对话框的实现取决于您的需求。仅为给您一个开始,您将在此示例中使用三种不同的数据源来填充您的新收件人对话框。

  • 使用 Table 对象(仅限 Outlook 2007)的内部 Outlook 数据
  • 外部 XML 文件
  • 使用 LINQ to SQL 和相应数据库的 SQL 数据

首先,您将访问“联系人”文件夹的数据。过去,您有以下选项之一来访问 Outlook 内部数据:

  • 循环遍历文件夹的 Items(非常慢,且在通常允许的 250 次 RPC 连接中存在问题)
  • 类似于选项 1,但缓存数据(需要同步)
  • 使用 CDO(不支持,安全违规)
  • 使用第三方 DLL,例如 Dmitry Streblechenko 的 Redemption

Outlook 2007 中新增了 Table 对象,它提供了对 Outlook 文件夹内容的快速访问。您将按如下所示使用它来获取个人联系人文件夹的内容并填充自定义对话框。辅助方法位于一个名为 OutlookUtility 的类中。您需要传递要检索的列的名称,并且可以对表项应用过滤器。

实现如下所示:

/// <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);
        }
    }
}

现在,仔细查看 .NET 窗体的实现。如前所述,您将使用一个 backgroundworker 来将数据推送到您的新对话框中。此外,您必须将对话框连接到您的 Inspector 的数据,以便您选择的收件人以某种方式显示在邮件中,反之亦然。

您可以通过将 Inspector 的 CurrentItem 对象传递给窗体,并在收件人对话框中直接修改该项来实现这一点。相应的代码如下所示:

/// <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();
    }
}

现在,您的对话框已连接到您的 Inspector,并且应该用数据填充。您希望保持应用程序的响应能力,因此决定为您的应用程序使用 backgroundworker。您从 Outlook 联系人文件夹数据开始。理论是:创建一个后台线程,获取文件夹表,循环遍历数据,并将其添加到您的数据集。在循环遍历数据时,显示进度条。完成后,启用所有用户界面元素。

研究 backgroundworker 进程更高级的代码

    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 + "*'";
    }

现在休息一下。您现在应该有一个初步可用的加载项了,并且到目前为止已达到了设计目标(有一些小错误)。您现在可以从这里下载此解决方案作为第一部分,并研究代码。

继续 - 在第一部分中,我们讨论了:

  • 创建 VSTO Outlook 应用程序加载项
  • Inspector/Explorer 包装器模板
  • 使用非托管 API 调用来处理超出 Outlook 对象模型的 Outlook 窗口
  • Outlook 2007 Table 对象
  • 使用 backgroundworker 来保持响应的用户界面

使用 LINQ to SQL 查询外部数据

您想为您的地址使用外部 SQL 数据库。好的——让我们创建一个。在企业中,您通常会使用中央 SQL Server。在这里,您将使用一个本地数据库,由您自己使用 SQL Server Express Edition 和新的 LINQ 语言扩展创建。

首先,您需要添加对 System.Data.Linq DLL 的引用。

Screenshot - CustomAddressDialogRefLINQ.png

在此场景中,只有一个简单的实体,因此称其为“Customer”。如果您还没有数据库,它将创建一个新数据库,并向其中添加一些客户。另外,提供两个从数据库检索数据回实体的有用方法将对应用程序有所帮助。注意:所有与数据库相关的类都放在名为“Database”的子文件夹/命名空间中。

Customer

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

}

CustomAddressDialogDB 类,继承自 DataContext

/// <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>();
    }
}

在您自定义的对话框中,您将使用新的数据源,并创建一个新的 backgroundworker,它将从数据库加载数据并用数据填充您的地址对话框。
您缺少的是数据库的连接字符串。您必须将数据库保存在您可以写入文件的位置。具体位置取决于您的系统账户的限制程度——最低限度,您可以写入 MyDocuments 文件夹。您如何获得它?在 .NET 3.5 中,这很容易通过

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

您的应用程序文档对应的目录将类似于

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

警告!当您尝试即时创建数据库时,此处存在一个问题——您将收到无访问权限异常。这是因为 SQLEXPRESS 实例默认情况下没有权限访问您的个人目录。但是,您很聪明,并赋予了服务在此目录中执行操作的权限。

为了简单起见,您在此处将控制权授予 Networkservice(德语为 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 );

现在,当您的自定义对话框首次打开时,将创建一个包含一些示例数据的新数据库,并准备好使用。

下一行显示了如何从服务器检索数据。

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

这很容易吗?此 backgroundworker 的其余部分与 Outlook 的非常相似,您可以参考示例代码。继续 Part 2

  • 使用新的 LINQ 语言功能创建简单的业务实体
  • Networkservice 添加正确的用户凭据到应用程序数据文件夹
  • 从头开始创建 SQL 数据库
  • 使用 LINQ 语言功能检索 SQL 数据
  • 将自定义的收件人选择对话框扩展到第二个数据源

下方下载带源代码的解决方案,第二部分

Outlook 特定调整

正如您所见,我是一个德国人,我使用的是 Microsoft Outlook 的本地化版本。然而,您如何找出当前运行的是德语版还是英语版?在 Outlook 2007 对象模型中,有一个名为 Application.LanguageSettings 的属性。语言 ID 1031 (0x407) 代表德语——ID 1033 (0x409) 代表英语版 Outlook。非常感谢 Ken Slovak 为我提供了“选择收件人”对话框的英文截图。以下代码片段将为您提供一种检索本地语言的方法。

    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;
    }

现在,每当您在 Inspector 窗口中单击“收件人”、“抄送”或“密送”按钮时,您会注意到,在您的对话框出现之前,您会在后台看到原始的“选择收件人”对话框闪烁一下。要阻止此窗口出现的唯一方法是使用一个不好的技巧。我们必须创建一个新的不可见窗口,并将原始窗口作为其子窗口。您需要两个额外的 API 调用——一个用于创建新窗口,一个用于更改窗口的父窗口。

这是所需的 API 调用:

/// <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);
}

然后,您可以在这里看到修改后的 InspectorWrapper 类。

        // 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();
    }
}

现在,恼人的原始对话框消失了。就这些了。通过这项技术,您现在有机会更改 Outlook 中的任何对话框。打印对话框等也是如此。现在,继续扩展您的 Outlook 定制功能,为您的客户带来更多价值。

第三部分总结

  • 确定 Outlook 实例的当前 UI 设置
  • 通过更改父窗口来抑制烦人的原始“选择收件人”对话框闪烁

下方下载第三部分(Outlook 调整)

注释

以下注释适用于此 VSTO 加载项以及所有 VSTO 加载项:

  • 此解决方案的临时密钥是在我的开发计算机上创建的。您必须创建并使用自己的密钥。
  • 该解决方案没有安装项目。有关分发 VSTO 加载项的信息,请参阅 部署 VSTO 解决方案
  • 对于您的加载项使用的每个 DLL,您都必须设置安全策略(MSI 安装包中的自定义操作)。

特别感谢:

历史

  • V.1.0 - 初始版本(2007 年 11 月 13 日)。
  • V.1.1 - 拼写更正(特别感谢 Ken Slovak)(2007 年 11 月 16 日)。
  • V.1.2 - 升级项目以支持 Visual Studio 2008 RTM 和 VSTO(2008 年 3 月 11 日)。
© . All rights reserved.