摘要: 本文介绍了 Microsoft Outlook 2003 对象模型介,并探讨了如何使用 C# 编程语言生成 Outlook 识别的应用程序和 Outlook 外接程序。(35 页打印页)
注 本文假设您已经熟悉 C# 编程语言和 .NET 平台。Outlook 2003 的开发知识不是必需或期望的。
适用于:
Microsoft Visual Studio .NET 2003
Microsoft Visual C# .NET 2003
Microsoft Outlook 2003
本页内容
作为对象模型的 Outlook 2003
在将应用程序的功能公开给外部程序方面,Microsoft 有很长的历史。例如,如果项目需要拼写检查功能,您可以利用从 Microsoft_ Word 公开的对象模型。以同样的方式,如果正在生成的应用程序需要 Microsoft_ Outlook_ 2003 提供的功能,则可以利用关联的对象模型。简单地说,Outlook 2003 对象模型允许您与下列各项交互:
• |
电子邮件项。 |
• |
Outlook 联系人数据库。 |
• |
Outlook 日历。 |
• |
Outlook 注释和任务。 |
• |
Outlook 本身的UI(资源管理器、检查器、命令栏等)。 |
这显然是所包含功能的子集,但我肯定您有了大概的了解:可以通过关联的对象模型来访问 Outlook 2003 的功能。
Outlook 主互操作程序集 (PIA)
到目前为止,Outlook 的功能是通过一个基于 COM 的进程内服务器 (msoutl.olb) 来公开的。.NET 开发人员希望与这些 COM 类型交互,因此,您需要通过互操作层来这样做。Microsoft Corporation 已经提供了一个 Outlook 2003 附带的"正式的"互操作程序集(即主互操作程序集)。
该程序集已经强命名,并驻留在名称 Microsoft.Office.Interop.Outlook.dll 下面的全局程序集缓存中。要从 Microsoft_ Visual Studio_ .NET 2003 引用该程序集,请从"Add References"对话框访问"COM"选项卡,并选择"Microsoft Outlook 11.0 Object Library"(图 1)。
图 1. 引用 Outlook PIA
注 如果利用 Outlook PIA 的以前版本(或多个版本)生成自定义应用程序,一定要阅读 http://go.microsoft.com/fwlink/?LinkId=30833,该文讨论了某些可能的版本冲突。
有关互操作程序集的简介
任何互操作程序集的最终目标都是要提供外观与体验与原始 COM 类型相似的 .NET 类型。互操作层(结合运行库生成的代理,该代理的术语名称是"运行库可调用包装",即 RCW)处理各种关于封送处理数据类型的详细信息。例如,如果接口方法定义为接受一个基于 COM 的 BSTR 参数,则 .NET 开发人员可以自由传递基于 CLR 的 System.String。
对于每个 COM 类,互操作程序集包含总是带有"–Class"后缀的具体类型,以及名称相同的托管等价项。例如,以下 COM IDL 定义:
coclass MyComObject { // Assume this interface defines three methods named // One(), Two(), Three(). [default] interface IMyComObject; // Assume this interface defines two methods named // Four(), Five(). interface ISomeOtherInterface; }
结果是两个名为 MyComObject 和 MyComObjectClass 的 .NET 类类型。MyComObject 类型只公开 [default] 接口的成员。希望访问其他接口的功能时,需要执行显式转换(这将在后台触发对 IUnknown::QueryInterface() 的调用):
// Create a new MyComObject. MyComObject c = new MyComObject(); c.One(); // To call Four() or Five(), we need to obtain the // ISomeOtherInterface explicity. ISomeOtherInterface itfOther = (ISomeOtherInterface)c; itfOther.Five();
另一方面,如果利用 MyComObjectClass 类型,您就能够使用单个对象引用来访问每个接口中的每个成员:
// Create a new MyComObjectClass. MyComObjectClass c = new MyComObjectClass(); c.One(); c.Five();
实际上,带 –Class 后缀的类型是原始 COM 类型的所有接口方法的联合。假定 Outlook 2003 coclass 最初旨在仅支持单个 [default] COM 接口,则通常可以忽略以 –Class 为后缀的类型,并利用名称相同的 .NET 代理。
注 如果希望更深入讨论 COM 到 .NET 的转换过程,访问 http://blogs.msdn.com/eric_carter/archive/2004/05/06/127698.aspx 将很有帮助。
关于从托管语言与 COM 类型的交互,最后一点与 CLR 垃圾收集器的角色有关。.NET 内存管理模型在本质上是不确定的,因为我们作为开发人员不会准确知道对象将在什么时候被销毁,而只知道它最终会被销毁。另一方面,COM 内存模型在本质上具有很麻烦的确定性,因为我们被迫使用各种 IUnknown::AddRef() 和 IUnknown::Release() 调用来手动调整对 COM 类型的对象内部引用计数(尽管 Visual Basic_ 6.0 试图隐藏这一点)。
针对互操作程序集中的类型进行编程时,将像任何 CLR 引用类型一样对代理类型进行垃圾收集。一旦代理已经被垃圾收集,RCW 将把所有需要的 IUnknown::Release() 调用转发给关联的 COM 对象,并在这时销毁代理。使用该技术,可以确信只要代理在内存中是活动的,则关联的 COM 对象在内存中也是活动的。
如果希望确保 COM 类型以更及时和更可预言的方式被销毁,则可以利用 System.Runtime.InteropServices.Marshal 类型。该类定义了一个静态方法,名为 ReleaseComObject()。只是传递对象引用,关联的 COM 类型将被当场销毁:
using System.Runtime.InteropServices; class MyClass { public void SomeMethod() { MyComObjectClass c = new MyComObjectClass(); ... // Explicitly destroy COM type after use. Marshal.ReleaseComObject(c); } }
虽然销毁 COM 类型的想法听起来可能很吸引人,但必须知道在 AppDomain 中的其他 CLR 对象现在无法使用基本 COM 类型。有了这种(危险的)可能性,本文中的示例代码将避免使用 Marshal.ReleaseComObject()。
注 在即将发布的 .NET 平台(Microsoft_ Visual Studio.NET_ 2005,即 Whidbey)的版本中,这个问题已经得到解决。有关进一步的详细信息,请参阅 http://blogs.msdn.com/yvesdolc/archive/2004/04/17/115379.aspx。
Outlook 2003 对象模型
一旦引用了 Outlook PIA,下一个任务就是调查 Microsoft.Office.Interop.Outlook 命名空间中的很多类型(图 2)。
图 2. Microsoft.Office.Interop.Outlook 命名空间
不管类型的大小是多少,好消息是对象模型本身组织得非常好,并利用了常见设计模式。因此,一旦了解如何遍历联系人列表,则遍历收件箱项就会很简单。
其他的好消息是,整个对象模型在帮助文件 (vbaol11.chm) 中有完整的介绍,默认情况下该帮助文件位于 :\Program Files\Microsoft Office\OFFICE11\1033 下(图 3)。
图 3. Outlook 2003 文档
现在,坏消息(取决于您的视点)是帮助系统使用 VBScript 代码示例和成员原型。假定本文没有试图详细介绍 Outlook 2003 对象模型中的每个类型,请您参考该帮助系统来获得完整的信息。下面,让我们来研究某些核心类类型。
Application 类型
第一个要知道的类型被适当地称为"Application",它是层次结构中其他所有对象的根。一旦获得该类型的对象,就能以编程方式控制 Outlook 本身的所有方面。表 1 列出了某些需要注意的(但决不是所有)成员。
表 1。选择 Application 类型的成员
Application 类型的成员 | 基本含义 |
ActiveExplorer() ActiveInspector() |
这些方法分别从当前 Outlook 实例检索 Explorer / Inspector 类型。本文随后描述资源管理器/检查器模型。 |
CreateItem() |
允许通过编程创建新的 Outlook 项。 |
GetNamespace() |
提供对数据存储项的访问。到 Outlook 2003 为止,MAPI 是唯一可以使用的命名空间,它用于访问 Outlook 文件夹组(收件箱、注释等)。 |
Quit() |
终止当前 Outlook 会话。 |
COMAddIns |
该属性允许您在运行时发现插入到当前 Outlook 实例中的外接程序集合。 |
Explorers Inspectors |
这些属性允许获得强类型的 Explorers/Inspectors 集合。 |
获得 Application 类型的确切方式将根据所开发软件的种类而有略微的不同。如果要生成一个合并 Outlook 2003 的自定义应用程序,则要做的所有事情是使用 C# new 关键字创建该类型:
// Create an Outlook Application object. Application outLookApp = new Application();
另一方面,生成 Outlook 2003 外接程序时(本文随后介绍),将通过名为 OnConnection() 的方法传递 Application 实例:
public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom) { // Assume ‘applicationObject‘ is a member variable of // type Microsoft.Office.Interop.Outlook.Application. applicationObject = (Microsoft.Office.Interop.Outlook.Application) application; }
除了各种属性和方法以外,Application 类型还定义了在各种环境中触发的很多事件(StartUp、Quit、ItemSend、NewMailEx)。请考虑以下代码片段:
public class MyApp { public static void Main() { // Create an Outlook Application object. Application outLookApp = new Application(); // Rig up the NewMailEx event. outLookApp.NewMailEx += new ApplicationEvents_11_NewMailExEventHandler(outLookApp_NewMailEx); ... } private static void outLookApp_NewMailEx(string EntryIDCollection) { // Do something interesting when a new e-mail arrives. } }
再次,在给定互操作层的角色后,处理基于 COM 的事件的过程看起来与处理 CLR 事件的过程相同。暂时不要理会细节,只需注意 NewMailEx 事件与一个具体的委托(ApplicationEvents_11_NewMailExEventHandler) 一起工作,这个委托可以调用任何接受 System.String 作为其唯一参数,并且不返回任何内容的方法。
Outlook"Item"类类型
一旦有了 Application 类型,就能创建新的 Outlook"项"。通过 Microsoft.Office.Interop.Outlook.OlItemType 枚举可以列出可能的项:
public enum OlItemType { olAppointmentItem = 1; olContactItem = 2; olDistributionListItem = 7; olJournalItem = 4; olMailItem = 0; olNoteItem = 5; olPostItem = 6; olTaskItem = 3; }
假设您希望通过编程来创建新的 Outlook Task 项。要这样做,请指定 OlItemType.olTaskItem 作为 Application.CreateItem() 的参数:
public static void Main() { // Create an Outlook Application object. Application outLookApp = new Application(); // Create a new TaskItem. TaskItem newTask = (TaskItem)outLookApp.CreateItem(OlItemType.olTaskItem); // Configure the task at hand and save it. newTask.Body = "Don‘t forget to send DOM the links..."; newTask.DueDate = DateTime.Now; newTask.Importance = OlImportance.olImportanceHigh; newTask.Subject = "Get DOM to stop bugging me."; newTask.Save(); }
注意,CreateItem() 的返回值是一般的 OlItemType;因此需要显式地将结果转换为正确的类型(在这里是 TaskItem)。这时,只需要使用类型的公共接口来配置项。一旦执行,将能够在 Outlook 任务检查器中查找任务(图 4)。
图 4. 通过编程生成的任务
尽管 OlItemType 枚举的名称很简单,但表 2 详细列出了 OlItemType 枚举的成员与 Application.CreateItem() 产生的返回类型之间的关系。
表 2。OlItemType enum/Outlook 类类型关系
OlItemType 枚举值 | 所产生的类型 | 基本含义 |
olAppointmentItem |
AppointmentItem |
表示单个约会。 |
olContactItem |
ContactItem |
表示单个联系人。 |
olDistributionListItem |
DistributionListItem |
表示一个通讯组列表。 |
olJournalItem |
JournalItem |
表示单个日记项。 |
olMailItem |
MailItem |
表示单个电子邮件项。 |
olNoteItem |
NoteItem |
表示单个注释。 |
olPostItem |
PostItem |
表示其他人可能浏览的公用文件夹中的公告。 |
olTaskItem |
TaskItem |
表示单个任务。 |
获得现有 Outlook 项
除了创建新项以外,Outlook 2003 模型还允许获得(并且可能修改)现有项。不管对枚举哪个 Outlook 项感兴趣,基本过程都是:
• |
从 Application.GetNamespace() 获得 NameSpace 类型。 |
• |
从 NameSpace.GetDefaultFolder() 获得 MAPIFolder 类型。 |
• |
使用 MAPIFolder.Items 索引器枚举子项。 |
指定字符串"MAPI"作为 GetNamespace() 的参数时,将收到一个 NameSpace 类型,该类型表示具体的 Outlook 数据存储的抽象级别(目前,"MAPI"是唯一有效的命名空间)。MAPIFolder 类型可以表示给定用户的邮件存储中的任何文件夹(已删除项、收件箱、日记项等)。文件夹选项的完整范围由 OlDefaultFolders 枚举来表示:
public enum OlDefaultFolders { olFolderCalendar = 9; olFolderConflicts = 19; olFolderContacts = 10; olFolderDeletedItems = 3; olFolderDrafts = 16; olFolderInbox = 6; olFolderJournal = 11; olFolderJunk = 23; olFolderLocalFailures = 21; olFolderNotes = 12; olFolderOutbox = 4; olFolderSentMail = 5; olFolderServerFailures = 22; olFolderSyncIssues = 20; olFolderTasks = 13; olPublicFoldersAllPublicFolders = 18; }
要请求具体的文件夹,请将 OlDefaultFolders 枚举中的值指定为 NameSpace.GetDefaultFolder() 的参数。请考虑以下代码,这些代码枚举了当前用户的任务集合:
static void Main(string[] args) { // Create an Outlook Application object. Application outLookApp = new Application(); // Print all tasks. NameSpace outlookNS = outLookApp.GetNamespace("MAPI"); MAPIFolder theTasks = outlookNS.GetDefaultFolder(OlDefaultFolders.olFolderTasks); foreach(TaskItem task in theTasks.Items) { Console.WriteLine("-> Time Created: {0}", task.CreationTime); Console.WriteLine("-> Body: {0}", task.Body); } }
Inspectors 和 Explorers
Outlook 对象模型不仅使您能够访问各种项,还定义了用来对用户界面进行操作的类型。Explorer 类型表示用于显示文件夹内容的窗口。另一方面,Inspectors 表示打开后可查看的单个项。Application 类维护一个由所有 Explorers 和 Inspectors 组成的集合,通过使用适当命名的 Explorers / Inspectors 属性可以获得这些类型:
Application app = new Application(); Explorers theExplorers = app.Explorers; foreach(Explorer e in theExplorers) { // Do something with each Explorer... }
Application 类的 GetActiveExplorer() 和 GetActiveInspector() 方法可以用来获得当前活动的 UI 元素:
Application app = new Application(); Explorer activeExp = app.ActiveExplorer(); Console.WriteLine("Explorer caption: {0}", activeExp.Caption);
当您生成自定义的 Outlook 外接程序时,Explorers 和 Inspectors 是很有用的,因为它们让您能够将 UI 小部件附加到现有的 CommandBars 集合中。本文稍后将进一步介绍这方面的情况。
生成 Outlook 识别的应用程序
要重点操作 Outlook 的对象模型(而不是生成奇特的用户界面),第一个示例将利用简单的命令行用户界面。如果希望跟着做,请创建一个新的 C# 控制台应用程序,并命名为 OPine。Unix 用户可能知道,"Pine"是一个很流行的命令行电子邮件实用工具的名称。OPine 将模仿 Pine 的功能子集。具体来说,OPine 将响应以下命令:
• |
dib:显示收件箱项 |
• |
snm:发送新邮件项 |
• |
cn:创建新注释 |
• |
dn:显示现有注释 |
• |
q:退出 OPine |
通过响应 NewMailEx 事件,OPine 还能在新邮件到达时通知用户。
注OPine 将利用 ApplicationClass 类型(而不是 Application)来解决一个在随后引用 System.Windows.Forms.dll 程序集时引入的名称冲突。也可以使用如下所示的 C# 别名解决名称冲突:
using OutLookApp = Microsoft.Office.Interop.Outlook.Application;
但在这种情况下,使用 –Class 类型将不会损害 OPine 示例。
处理"dib"命令
假定已经引用了 Outlook 2003 PIA,下一步是创建一个帮助器类 (OPineHelper),用该类定义一组执行批量处理的静态方法。首先,我们有一个名为 DisplayInbox() 的方法,该方法接受 ApplicationClass 类型作为其唯一参数。DisplayInbox() 的实现将获得当前的 MAPI 命名空间,以便检索收件箱文件夹中的每个 MailItem。在这里,我们将使用 MailItem 类型的各种属性,将接收时间、发件人名称和主题打印到控制台:
public static void DisplayInbox(ApplicationClass o) { // Get items in my inbox. NameSpace outlookNS = o.GetNamespace("MAPI"); MAPIFolder inboxFolder = outlookNS.GetDefaultFolder(OlDefaultFolders.olFolderInbox); // Print out some basic info. Console.WriteLine("You have {0} e-mails.", inboxFolder.Items.Count); Console.WriteLine(); foreach(object obj in inboxFolder.Items) { MailItem item = obj as MailItem; if(item != null) { Console.WriteLine("-> Received: {0}", item.ReceivedTime.ToString()); Console.WriteLine("-> Sender: {0}", item.SenderName); Console.WriteLine("-> Subject: {0}", item.Subject); Console.WriteLine(); } } }
注意,我们将通过 Items 属性所获得的项当作一般 System.Objects,而不是所期望的 MailItem 类型。此外还要注意,我们执行了一个动态检查,以确定当前项是否可以被视为 MailItem(通过 C# 的 as 关键字),以及如果这样我们将与类型的各种属性交互。我们执行该动态检查的理由是 Outlook 收件箱的确可以包含超过 MailItem 类型的项(例如,满足请求)。如果将 foreach 逻辑设置为:
foreach(MailItem item in inboxFolder.Items) { ... }
那么,如果遇到 MailItem 以外的任何内容,就可以收到运行库异常。
在任何情况下,除了 ReceivedTime、SenderName 和 Subject 属性,MailItem 类型还能够访问附件和重要性级别,以及内容的 HTML 表现形式(通过 HTMLBody 属性)。有关这方面的完整细节,请参阅 Outlook 2003 文档。
处理"snm"命令
OPineHelper 的下一个静态方法是 SendNewMail(),该方法负责代表用户创建和发送新的电子邮件。和前面看到的一样,我们将通过 ApplicationClass.CreateItem() 创建新的 MailItem 类型。阅读到这里,以下代码应当很容易理解:
public static void SendNewMail(ApplicationClass o) { // Create a new MailItem. MailItem myMail = (MailItem)o.CreateItem(OlItemType.olMailItem); // Now gather input from user. Console.Write("Receiver Name: "); myMail.Recipients.Add(Console.ReadLine()); Console.Write("Subject: "); myMail.Subject = Console.ReadLine(); Console.Write("Message Body: "); myMail.Body = Console.ReadLine(); // Send it! myMail.Send(); }
创建 (cn) 和显示 (dn) 注释
假如我们实际需要做的只是重复用来创建新电子邮件和遍历现有电子邮件项的过程,那么随后两个静态方法是很简单的。在以下代码中,请注意由 OlItemType 和 OlDefaultFolders 枚举所指定值:
public static void CreateNote(ApplicationClass o) { // Get access to notes. NoteItem myNote = (NoteItem)o.CreateItem(OlItemType.olNoteItem); Console.Write("Enter your note: "); myNote.Body = Console.ReadLine(); // Now save the note. myNote.Save(); } public static void DisplayNotes(ApplicationClass o) { // Get items in my inbox. NameSpace outlookNS = o.GetNamespace("MAPI"); MAPIFolder notesFolder = outlookNS.GetDefaultFolder(OlDefaultFolders.olFolderNotes); // Print out some basic info. Console.WriteLine("You have {0} notes.", notesFolder.Items.Count); Console.WriteLine(); foreach(NoteItem item in notesFolder.Items) { Console.WriteLine("-> Time Created: {0}", item.CreationTime); Console.WriteLine("-> Body: {0}", item.Body); Console.WriteLine(); } }
最后接触
这里关心的最后的静态方法只是向最终用户显示一组选项:
public static void DisplayOPineOptions() { Console.WriteLine("***** Welcome To OPine *****"); Console.WriteLine("dib : Display Inbox"); Console.WriteLine("snm : Send New Mail"); Console.WriteLine("cn : Create Note"); Console.WriteLine("dn : Display Notes"); Console.WriteLine("q : Quit"); Console.WriteLine("****************************"); }
这将包装 OPine 帮助器类的创建过程;现在可以使用它。
实现 Main() 方法
到这里,我们准备实现 Main() 方法,该方法负责执行以下任务:
• |
创建 ApplicationClass 类型的实例 |
• |
通过 Console.ReadLine() 获得用户的命令选项 |
• |
接受用户提供的字符串,并执行合适的方法 OPineHelper |
给出这些要点后,下面是一个可能的实现:
static void Main(string[] args) { // String to hold the user request. string userOption; // Create an Outlook application object. ApplicationClass outLookApp = new ApplicationClass(); // Display info. OPineHelper.DisplayOPineOptions(); do { Console.Write("\nPlease enter your command: "); userOption = Console.ReadLine(); switch(userOption) { // Display Inbox (dib) case "dib": OPineHelper.DisplayInbox(outLookApp); break; // Create Note (cn) case "cn": OPineHelper.CreateNote(outLookApp); break; // Send New Mail (snm) case "snm": OPineHelper.SendNewMail(outLookApp); break; // Display Notes (dn) case "dn": OPineHelper.DisplayNotes(outLookApp); break; // Quit (q) case "q": userOption = "q"; break; default: // Anything else? Just display options. OPineHelper.DisplayOPineOptions(); break; } }while(userOption != "q"); }
处理 NewMailEx 事件
我们将添加到 OPine 中的最后一项功能是处理传入新电子邮件的能力。首先,在分配 ApplicationClass 类型之后处理 NewMailEx 事件:
// Create an Outlook application object. ApplicationClass outLookApp = new ApplicationClass(); // Rig up the new message event. outLookApp.NewMailEx += new ApplicationEvents_11_NewMailExEventHandler(outLookApp_NewMailEx);
ApplicationEvents_11_NewMailExEventHandler 委托的目标需要一个类型为 System.String 的参数。该字符串的值表示新的 MailItem 本身的 ID(可以通过 NameSpace.GetItemFromID() 方法来获得 MailItem)。
在后面的事件处理程序中,注意我们使用 System.Windows.Forms.MessageBox 类型来通知用户有新的邮件,所以一定要添加对 System.Windows.Forms.dll 的引用(并使用指令集更新您的文件):
private static void outLookApp_NewMailEx(string EntryIDCollection) { if(DialogResult.Yes == MessageBox.Show("Do you want to see your message?", "You‘ve got mail!", MessageBoxButtons.YesNo)) { // Get the incoming MailItem based on ID. ApplicationClass o = new ApplicationClass(); NameSpace outlookNS = o.GetNamespace("MAPI"); MAPIFolder mFolder = o.Session.GetDefaultFolder(OlDefaultFolders.olFolderInbox); MailItem newMail = (MailItem) outlookNS.GetItemFromID(EntryIDCollection, mFolder.StoreID); // Now print out. Console.WriteLine("\n\n***** New Message *****"); Console.WriteLine("-> Sender: {0}", newMail.SenderName); Console.WriteLine("-> Subject: {0}", newMail.Subject); Console.WriteLine("-> Body: {0}", newMail.Body); Console.WriteLine("***********************\n\n"); } }
这就是最后的步骤。现在我们可以执行编译,并对 OPine 进行测试(图 5)。
图 5. 运行中的 Opine
我敢肯定,您可以找到很多方式来扩展和改进 OPine,包括从基于控制台的 UI 移动到图形 UI(通过 Windows 窗体)。尽管我显然知道你们很少生成命令行电子邮件程序,但我希望该示例已经阐明了通过自定义应用程序与 Outlook 交互的过程。
与 Outlook 安全更新的冲突
运行 OPine 时,您肯定知道由 Outlook 启动的以下对话框(图 6)
图 6. Outlook 2003 安全警告
尽管这会干扰最终用户,但该行为是设计造成的。在 Outlook 2003 下面,(选择对象的)选择成员被认为是可能有安全风险的。因而,系统会提示用户输入权限才能继续操作,防止电子邮件蠕虫和病毒使用对象模型干坏事。
注 如果希望阅读关于哪些 Outlook 类型和成员导致该安全提示的文档,请参阅文章 What‘s New in Microsoft Office Outlook 2003 for Developers?
假如最终用户总是可以通过对安全提示作出"No"的响应来拒绝对 Outlook 的访问,您就能够在自定义应用程序中通过编程使用 Outlook 时自由地使用 try/catch 逻辑。例如,为了避免发生运行库故障,应当对 OPineHelper.DisplayNotes()(以及其余方法)进行如下更改:
public static void DisplayNotes(ApplicationClass o) { // Same as before... try { foreach(NoteItem item in notesFolder.Items) { // Same as before... } } catch{Console.WriteLine("Can‘t access Outlook");} }
注 值得注意的是,生成 Outlook 外接程序时,OnConnection() 方法的传入 Microsoft.Office.Interop.Outlook.Application 参数被假设为可信任的,在大多数 情况下,这将阻止安全警报的出现。
使用 Visual Studio .NET 2003 创建 Outlook 外接程序
下一个示例研究如何使用自定义功能来扩展 Outlook 2003。应用程序可扩展性的整个想法是 Microsoft 产品的另一个基石。基本上,诸如Outlook、Word 或 Visual Studio .NET 这样的应用程序都旨在能够让外部厂商或个人通过插入新的对象(假设上述对象实现了正确的接口)来扩展上述功能。
虽然大多数时候您肯定只使用 C# 命令行编译器 (csc.exe) 和 notepad.exe 来生成外接程序,但如果利用 Visual Studio .NET 共享外接程序项目模板,将节省一些键入时间。为了清楚地加以说明,我们将创建一个名为 EMailStatsAddIn 的外接程序。该外接程序将插到 Outlook 2003 的现有 UI 中,并提供以下功能:
• |
显示用户在该天/月接收了多少封邮件。 |
• |
显示用户在该天/月发送了多少封邮件。 |
首先从 New Project 对话框的 Other Projects | Extensibility Projects 文件夹中选择该模板(图 7)。
图 7. 共享的外接程序项目模板
一旦单击 OK 按钮,系统将指引您执行一个五步的向导,以便配置初始的共享外接程序项目。第一个步骤是选择要使用的语言(Microsoft_ Visual C#_ 或 Microsoft_ Visual Basic_ .NET C++)和基本框架(.NET 或基于 COM 的 ATL)。对于我们的示例,我们显然希望 Visual C#。
第二个步骤是选择外接程序的宿主。假定我们只对 Outlook 2003 感兴趣,请取消选中其他所有选项(图 8)。
图 8. 选择所需宿主
步骤三允许您提供当前外接程序的"友好"名称和说明。这些字符串值用于控制外接程序将被宿主外接程序对话框如何注册和显示。请随便输入您认为适合的值。
步骤 4 中显示的两个选项用于指定外接程序是否应当在启动时自动加载到宿主中,以及允许目标机器上的哪些用户访问外接程序。对于 EMailStatsAddIn,我们将同时选中这两个选项(图 9)。
图 9. 加载共享外接程序的选项。
步骤五只用来确认所选项。一旦完成共享外接程序向导,就会发现您已经得到一个包含两个项目的解决方案:
• |
外接程序本身 (EMailStatsAddIn) |
• |
安装项目 (EMailStatsAddInSetup) |
本文稍后讨论安装项目的角色。
引用的程序集
除了标准的 System.dll、System.Data.dll 和 System.XML.dll 程序集以外,共享外接程序还自动引用了一个名为 Extensibility.dll 的程序集。该程序集包含单个命名空间 (Extensibility),该命名空间确切定义了三个类型(参阅表 3)。
表 3。可扩展性命名空间的类型
可扩展性命名空间的类型 | 基本含义 |
IDTExtensibiltity2 |
所有外接程序都必须实现的关键接口。 |
ext_ConnectMode |
枚举,表示将宿主连接到给定外接程序的各种方式。 |
ext_DisconnectMode |
枚举,表示可以将给定的外接程序与宿主断开的各种方式。 |
需要注意的被引用的其他程序集是 Office.dll。虽然该程序集的确定义很多类型(可以通过 Visual Studio.NET 2003 对象浏览器来确认),但最重要的类型必须与用自定义小部件来扩展宿主的 GUI 以便与正在开发的外接程序进行交互有关。在这里,我不会对 Office 对象模型深入讨论太多,这方面的内容请参阅 MSDN 网站上的 online reference。
注意,共享外接程序项目模板不会自动引用 Microsoft.Office.Interop.Outlook.dll 程序集,所以一定要现在通过 Add References 对话框进行该引用。这样做时,请添加对 System.Windows.Forms.dll 的引用,在访问 MessageBox 类型时将需要它。
分析连接类类型
需要注意的主文件被命名为 Connect.cs。在这里将找到一个名为 Connect 的类类型,如下所示(为了简洁起见,删除了 XML 代码注释):
[GuidAttribute("762B03FF-52D0-4735-9D2B-4DE32DB9393E"), ProgId("EMailStatsAddIn.Connect")] public class Connect : Object, Extensibility.IDTExtensibility2 { public Connect(){} public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom) { applicationObject = application; addInInstance = addInInst; } public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref System.Array custom) {} public void OnAddInsUpdate(ref System.Array custom){} public void OnStartupComplete(ref System.Array custom){} public void OnBeginShutdown(ref System.Array custom){} private object applicationObject; private object addInInstance; }
需要注意的第一点是,该类具有 [Guid] 和 [ProgId] 属性,这些众所周知的属性值用来标识 COM 类。生成托管代码库时,您可能想知道为什么需要将程序集注册为由 COM 使用。请回忆,大多数能够访问外接程序类型的宿主都被编写为加载 COM 服务器(而不是 .NET 程序集)。因此,为了启用互操作,就要为 Connect 类型提供所需的 COM 命名约定(当然,GUID 值将是不同的)。
接下来要注意的是,Connect 类型没有扩展以外接程序为中心的基类,而只是从 System.Object 派生。使共享外接程序具有唯一性的原因是它实现了一个关键接口:Extensibility.IDTExtensibility2。实际上,Connect 类的成员只是接口合约的基本实现。
最后,请注意 Connect 类定义了两个私有成员变量:
private object applicationObject; private object addInInstance;
第二个成员变量 (addInInstance) 表示由宿主分配和插入的当前外接程序的实例。applicationObject 成员变量表示对宿主应用程序的引用。
假设共享外接程序可以插入到很多不同的应用程序(Outlook 2003、Visual Studio.NET 2003、各种 Microsoft Office 应用程序等等)中,那么将 applicationObject 定义为一般 System.Object。但是,假如我们的外接程序只是为了由 Outlook 使用,则将 applicationObject 定义为 Microsoft.Office.Interop.Outlook.Application 类型:
// Don‘t forget to reference the Outlook interop assembly! using Microsoft.Office.Interop.Outlook; ... public class Connect : Object, Extensibility.IDTExtensibility2 { ... // Change to a strongly typed Outlook Application type. // private object applicationObject; private Microsoft.Office.Interop.Outlook.Application applicationObject; private object addInInstance; }
使用该初始代码更新,让我们检查 IDTExtensibility2 接口提供的功能。
IDTExtensibility2 接口的角色
IDTExtensibility2 接口类型定义了在外接程序生命周期的各个阶段宿主应用程序所调用的五个方法。下面是正式定义:
public interface IDTExtensibility2 { void OnAddInsUpdate(ref Array custom); void OnBeginShutdown(ref Array custom); void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom); void OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom); void OnStartupComplete(ref Array custom); }
OnConnection() 方法
第一个需要注意的方法是 OnConnection(),外接程序连接到宿主时将调用该方法。第一个参数表示宿主应用程序本身(Visual Studio.NET、Microsoft Outlook 等)。第二个参数是类型 ext_ConnectMode 的枚举,它表示宿主具体怎样 加载外接程序类型。ext_ConnectMode 枚举定义以下值(其中,ext_cm_CommandLine 和 ext_cm_Solution 不会被 Outlook 2003 使用):
public enum ext_ConnectMode { // Add-in loaded after host. ext_cm_AfterStartup = 0; // Add-in loaded from command line. ext_cm_CommandLine = 3; // Add-in loaded indirectly from host. ext_cm_External = 2; // Add-in loaded by a VS.NET solution which required it. ext_cm_Solution = 4; // Loaded when host starts-up. ext_cm_Startup = 1; // Loaded for very first time after installed. ext_cm_UISetup = 5; }
第三个参数是一个对象,该对象表示由 IDE 宿主的外接程序实例,其中最后一个参数类型为 System.Array,该参数表示用户提供的任何自定义数据(在我们的示例中可以被忽略)。
生成的代码很简单。但是,假如已经定义了强类型的 applicationObject 成员变量,则现在需要使用显式转换来分配该变量:
public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom) { applicationObject = (Microsoft.Office.Interop.Outlook.Application)application; addInInstance = addInInst; }
我们需要对 OnConnection() 方法再有一个初始更新。按照 Knowledge Base article,OnConnection() 的整洁且正确的实现应当执行运行库测试来确定连接模式是否不同于 ext_ConnectMode.ext_cm_Startup,如果是这样,则将传入的 System.Array 转发给 OnStartupComplete() 的外接程序实现:
public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom) { // Cast to a strongly typed Application. ... // If we are not loaded upon startup, forward to OnStartupComplete() // and pass the incoming System.Array. if(connectMode != ext_ConnectMode.ext_cm_Startup) { OnStartupComplete(ref custom); } }
OnDisconnection() 方法
当外接程序与宿主断开时(通常通过添加/删除外接程序对话框或在宿主关闭时)将调用该方法。断开模式由类型 ext_DisconnectMode 的第一个参数表示:
public enum ext_DisconnectMode { ext_dm_HostShutdown = 0; ext_dm_SolutionClosed = 3; ext_dm_UISetupComplete = 2; ext_dm_UserClosed = 1; }
向导生成的 OnDisconnection() 实现当前为空。很明显,这是外接程序执行任何清理操作的地方,为了成功关闭,外接程序可能需要执行该操作。再次,按照 Knowledge Base article,该方法的整洁且正确的实现应当对连接模式进行测试(这次针对除了 ext_DisconnectMode.ext_dm_HostShutdown 以外的任何内容),并将传入的 System.Array 转发给我们的 OnBeginShutdown() 的实现:
public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref System.Array custom) { if(disconnectMode != ext_DisconnectMode.ext_dm_HostShutdown) { OnBeginShutdown(ref custom); } applicationObject = null; }
OnAddInsUpdate() 方法
如果最终用户在宿主中插入或删除外接程序,将调用 OnAddInsUpdate() 方法(可以用 Application.COMAddins 属性在运行时获得当前列表)。如果需要在最终用户添加或删除新外接程序时执行任何特殊的过程,这里将是执行该操作的地方。自动生成的实现当前为空,并且可以保留该状态。
OnStartupComplete() 方法
宿主应用程序完成加载之后,将调用该方法。这时,所有宿主资源均可供外接程序使用。这是为外接程序类型构造 UI 的理想地方,因为可以安全获得 Explorers 和 Inspectors 集。
OnBeginShutdown() 方法
最后介绍 OnBeginShutdown(),它表示宿主正处于关闭过程中(正好在调用 OnDisconnection() 方法之前)。这时,仍然可以访问宿主应用程序,所以,这是删除插入到活动资源管理器中的任何 UI 小部件的理想地方。
实现外接程序的 UI
我们的第一份业务定单是为我们的外接程序的功能生成用户界面。假如外接程序的 UI 要插入宿主应用程序中,我们将为 Connect 类类型定义一个类型为 Microsoft.Office.Core.CommandBarButton 的新成员变量:
public class Connect : Object, Extensibility.IDTExtensibility2 { ... // Our UI will consist of a single CommandBarButton private CommandBarButton btnGetEMailStats; }
CommandBarButton 小部件(用于显示标题"Statistics")将插入到 Outlook 的标准命令栏中(图 10):
图 10. 自定义的 Statistics CommandBarButton
前面已经提到,假如宿主完全启动,则 OnStartupComplete() 方法是生成 UI 元素的理想位置。下面是将新的 CommandBarButton 类型插入到现有命令栏时所需的步骤:
• |
从活动的资源管理器获得命令栏集。 |
• |
查看按钮当前是否在希望修改的命令栏的控件集合中。如果不在,则创建并启用新的实例。 |
• |
在 CommandBarButton 中挂钩 Click 事件,以响应小部件的自定义功能。 |
这样,下面是经过更新的(并有详细批注) OnStartupComplete() 的实现:
public void OnStartupComplete(ref System.Array custom) { // First, get access to the CommandBars on // the active explorer. CommandBars commandBars = applicationObject.ActiveExplorer().CommandBars; try { // If our button is already // on the Standard CommandBar, use it. btnGetEMailStats = (CommandBarButton) commandBars["Standard"].Controls["Statistics"]; } catch { // OOPS! Our button is not there, so // we need to make a new instance. // Note that the Add() method was // defined to take optional parameters, // which are not supported in C#. // Thus we must specify Missing.Value. btnGetEMailStats = (CommandBarButton) commandBars["Standard"].Controls.Add(1, System.Reflection.Missing.Value, System.Reflection.Missing.Value, System.Reflection.Missing.Value, System.Reflection.Missing.Value); btnGetEMailStats.Caption = "Statistics"; btnGetEMailStats.Style = MsoButtonStyle.msoButtonCaption; } // Setting the Tag property is not required, but can be used // to quickly reterive your button. btnGetEMailStats.Tag = "Statistics"; // Setting OnAction is also optional, however if you specify // the ProgID of the Add-in, the host will automatically // load the Add-in if the user clicks on the CommandBarButton when // the Add-in is not loaded. After this point, the Click // event handler is called. btnGetEMailStats.OnAction = "!"; btnGetEMailStats.Visible = true; // Rig-up the Click event for the new CommandBarButton type. btnGetEMailStats.Click += new _CommandBarButtonEvents_ClickEventHandler( btnGetEMailStats_Click); }
注意,CommandBarButton 已被配置为在被单击时调用名为 btnGetEMailStats_Click() 的方法。我们可以很快实现自定义逻辑,但目前只是下面的 stub 代码:
private void btnGetEMailStats_Click(CommandBarButton Ctrl, ref bool CancelDefault) { // ToDo: Implement custom logic. }
删除 UI
现在,我们完成了一旦宿主启动就创建 CommandBarButton 的逻辑,然后我们将修改 OnBeginShutdown() 以便在关闭时删除这个小部件:
public void OnBeginShutdown(ref System.Array custom) { // Get set of command bars on active explorer. CommandBars commandBars = applicationObject.ActiveExplorer().CommandBars; try { // Find our button and kill it. commandBars["Standard"].Controls["GetEMailStats"].Delete( System.Reflection.Missing.Value); } catch(System.Exception ex) {MessageBox.Show(ex.Message);} }
注 您可能猜到,可以用很多方式扩展 Outlook 2003 的 GUI。要了解其他技术,请参阅 Creating Managed Outlook Buttons with Icons。
实现电子邮件统计逻辑
最后的任务是在 Click 事件处理程序中实现逻辑。我们将利用 Outlook 对象模型来确定每天和每月所接收和发送的电子邮件数。在生成 OPine 以后,以下逻辑应当相当简单:
private void btnGetEMailStats_Click(CommandBarButton Ctrl, ref bool CancelDefault) { string statInfo; DateTime today = DateTime.Today; // The stats we are tracing. int eMailsToday = 0; int eMailsThisMonth = 0; int eMailSentToday = 0; int eMailSentThisMonth = 0; // Get items in user‘s inbox. NameSpace outlookNS = applicationObject.GetNamespace("MAPI"); MAPIFolder inboxFolder = outlookNS.GetDefaultFolder(OlDefaultFolders.olFolderInbox); // Compare time received to current day / month // and update our counters. foreach(object item in inboxFolder.Items) { MailItem mi = item as MailItem; if(mi != null) { if(mi.ReceivedTime.Day == today.Day) eMailsToday++; if(mi.ReceivedTime.Month == today.Month) eMailsThisMonth++; } } // Build first part of statInfo string. statInfo = string.Format("E-mails received today: {0}\n", eMailsToday); statInfo += string.Format("E-mails received this Month: {0}\n", eMailsThisMonth); statInfo += "--------------------------------------\n"; // Get items in user‘s sent item folder and // test again. MAPIFolder SentFolder = outlookNS.GetDefaultFolder(OlDefaultFolders.olFolderSentMail); foreach(object item in SentFolder.Items) { // See if current item is a MailItem MailItem mi = item as MailItem; if(mi != null) { // It is, so get day/month stats. if(mi.SentOn.Day == today.Day) eMailSentToday++; if(mi.SentOn.Month == today.Month) eMailSentThisMonth++; } } // Build last part of statInfo string. statInfo += string.Format("E-mails sent today: {0}\n", eMailSentToday); statInfo += string.Format("E-mails sent this Month: {0}\n", eMailSentThisMonth); // Show results. MessageBox.Show(statInfo, "Current E-mail stats"); }
到这里,我们的外接程序就完成了!假定您能够编译项目而没有错误,那么现在就可以注册和测试 EMailStatsAddIn 功能了。
注册共享外接程序
请回忆,在用 Visual Studio.NET 2003 创建共享外接程序项目时,您收到过一个安装项目。要生成可以使用的 EMailStatsAddIn,请在解决方案资源管理器中右键单击 EMailStatsAddInSetup 项目图标,并选择"Rebuild"(图 11)。
图 11. 生成安装程序
在此以后,项目目录将包含标准的安装可执行文件和 *msi 安装程序文件。可以使用这些文件安装外接程序,也可以直接在 Visual Studio.NET 2003 中进行安装(图 12)。
图 12. 通过 VisualStudio.NET 2003 安装 EMailStatsAddIn
现在,当启动 Outlook 2003 时,应当在标准命令栏中找到 Statistics 按钮。毫无疑问,单击它时,将看到今天的电子邮件统计信息(图 13)。
图 13. 运行中的 EMailStatsAddIn
注 值得说明的是,为了满足您的需要,可能需要对 Visual Studio.NET 2003 生成的安装逻辑加以修改。您的 .NET 程序集可能无法将自己正确注册为可被 COM 使用。如果出现这样的情况,托管的外接程序就无法出现在 Outlook COM 外接程序对话框中,如果没有将外接程序配置为在启动时启动,这会是很大的问题。Microsoft 的 Omar Shahine 已在 http://go.microsoft.com/fwlink/?LinkId=30833 张贴了这个问题的解决方案。另一个关于该问题的观点可以在 http://blogs.msdn.com/robmen/archive/2004/04/28/122491.aspx 找到。
小结
我们已经看到,使用主互操作程序集,.NET 开发人员可以与 Outlook 2003 类型交互。Application 类型是该模型的根,而该模型公开了代表各种 Outlook 项的很多集合。在开发命令行驱动的邮件应用程序 (OPine) 期间,您已经了解如何通过编程创建并获得各个项,以及如何响应选择事件。我们已经阐明,通过创建自定义外接程序,可以对 Outlook 2003 本身进行扩展。Visual Studio.NET 2003 为此提供了具体的项目模板,该模板可以产生实现 IDTExtensibility2 接口的类,以及相关的安装程序项目。
很明显,在这篇介绍性的文章已经考查的内容以外,外接程序和 Outlook 2003 对象模型还有更多可介绍的内容。我的目标是从 C# 程序员的视角提供对 Outlook 2003 对象模型的简介,我希望我的介绍已经为程序员进一步探索打下了基础。如果对其他信息感兴趣,请参阅以下参考资料:
关于作者
Andrew Troelsen 是 Intertech Training 的顾问和培训师。Andrew 是很多书籍的作者,包括获奖的 C# and the .NET Platform Second Edition(Apress 出版社,2002 年)。他也为 MacTech 的每月专栏的所有版面写文章,在该专栏中,他探讨如何使用 SSCLI、Portible.NET 和 Mono CLI 分发软件在基于 Unix 的系统上开发 .NET。