Programming MicrosoftВ® OutlookВ® and Microsoft Exchange 2003, Third Edition (Pro-Developer)
Note | Before reading this section, you might want to read the section titled "Building Event Handlers in Visual Studio .NET" in Chapter 17, which covers overall COM interoperability issues with .NET. |
Building a .NET application that requires you to interoperate with COM interfaces is similar to the event handler samples you saw earlier. The main differences between COM interoperability projects is that Visual Studio .NET sometimes does not import type libraries for you correctly in its automatic import capabilities. You might receive some strange and unexpected errors.
One of the best examples of Visual Studio .NET not importing a type library as it should is when you build a .NET COM add-in for Outlook. If you follow the standard steps shown earlier for event handler code and all you do is modify the code to work with the IDTExtensibility2 interfaces and Outlook interfaces, you will run into problems. The problem is not with the IDTExtensibility2 interfaces but with the way Visual Studio .NET imports Outlook interfaces. If you attempt to use any sort of events with the Outlook object model ”for example, trying to capture Explorer or Inspector events ”you will get a "No Interface" error. This error occurs because the type library was not imported correctly by Visual Studio .NET. Let's look in more detail at the process of importing a COM type library into .NET to uncover why these errors occur and how to fix them.
When you add a reference to a COM component in .NET, Visual Studio .NET imports the COM component as a type library and creates a .NET safe wrapper around that component's methods . This wrapper is usually an EXE or DLL assembly. For example, when you import the Outlook object library, Visual Studio .NET creates a file named Interop.Outlook.dll. To understand the information this file contains, you can use the IL disassembly tool (Ildasm.exe, which is part of the .NET Framework SDK) to view the file in a human-readable format.
To load the Outlook DLL into Ildasm, type Ildasm.exe path \Interop.Outlook.dll . You should see the interface shown in Figure 7-7. Ildasm shows not only the Microsoft Intermediate Language (MSIL) code but also namespaces, types, and interfaces.
Now let's find the Outlook Application events. If you look in the Ildasm tool, you will see the ApplicationEvents_10 and ApplicationEvents interfaces if you're using Outlook 2002; in Outlook 2003, you should see ApplicationEvents_11 . In those interfaces, you will see the standard Outlook Application events such as NewMail and OptionsPagesAdd . If you find the ApplicationEvents_SinkHelper , you will notice that this class is declared private in the first line, which states .class private auto ansi sealed . You must change all the SinkHelper classes to public in the IL file for Outlook events to work. The Ildasm tool does not allow you to modify the file, so you must use a text editor to do this.
After you modify the file, you must recompile the IL file. You do this using Ilasm.exe. You should pass the name of your IL file and the output file name and indicate that you want to compile the file as a DLL. The following is an example of compiling an Outlook IL file:
Ilasm.exe /dll /output=interop.outlook.dll /dll interop.outlook.il
Once the assembly is created, by running the command line just shown, you can use events successfully in the Outlook object model. If you continue to receive "No Interface Supported" errors, be sure to check your IL file again and recompile it. Also, you might want to set the Copy Local parameter for the interop DLL to False .
Note | To make it easier for you to get started using Outlook with Visual Studio .NET, I've included a sample that shows the modified Outlook interop assembly working within a program. This sample is in the OutlookEvents folder. You can take the Interop.Outlook.dll file from the bin directory of that sample and use it in your Outlook applications. Also, you can look at the Outlook.il file to see the correctly modified IL file that makes Outlook events work. Note that this is the Outlook 2002 IL ”Outlook 2003 is in beta as of this writing. Be sure to use the Primary Interop Assembly (PIA) for Outlook 2003, which contains an already fixed Outlook interop DLL. This PIA ships with Outlook. |
Visual Basic programmers can use the standard WithEvents keyword to implement Outlook events. Here's a quick example:
Public WithEvents oExplorer As Outlook.Explorer Shared Sub oExplorer_SelectionChange() Handles oExplorer.SelectionChange MsgBox("Selection Changed") End Sub
C# developers use different semantics to hook up events. Instead of With Events , you use the += operator to add an event handler for Outlook events. The following C# example shows how to add an event handler for Outlook events. The code assumes that the variable application contains a valid reference to an Outlook Application object.
outlookApp = (Outlook.Application) application; outlookApp.Inspectors.NewInspector += new Outlook.Inspectors Events_NewInspectorEventHandler (this.NewInspectorCallback); public void NewInspectorCallback (Outlook.Inspector oInspector) { try { //Your code goes here } catch (System.Exception e) { //Error handling here } }
Tracing Errors
One important skill you need for writing .NET code ”and especially COM add-ins ”is the ability to trace errors through the stack trace in .NET. Both Visual Basic .NET and C# include the System.Exception class, which lets you trace error messages thrown by the .NET Framework at run time as well as through the stack trace. The following examples show both the error message and the stack trace in Visual Basic .NET and C# so you can track down where errors occur in your code.
'VB Example On Error Resume Next If Err.Number <> 0 Then Dim e As System.Exception = _ Microsoft.VisualBasic.Information.Err.GetException() MsgBox(e.Message) MsgBox(e.StackTrace) End if //C# example private debug_utils.logFileWriter _writer; // // Setup the log file writer // _writer = new debug_utils.logFileWriter (); _writer.createNewLog = true; _writer.logfileName = "c:\error.log"; _writer.writeLine ("Starting up the addin"); try { //Code goes here } catch (System.Exception e) { __writer.writeLine ("Code failed: " + e.Message + "stack trace: " + e.StackTrace); }
Using the Extensibility Project for Your Add-In
The easiest way to create a COM add-in is to use the Visual Studio Shared Add-in Wizard. You can find this wizard under the Extensibility folder when you create a new project. Figure 7-8 shows the Shared Add-in Wizard template in the New Project dialog box in Visual Studio .NET. Figure 7-9 shows the user interface for the Visual Studio Add-in Wizard.
The wizard steps you through the process of creating an add-in by asking what language you want to use, the application hosts you want to target (for example, Outlook, Microsoft Word, or Visual Studio .NET), the friendly name and description of your add-in, and when you want to load the add-in. When you finish the wizard, it creates a project for you, fills out a stub of the add-in to implement the basic IDTExtensibility2 interfaces, and adds a setup program that will automatically install and configure your application on the target machine as a COM add-in. The project that's created by default is shown in Figure 7-10. In the default project, you add your code to perform the functionality of your COM add-in.
An Add-In Example
To show you a simple example of an add-in, I created one that sends an e-mail message when Outlook starts up. You should note a couple of things in the code. First, in the oConnection subroutine, you must type cast the variable oMailItem , which is an OutlookMailItem object, to Outlook._MailItem before you can successfully call the Send method. You must do this because the word Send is ambiguous ”it might relate to the OutlookMailItem object or the Send event on the Outlook Items collection. You must explicitly tell .NET that you want to call the Send method and are not implementing the Send event. Also notice the explicit garbage collection in the OnDisconnection event (which I'll discuss in more detail in the next section).
Imports System.Runtime.InteropServices Imports Extensibility Imports Outlook <ProgId("DotNetAddin.OutComAddin"), _ Guid("BCD79E56-29A5-42a2-8906-35C62FCFEC0F"), ComVisible(True)> _ Public Class DotNetCOMAddin Implements IDTExtensibility2 Public oApplication As Outlook.Application Public oExplorer As Outlook.Explorer Public Sub OnBeginShutdown(ByRef custom As System.Array) _ Implements Extensibility.IDTExtensibility2.OnBeginShutdown On Error Resume Next 'Make sure to kill our references to our public variables 'so Outlook can shut down Marshal.ReleaseComObject(oExplorer) Marshal.ReleaseComObject(oApplication) GC.WaitForPendingFinalizers() GC.Collect() CType(oApplication, Outlook._Application).Quit() End Sub Public Sub OnConnection(ByVal Application As Object, _ ByVal ConnectMode As Extensibility.ext_ConnectMode, _ ByVal AddInInst As Object, _ ByRef custom As System.Array) _ Implements Extensibility.IDTExtensibility2.OnConnection On Error Resume Next oApplication = CType(Application, Outlook.Application) 'Send a new message telling the user we just connected Dim oMailItem As Outlook.MailItem = _ oApplication.CreateItem(Outlook.OlItemType.olMailItem) oMailItem.To = oApplication.Session.CurrentUser.Address oMailItem.Subject = "OnConnection Called for .NET COM Add-in" oExplorer = oApplication.ActiveExplorer Dim oFolder As MAPIFolder = oExplorer.CurrentFolder Dim strOtherText As String = "Current Explorer: " & _ oExplorer.Caption & " Number of items in " & oFolder.Name & _ " is " & oFolder.Items.Count oMailItem.Body = "Outlook Version: " & oApplication.Version & _ " " & "Current User: " & _ oApplication.Session.CurrentUser.Name & strOtherText CType(oMailItem, Outlook._MailItem).Send() End Sub Public Sub OnDisconnection( _ ByVal RemoveMode As Extensibility.ext_DisconnectMode, _ ByRef custom As System.Array) _ Implements Extensibility.IDTExtensibility2.OnDisconnection End Sub Public Sub OnStartupComplete(ByRef custom As System.Array) _ Implements Extensibility.IDTExtensibility2.OnStartupComplete On Error Resume Next 'Send a new message telling the user we just connected Dim oMailItem As Outlook.MailItem = _ oApplication.CreateItem(Outlook.OlItemType.olMailItem) oMailItem.To = oApplication.Session.CurrentUser.Address oMailItem.Subject = "OnStartup Complete Called for .NET COM Add-in" oMailItem.Body = "Outlook Version: " & oApplication.Version & _ " " & "Current User: " & oApplication.Session.CurrentUser.Name oMailItem.Save() CType(oMailItem, Outlook._MailItem).Send() End Sub Public Sub OnAddInsUpdate(ByRef custom As System.Array) _ Implements Extensibility.IDTExtensibility2.OnAddInsUpdate End Sub End Class
PropertyPage Extensions and .NET
PropertyPage extensions, which you will learn about in the next chapter, allow you to add custom pages to the property pages for folders or for the Options dialog box in Outlook. You can successfully create a PropertyPage extension using a .NET control that has COM interoperability enabled. However, you cannot get the parent of the control or use any of the advanced functionality of the PropertyPage extension because the .NET control does not exhibit the same interfaces as a true COM object. There is no real workaround for this problem, so if you want to use a .NET control in your PropertyPage, you should embed that control in a true COM control written in Visual Studio 6.0. However, if you are going to write the wrapper in Visual Studio 6.0, you might as well write the entire PropertyPage extension in that environment as well.
You can have a COM add-in written in .NET that calls a PropertyPage extension written in pure COM. That sort of mix and match works fine in Outlook.
Forcing Collection of Your Variables and Objects
One operation that can be difficult to grasp is the garbage collection environment that .NET introduces. If you've programmed with Visual Basic 6.0, you are used to Visual Basic controlling the lifetime of your variables and, when the variables go out of scope or are set to Nothing , having the variables immediately disappear.
This behavior does not occur in .NET. The .NET Framework destroys your references only on the next garbage collection cycle. For COM add-ins, this means that Outlook remains in memory until .NET garbage collection occurs, but this is not what you want.
To get around this situation, you can explicitly force garbage collection in your programs. As you can see in the sample add-in code presented earlier, you can explicitly control the lifetime of a COM object from your .NET application by using Marshal.ReleaseCOMObject and passing the COM object you want to release. This method decrements the reference count in the runtime callable wrapper (COM wrapper) for the COM object in .NET.
Next you call GC.WaitForPendingFinalizers . This method waits until all released objects have finished whatever functions they need to perform so they can be destroyed . Finally, you call GC.Collect , which forces garbage collection in .NET. This call allows .NET to reclaim the memory discarded by the released COM objects and allows Outlook to shut down cleanly. It's also good practice to call the Quit method on the Application object after forcing garbage collection to be sure that Outlook truly exits.
Working with Outlook Item Types and Common Properties
One gotcha you will find when you're working with the Outlook object model and .NET is that the Outlook Items collection allows heterogeneous item types in the collection. For this reason, you have to scroll through and check the type for each item. You must then declare the item as the correct type in .NET, such as MailItem or PostItem . To implement this type checking, you must produce a set of If...Then statements in your code.
Debugging Your .NET COM Add-In
Debugging in .NET is a bit different than in Visual Studio 6.0. In your .NET add-in, you set the debugging in your project so that Outlook will launch as an external application and then set your breakpoints in your code. Then you just run your program. Outlook will launch, and your breakpoint will be hit if your add-in loads correctly. You can add Outlook.exe as the external program to launch for your add-in under the Project Properties dialog box in Visual Studio .NET.
Note | Microsoft has released PIAs for Office 2002 and Office 2003 that fix a number of the problems described in this chapter. However, if you do not have these PIAs or you are running Outlook 2000, you can use the techniques described here to fix the Office interop assemblies. You should download the official PIAs for Office from http://msdn.microsoft.com/office . You must then reference these PIAs in your applications or place them in the Global Assembly Cache (GAC) for global use. You must also distribute the PIAs as part of your setup for client machines that will run your application. With Office 2003, the PIAs are part of the core Office setup. |