TECHNICAL ARTICLES


Using Micro Eye AddInMon in an Outlook COM Add-in

Introduction
Registry Settings
Sample Code
Setup and Deployment
Download

Introduction

AddInMon resolves a basic problem in Outlook COM Add-in development. Any Outlook COM Add-in must be able to detect when Outlook is shutting down so that the Add-in can release its references and not cause Outlook to remain in memory. The OnDisconnection event is not useful for this purpose since it may not fire if an Add-in has instantiated any Outlook objects. The accepted workaround has been to trap the Explorer and Inspector Close events and evaluate Explorers.Count and Inspectors.Count  to detect when Outlook is closing. This approach requires that an Add-in does not load during the OnConnection event if Explorers.Count = 0. An Outlook Explorer is an object model representation of Outlook's main window. If an Add-in loaded when Explorers.Count = 0, it would not be possible to use the Explorer Close event to determine when Outlook shuts down. For a brief explanation of the problem, see the following Microsoft Knowledge Base articles:

If the Add-in did initialize its objects when there was no Outlook UI, there would be no Explorer Close or Inspector Close events that would alert the Add-in to an Outlook shutdown so it could properly clean up its own objects

OL2000: You Cannot Fully Quit Outlook When You Use a COM Add-in
OL2002: You Cannot Fully Quit Outlook When You Use a COM Add-in

 Microsoft ActiveSync® 3.5 or above and similar programs break this workaround. ActiveSync synchronizes Outlook items with a Windows Mobile device. Version 3.5 and above typically instantiate an Outlook Application object with no user interface (UI) when Windows starts and keep it running until Windows shuts down. Only a subset of Outlook's working set loads into memory at this point, but the OnConnection event fires for COM Add-ins. If a user or other application later launches Outlook with full UI OnConnection does not fire again.

If ActiveSync loads during startup and launches Outlook with no UI then a COM Add-in that uses Explorers.Count to determine when to load will detect Outlook without UI and not initialize its own objects. When the user subsequently launches Outlook and opens an Explorer there will not be any new Connection event to cause the Add-in to reinitialize. As a result the Add-in will appear to fail when ActiveSync loads before Outlook.

This Microsoft Knowledge Base article contains additional information:

OL2002: Add-ins Are Disabled If a Process First Starts Outlook

Note: The KB article above (Q329098) describes a fix related to this issue that first appeared in Outlook 2002 SP-3. We have tested this with our own Add-ins and haven't found any difference in load behavior when Outlook is launched without UI. If your design is different you may have different results.

AddInMon is an executable that provides a solution to this issue. AddInMon.exe monitors the Windows messages associated with Outlook's thread to detect when UI is instantiated. If that occurs, the EXE will disconnect and then connect any Add-ins set to load on startup that have an AddInMon value in the Windows registry. Disconnecting and then connecting the Add-in causes the Add-in to properly initialize its objects.

Micro Eye developed AddInMon for our own Add-ins. We subsequently decided to make it available to the Outlook development community to handle this common problem. You are free to use the executable and the code below as you see fit with the understanding that it is not supported or warranted in any way. Micro Eye, Inc. assumes no responsibility for this software in any commercial or non-commercial product or use.

Registry Settings

Your installation program or code in the Add-in's OnConnection event must add a registry value named AddInMon. This value can be added under HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE depending upon where your Add-in is registered. The DWORD AddInMon is located under

HKEY_CURRENT_USER\Software\Microsoft\Office\Outlook\Addins\ProgID

or

HKEY_LOCAL_MACHINE\Software\Microsoft\Office\Outlook\Addins\ProgID

where ProgID represents the Programmatic Identifier of the COM Add-in. The actual value of AddInMon is inconsequential - any numeric value will work. Figure 1 shows AddInMon under the key for an Add-In with the ProgID TestTrustAddin.Connect.

AddInMon key in the Windows Registry

Figure 1. AddInMon key in the Windows Registry

Note: If you add a WriteDiagnostics DWORD value under the ProgID of your Add-in, you will cause AddInMon to write a diagnostics file named AddInMon.log to the Temp folder. This log file can be used to debug the operation of AddInMon with your Add-in. You should only enable the WriteDiagnostics DWORD when you are testing your Add-in with AddInMon. Like the AddInMon value shown above, the actual value of WriteDiagnostics is inconsequential. Sample content for AddInMon.log is shown in Figure 2.

AddInMon Log

Figure 2. AddInMon Log enabled with WriteDiagnostics

Sample Code

The following sample code shows how to implement AddInMon.exe in your solution. Your Setup program or your Add-in itself must write the AddInMon key to the correct location in the Registry. The OnConnection sample code requires two additional declarations for GetCurrentProcessID and GetCurrentThreadID and a conditional evaluation in the OnConnection event. An additional code module named modProcess contains the IsAddInMonProcess procedure to determine whether AddInMon is already loaded in memory. IsAddInMonProcess is called on the OnConnection code that launches AddInMon. You must add modProcess to your solution. If AddInMon.exe is not running and Outlook has started without UI, then AddInMon.exe is launched with a Shell statement. AddInMon requires three arguments: ProgID of your Add-in, ProcessID of Outlook, and ThreadID of Outlook. Since your Add-in runs in the Outlook process, Calling GetCurrentProcessID and GetCurrentThreadID returns values that pertain to Outlook. Provided that your installation program has written the AddInMon key to the correct location in the Windows Registry, AddInMon.exe will disconnect your Add-in immediately and then reconnect it if Outlook subsequently launches with UI. AddInMon.exe unloads itself from memory whenever the Outlook process unloads. If more than one Add-in has been registered with the AddInMon key, the first one to load will launch AddInMon.exe and then be disconnected. The next Add-in to load will also Shell AddInMon.exe, but the executable will shut itself down immediately if it detects an existing running instance of itself.

'Declarations

Private Declare Function GetCurrentProcessId Lib "Kernel32.dll" () As Long
Private Declare Function GetCurrentThreadId Lib "kernel32" () As Long

Private Sub AddinInstance_OnConnection(ByVal Application As Object, _
    ByVal ConnectMode As AddInDesignerObjects.ext_ConnectMode, _
    ByVal AddInInst As Object, custom() As Variant)
    Dim lngPID As Long
    Dim lngThreadID As Long
  
    On Error Resume Next

    If Application.Explorers.Count > 0 Then 'Outlook launched with UI
        'Create and Initialize a base class or write your Initialization code here
        gBaseClass.InitHandler Application, AddInInst.ProgId
    Else 'Outlook launched without UI
        'Get Outlook ProcessID (same as current process ID since Add-In runs in-process
        lngPID = GetCurrentProcessId
        lngThreadID = GetCurrentThreadId
        If IsAddInMonProcess = False Then
            'Launch AddInMonitor, supplying Add-In ProgID, Outlook PID, ThreadID
            Shell App.Path & "\AddInMon.exe /n " & AddInInst.ProgId _
            & " /p " & lngPID & " /t " & lngThreadID
        End If
    End If
End Sub

This modification to your OnConnection code is required for correct operation of AddInMon. Be aware that for correct Outlook shutdown you must write code in the Close event of event-aware Inspector and Explorer objects. For a complete example, see the Outlook COM Add-in Template.

The code for modProcess is shown below and is also provided for you convenience in the download for AddInMon. modProcess contains the IsAddInMonProcess procedure that determines whether AddInMon has already been loaded into memory. IsAddInMonProcess is called from the OnConnection procedure shown above.

'Declarations

Option Explicit

Private Declare Function Process32First Lib "kernel32" ( _
ByVal hSnapshot As Long, lppe As PROCESSENTRY32) As Long

Private Declare Function Process32Next Lib "kernel32" ( _
ByVal hSnapshot As Long, lppe As PROCESSENTRY32) As Long

Private Declare Function CloseHandle Lib "Kernel32.dll" _
(ByVal Handle As Long) As Long

Private Declare Function OpenProcess Lib "Kernel32.dll" _
(ByVal dwDesiredAccessas As Long, ByVal bInheritHandle As Long, _
ByVal dwProcId As Long) As Long

Private Declare Function EnumProcesses Lib "psapi.dll" _
(ByRef lpidProcess As Long, ByVal cb As Long, _
ByRef cbNeeded As Long) As Long

Private Declare Function GetModuleFileNameExA Lib "psapi.dll" _
(ByVal hProcess As Long, ByVal hModule As Long, _
ByVal ModuleName As String, ByVal nSize As Long) As Long

Private Declare Function EnumProcessModules Lib "psapi.dll" _
(ByVal hProcess As Long, ByRef lphModule As Long, _
ByVal cb As Long, ByRef cbNeeded As Long) As Long

Private Declare Function CreateToolhelp32Snapshot Lib "kernel32" ( _
ByVal dwFlags As Long, ByVal th32ProcessID As Long) As Long

Private Declare Function GetVersionExA Lib "kernel32" _
(lpVersionInformation As OSVERSIONINFO) As Integer

Public Type PROCESSENTRY32
dwSize As Long
cntUsage As Long
th32ProcessID As Long ' This process
th32DefaultHeapID As Long
th32ModuleID As Long ' Associated exe
cntThreads As Long
th32ParentProcessID As Long ' This process's parent process
pcPriClassBase As Long ' Base priority of process threads
dwFlags As Long
szExeFile As String * 260 ' MAX_PATH
End Type

Public Type OSVERSIONINFO
dwOSVersionInfoSize As Long
dwMajorVersion As Long
dwMinorVersion As Long
dwBuildNumber As Long
dwPlatformId As Long '1 = Windows 98/ME
                     '2 = Windows NT/2000/XP
szCSDVersion As String * 128
End Type

Public Const PROCESS_QUERY_INFORMATION = 1024
Public Const PROCESS_VM_READ = 16
Public Const MAX_PATH = 260
Public Const STANDARD_RIGHTS_REQUIRED = &HF0000
Public Const SYNCHRONIZE = &H100000
'STANDARD_RIGHTS_REQUIRED Or SYNCHRONIZE Or &HFFF
Public Const PROCESS_ALL_ACCESS = &H1F0FFF
Public Const TH32CS_SNAPPROCESS = &H2&
Public Const hNull = 0
Public m_lngPID As Long

Public Function getVersion() As Long
    On Error Resume Next
    Dim osinfo As OSVERSIONINFO
    Dim retvalue As Integer
    osinfo.dwOSVersionInfoSize = 148
    osinfo.szCSDVersion = Space$(128)
    retvalue = GetVersionExA(osinfo)
    getVersion = osinfo.dwPlatformId
End Function

Public Function IsAddInMonProcess() As Boolean
    On Error Resume Next
    Select Case getVersion()
        Case 1 'Windows 98/ME
        Dim f As Long, sname As String
        Dim hSnap As Long, proc As PROCESSENTRY32
        hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
        If hSnap = hNull Then Exit Function
        proc.dwSize = Len(proc)
        ' Iterate through the processes
        f = Process32First(hSnap, proc)
        Do While f
            sname = Trim(TrimNull(proc.szExeFile))
            sname = Right(sname, Len(sname) - InStrRev(sname, "\"))
            If StrComp(sname, "addinmon.exe", vbTextCompare) = 0 Then
                IsAddInMonProcess = True
                Exit Function
            End If
            f = Process32Next(hSnap, proc)
        Loop
        Case 2 'Windows NT/2000/XP
            Dim cb As Long
            Dim cbNeeded As Long
            Dim NumElements As Long
            Dim ProcessIDs() As Long
            Dim cbNeeded2 As Long
            Dim NumElements2 As Long
            Dim Modules(1 To 200) As Long
            Dim lRet As Long
            Dim ModuleName As String
            Dim nSize As Long
            Dim hProcess As Long
            Dim i As Long
            'Get the array containing the process id's for each process object
            cb = 8
            cbNeeded = 96
            Do While cb <= cbNeeded
                cb = cb * 2
                ReDim ProcessIDs(cb / 4) As Long
                lRet = EnumProcesses(ProcessIDs(1), cb, cbNeeded)
            Loop
            NumElements = cbNeeded / 4
            For i = 1 To NumElements
                'Get a handle to the Process
                hProcess = OpenProcess(PROCESS_QUERY_INFORMATION _
                    Or PROCESS_VM_READ, 0, ProcessIDs(i))
                'Got a Process handle
                If hProcess <> 0 Then
                    'Get an array of the module handles for the specified process
                    lRet = EnumProcessModules(hProcess, Modules(1), 200, _
                        cbNeeded2)
                    'If the Module Array is retrieved, Get the ModuleFileName
                    If lRet <> 0 Then
                        ModuleName = Space(MAX_PATH)
                        nSize = 500
                        lRet = GetModuleFileNameExA(hProcess, Modules(1), _
                            ModuleName, nSize)
                        ModuleName = Trim(TrimNull(ModuleName))
                        ModuleName = Right(ModuleName, Len(ModuleName) - InStrRev(ModuleName, "\"))
                        If LCase(ModuleName) = "addinmon.exe" Then
                            IsAddInMonProcess = True
                            'Close the handle to the process
                            lRet = CloseHandle(hProcess)
                            Exit Function
                        End If
                    End If
                End If
                'Close the handle to the process
                lRet = CloseHandle(hProcess)
            Next
    End Select
End Function

Private Function TrimNull(ByVal StrIn) As String
    On Error Resume Next
    TrimNull = Left$(StrIn, InStr(StrIn, vbNullChar) - 1)
End Function

Setup and Deployment

To deploy AddInMon in your solution, your setup package should install AddInMon.exe in your application folder. No other support files are required for AddInMon.exe. If multiple applications use AddInMon, the first Add-in to load will call AddInMon and that instance of AddInMon will look for Outlook starting with UI. Multiple instances of AddInMon.exe are not allowed and are not required. You should modify your Add-in to use the OnConnection code shown above and redistribute a new version of your Add-in.

Download

AddInMon is a freeware contribution by Micro Eye, Inc. to the Outlook development community. It is unsupported by Micro Eye. It is your responsibility to thoroughly test AddInMon before including it any solution that you distribute to others.

Current Version: 1.0.0.2106

Last updated 12/21/04 to reduce the polling frequency. This handled a report of AddInMon interfering with the launch of an unrelated application.

Previous update 4/8/2004 to fix a bug that could disconnect the calling add-in. Revised to improve shutdown behavior in Outlook 2003, with help from Ken Slovak of Slovak Technical Services.

Digital Signature: Micro Eye, Inc.

Click Here to Download AddInMon.


Copyright © 1998-2011 Micro Eye, Inc. All rights reserved.  Terms of Use   Privacy Policy