none
Late-bound Event Sink: works with Word, not with Outlook. Why? RRS feed

  • Question

  • Dear friends, I need your help with solving this puzzle.

    I wanted to wrap Word and Outlook interop code in classes that work regardless of Office Version on target user machines.
    For that reason, I chose late binding instead of adding reference to a specific PIA.

    Much with help of examples found on web, I wrote two classes, AutoWord and AutoOutlook, and their code follows below. Both execute well, that is, both run their commands against both Word and Outlook, so I can open documents and send e-mails, ok.

    But I also wanted to listen to some events raised by these external applications. So I also included implementation for Event Interfaces of both applications, from their GUID, and their events DispId. Once again, I relied a lot on samples found in the web.

    What puzzles me, however, is that only AutoWord worked fine, and I can listen to MS-Word events. AutoOutlook, in the other hand, fails on executing the IConnectionPoint.Advise command, which would link it to the Event Sink.

    If I comment and neutralize that single line, the class starts to work, but event-deaf.

    Can someone point what is going wrong here?

    Thank you very much!


    Class AutoWord

        Imports System.Reflection
        Imports System.Runtime.CompilerServices
        Imports System.Runtime.InteropServices
        <ClassInterface(ClassInterfaceType.None)> _
        Public Class AutoWord
            Implements IApplicationEvents2
            Implements IDisposable
         
            Private wordApplication As Object
            Private mConnectionPoint As ComTypes.IConnectionPoint
            Private mSinkCookie As Integer
            Public Sub New()
                Renew()
            End Sub
            Private Sub Renew()
                wordApplication = CreateObject("Word.Application")
                DirectCast(wordApplication, ComTypes.IConnectionPointContainer).FindConnectionPoint( _
                   GetType(IApplicationEvents2).GUID, mConnectionPoint)
                mConnectionPoint.Advise(Me, mSinkCookie)
                wordApplication.Visible = True
            End Sub
            Public Sub Close()
                overrideDocumentBeforeClose = True
                wordApplication.Documents.Close(0)
                wordApplication.Quit(0)
                wordApplication = Nothing
            End Sub
            Public ReadOnly Property Active As Boolean
                Get
                    Return wordApplication IsNot Nothing
                End Get
            End Property
            Public ReadOnly Property Documents() As String()
                Get
                    Dim DD As New List(Of String)
                    Dim DC As Integer = wordApplication.Documents.Count()
                    Dim DI As Object
                    If DC > 0 Then
                        For i = 1 To DC
                            DI = wordApplication.Documents.Item(i)
                            DD.Add(DI.Name)
                        Next
                    End If
                    Return DD.ToArray
                End Get
            End Property
            Public Function AddDocument(ByVal full_name As String) As String
                Dim DI As Object = Nothing
                Try
                    DI = wordApplication.Documents.Add(full_name)
                Catch ex As Exception
                End Try
                If DI IsNot Nothing Then Return DI.Name
                Return Nothing
            End Function
            Public Function OpenDocument(ByVal full_name As String) As String
                Dim _objDocument As Object = Nothing
                Dim _fileName As Object = full_name
                Dim _confirmConversions As Object = False
                Dim _readOnly As Object = False
                Dim _addToRecentFiles As Object = False
                Dim _revert As Object = False
                Dim _format As Object = 0
                Dim _encoding As Object = 50001
                Dim _visible As Object = True
                Dim _openAndRepair As Object = True
                Dim _documentDirection As Object = 0
                Dim _nNoEncodingDialog As Object = True
                Try
                    _objDocument = wordApplication.Documents.Open(_fileName, _confirmConversions, _readOnly, _addToRecentFiles, , , _revert, , , _format, _encoding, _visible, _openAndRepair, _documentDirection, _nNoEncodingDialog)
                Catch ex As Exception
                    Stop
                End Try
                If _objDocument IsNot Nothing Then Return _objDocument.Name
                Return Nothing
            End Function
            Public Sub SaveDocument(ByVal item_name As String)
                Dim DI As Object = Nothing
                Try
                    DI = wordApplication.Documents.Item(item_name)
                Catch ex As Exception
                End Try
                If DI IsNot Nothing Then DI.Save()
            End Sub
            Public Function SaveDocumentAs(ByVal item_name As String, ByVal new_full_name As String) As String
                Dim DI As Object = Nothing
                Try
                    DI = wordApplication.Documents.Item(item_name)
                Catch ex As Exception
                End Try
                If DI IsNot Nothing Then
                    DI.SaveAs(new_full_name)
                    Return DI.Name
                End If
                Return Nothing
            End Function
            Public Sub DocumentFindReplace(ByVal item_name As String, ByVal dfr As Dictionary(Of String, String))
                Dim SL = New List(Of Object), FI As Object
                Dim DI As Object = Nothing, SC As Integer
                Try
                    DI = wordApplication.Documents.Item(item_name)
                Catch ex As Exception
                End Try
                If DI IsNot Nothing Then
                    SC = DI.StoryRanges.Count
                    If SC > 0 Then
                        For i = 1 To SC
                            SL.Add(DI.StoryRanges.Item(i))
                        Next
                    End If
                End If
                Dim _findText As Object
                Dim _matchCase As Object = False
                Dim _matchWholeWord As Object = False
                Dim _matchWildcards As Object = False
                Dim _matchSoundsLike As Object = False
                Dim _matchAllWordForms As Object = False
                Dim _forward As Object = True
                Dim _wrap As Object = 1
                Dim _format As Object = False
                Dim _replaceWith As Object
                Dim _replace As Object = 2
                Dim _matchKashida As Object = False
                Dim _matchDiacritics As Object = False
                Dim _matchAlefHamza As Object = False
                Dim _matchControl As Object = False
                For Each SR In SL
                    FI = SR.Find
                    For Each KVP In dfr
                        _findText = KVP.Key
                        _replaceWith = KVP.Value
                        FI.Execute(
                            _findText,
                            _matchCase,
                            _matchWholeWord,
                            _matchWildcards,
                            _matchSoundsLike,
                            _matchAllWordForms,
                            _forward,
                            _wrap,
                            _format,
                            _replaceWith,
                            _replace,
                            _matchKashida,
                            _matchDiacritics,
                            _matchAlefHamza,
                            _matchControl)
                    Next
                Next
            End Sub
         
            Public Event Quit(ByVal sender As Object, ByVal e As EventArgs)
            Public Sub OnQuit() Implements IApplicationEvents2.Quit
                wordApplication = Nothing
                RaiseEvent Quit(Me, New EventArgs)
            End Sub
         
            Public Event DocumentChange(ByVal sender As Object, ByVal e As EventArgs)
            Public Sub OnDocumentChange() Implements IApplicationEvents2.DocumentChange
                RaiseEvent DocumentChange(Me, New EventArgs)
            End Sub
         
            Public Event DocumentOpen(ByVal sender As Object, ByVal e As DocumentOpenEventArgs)
            Public Sub OnDocumentOpen(ByVal doc As Object) Implements IApplicationEvents2.DocumentOpen
                RaiseEvent DocumentOpen(Me, New DocumentOpenEventArgs(doc))
            End Sub
         
            Public Class DocumentOpenEventArgs
                Inherits EventArgs
                Public Sub New(ByVal document As Object)
                    _doc = document
                End Sub
                Private _doc As Object
                Public ReadOnly Property Document As Object
                    Get
                        Return _doc
                    End Get
                End Property
            End Class
         
            Private overrideDocumentBeforeClose As Boolean
            Public Event DocumentBeforeClose(ByVal sender As Object, ByVal e As DocumentBeforeCloseEventArgs)
            Public Sub OnDocumentBeforeClose(ByVal doc As Object, ByRef cancel As Boolean) Implements IApplicationEvents2.DocumentBeforeClose
                Dim DBCEA = New DocumentBeforeCloseEventArgs(doc)
                RaiseEvent DocumentBeforeClose(Me, DBCEA)
                cancel = DBCEA.Cancel And Not overrideDocumentBeforeClose
            End Sub
            Public Class DocumentBeforeCloseEventArgs
                Inherits EventArgs
                Public Sub New(ByVal document As Object)
                    _doc = document
                End Sub
                Private _doc As Object
                Public ReadOnly Property Document As Object
                    Get
                        Return _doc
                    End Get
                End Property
                Public Property Cancel As Boolean
            End Class
         
         
        #Region "IDisposable Support"
            Private disposedValue As Boolean
         
            Protected Overridable Sub Dispose(ByVal disposing As Boolean)
                If Not Me.disposedValue Then
                    If disposing Then
                        RemoveConnection()
                    End If
                    If Me.Active Then Me.Close()
                End If
                Me.disposedValue = True
            End Sub
         
            Public Sub Dispose() Implements IDisposable.Dispose
                Dispose(True)
                GC.SuppressFinalize(Me)
            End Sub
            Public Sub RemoveConnection()
                If mConnectionPoint IsNot Nothing AndAlso mSinkCookie <> 0 Then
                    mConnectionPoint.Unadvise(mSinkCookie)
                End If
                mConnectionPoint = Nothing
                mSinkCookie = 0
            End Sub
        #End Region
         
            <ComImport(), Guid("000209FE-0000-0000-C000-000000000046"), TypeLibType(CShort(4304))> _
            Private Interface IApplicationEvents2
                <MethodImpl(MethodImplOptions.InternalCall), DispId(2)> _
                Sub Quit()
         
                <MethodImpl(MethodImplOptions.InternalCall), DispId(3)> _
                Sub DocumentChange()
         
                <MethodImpl(MethodImplOptions.InternalCall), DispId(4)> _
                Sub DocumentOpen(<InAttribute(), MarshalAs(UnmanagedType.Interface)> ByVal doc As Object)
         
                <MethodImpl(MethodImplOptions.InternalCall), DispId(6)> _
                Sub DocumentBeforeClose(<InAttribute(), MarshalAs(UnmanagedType.Interface)> ByVal doc As Object,
                                    <InAttribute(), Out(), MarshalAs(UnmanagedType.Interface)> ByRef cancel As Boolean)
            End Interface
        End Class

    Class AutoOutlook

        Imports System.Reflection
        Imports System.Runtime.CompilerServices
        Imports System.Runtime.InteropServices
        <ClassInterface(ClassInterfaceType.None)> _
        Public Class AutoOutlook
            Implements IApplicationEvents
            Implements IDisposable
         
            Private outlookApplication
            Private outlookNamespace As Object
            Private mConnectionPoint As ComTypes.IConnectionPoint
            Private mSinkCookie As Integer
         
            Public Sub New()
                Renew()
            End Sub
            Private Sub Renew()
                Try
                    outlookApplication = GetObject(, "Outlook.Application")
                Catch ex As Exception
                    outlookApplication = CreateObject("Outlook.Application")
                End Try
                TryCast(outlookApplication, ComTypes.IConnectionPointContainer).FindConnectionPoint( _
                   GetType(IApplicationEvents).GUID, mConnectionPoint)
         
                   ' mConnectionPoint.Advise(Me, mSinkCookie)
                   ' the line above doesn't execute in this class and I don't know why
         
                outlookNamespace = outlookApplication.GetNamespace("MAPI")
                outlookNamespace.Logon()
            End Sub
            Public ReadOnly Property Active As Boolean
                Get
                    Return outlookApplication IsNot Nothing
                End Get
            End Property
            Public Sub Test()
                Dim PF = outlookNamespace.PickFolder
                Stop
            End Sub
            Public Function SendMessageHTML(ByVal _to As String, ByVal _subject As String, ByVal _htmlBody As String, ByVal _attachments As String()) As Boolean
                Try
                    Dim olMail As Object
                    olMail = outlookApplication.CreateItem(0)
                    olMail.To = _to
                    olMail.Subject = _subject
                    olMail.BodyFormat = 2
                    olMail.HTMLBody = _htmlBody
                    For Each _file In _attachments
                        If IO.File.Exists(_file) Then
                            olMail.Attachments.Add(_file, 1)
                        End If
                    Next
                    olMail.Send()
                    olMail = Nothing
                    Return True
                Catch ex As Exception
                    Return False
                End Try
            End Function
         
            <ComImport(), Guid("0006304E-0000-0000-C000-000000000046"), TypeLibType(CShort(4304))> _
            Private Interface IApplicationEvents
                <MethodImpl(MethodImplOptions.InternalCall), DispId(61442)> _
                Sub ItemSend(<InAttribute(), MarshalAs(UnmanagedType.Interface)> ByVal item As Object,
                             <InAttribute(), Out(), MarshalAs(UnmanagedType.Interface)> ByRef cancel As Boolean)
         
                <MethodImpl(MethodImplOptions.InternalCall), DispId(&HF003)> _
                Sub NewMail()
         
                <MethodImpl(MethodImplOptions.InternalCall), DispId(&HF006)> _
                Sub Startup()
         
                <MethodImpl(MethodImplOptions.InternalCall), DispId(&HF007)> _
                Sub Quit()
         
            End Interface
         
            Private Sub OnStartup() Implements IApplicationEvents.Startup
                Stop
            End Sub
         
            Private Sub OnQuit() Implements IApplicationEvents.Quit
                Stop
            End Sub
         
            Private Sub OnItemSend(ByVal item As Object, ByRef cancel As Boolean) Implements IApplicationEvents.ItemSend
                Stop
            End Sub
         
            Private Sub OnNewMail() Implements IApplicationEvents.NewMail
                Stop
            End Sub
         
         
        #Region "IDisposable Support"
            Private disposedValue As Boolean
            Protected Overridable Sub Dispose(ByVal disposing As Boolean)
                If Not Me.disposedValue Then
                    If disposing Then
                        RemoveConnection()
                    End If
                End If
                Me.disposedValue = True
            End Sub
            Public Sub Dispose() Implements IDisposable.Dispose
                Dispose(True)
                GC.SuppressFinalize(Me)
            End Sub
            Public Sub RemoveConnection()
                If mConnectionPoint IsNot Nothing AndAlso mSinkCookie <> 0 Then
                    mConnectionPoint.Unadvise(mSinkCookie)
                End If
                mConnectionPoint = Nothing
                mSinkCookie = 0
            End Sub
        #End Region
         
        End Class

    • Moved by Yang,Chenfei Wednesday, March 26, 2014 2:07 AM VSTO Issue
    Tuesday, March 25, 2014 11:26 AM

Answers

All replies

  • Hello,

    Kevin and some others help here a lot with office, despite that I think that you have a wider response for your very Office inside question in the visual studio for office forum.

    http://social.msdn.microsoft.com/Forums/vstudio/en-US/home?forum=vsto


    Success
    Cor

    Tuesday, March 25, 2014 11:33 AM
  • Hello,

    Why do you need to use IConnectionPoint interface for handling Outlook events?

    In .NET world you need to use events instead. Please see the VB app automates Outlook (VBAutomateOutlook) sample add-in project for more information. Also you may find the following article helpful:

    How to automate Outlook by using Visual Basic

    How to create an appointment by using Outlook Object Model in Microsoft Visual Basic .NET

    How to retrieve contacts by using Outlook Object Model in Visual Basic .NET


    • Edited by Eugene Astafiev Wednesday, March 26, 2014 3:09 PM
    • Marked as answer by Marvin_Guo Wednesday, April 2, 2014 1:52 AM
    Wednesday, March 26, 2014 1:03 PM
  • If you were to use a PIA for the oldest version of Outlook you want to support that would work with all later versions. If you used say an embedded PIA for Outlook 2013 with Framework 4 or higher and checked at runtime for version you can handle methods/properties/events for that version. Just don't try to use or handle things added in a version later than you detect at runtime.

    For the method you use have you tried doing a QI for the IConnectionPointContainer for the specific object (it could be Outlook.Application or any other Outlook object that has events) and then calling FindConnectionPoint on that container object?


    Ken Slovak MVP - Outlook

    Wednesday, March 26, 2014 3:03 PM
  • First, sorry for taking long time to answer. I didn't receive an e-mail warning me about your answer.

    I've chosen late binding because I don't really know which versions of office suite will be found in the workstations where my app will run. Therefore, I can't really use PIA.

    I was delighted to see that my class AutoWord was able to listen to events raised by late-bound Word objects. However, i'm just intrigued about the possible reason for AutoOutlook can't do the same.

    Thank you very much for your kind attention.

    Thursday, July 31, 2014 1:57 PM
  • First, sorry for taking long time to answer. I didn't receive an e-mail warning me about your answer.

    Please forgive my cluelessness, but what exactly do you mean with "doing a QI for the IConnectionPointContainer for the specific object (it could be Outlook.Application or any other Outlook object that has events) and then calling FindConnectionPoint on that container object?"

    Thank you very much!

    Thursday, July 31, 2014 2:00 PM
  • A QI is a QueryInterface call. It returns the capabilities of the object and if a specific thing is here for the object. In this case it would be a QI for an Outlook object such as Outlook.Application to see if IConnectionPointContainer is supported.

    As I mentioned originally I don't think you need anything as elaborate as what you're using. I'd use Framework 4 or later and embed the Office PIAs for the earliest version you plan to support. Those methods, properties and events will be available in later versions (aside from a handful of deprecated things).

    In that case there's no need for the complexity of the code that's being used.


    Ken Slovak MVP - Outlook

    Thursday, July 31, 2014 2:45 PM
  • A QI is a QueryInterface call. It returns the capabilities of the object and if a specific thing is here for the object. In this case it would be a QI for an Outlook object such as Outlook.Application to see if IConnectionPointContainer is supported.

    As I mentioned originally I don't think you need anything as elaborate as what you're using. I'd use Framework 4 or later and embed the Office PIAs for the earliest version you plan to support. Those methods, properties and events will be available in later versions (aside from a handful of deprecated things).

    In that case there's no need for the complexity of the code that's being used.


    Ken Slovak MVP - Outlook


    In that case, how should I get those PIAs (e.g. for Office 2002) and embed them in my project?
    Tuesday, August 5, 2014 5:36 PM
  • I wouldn't recommend doing that for Outlook 2002. The PIA's for 2002 were quite buggy, I wouldn't even use them at all.

    Why do you want to support a 12 year old version of Office that's no longer supported by almost anyone, or used by almost anyone? I would start support for the 2003 version of Office, or even 2007.

    You get the PIA's by having that version of Office installed and referencing the PIA's in your project.

    If you don't have Outlook 2003 you can use a later version and its PIA's. In a case like that you just have to be very, very careful that you don't use methods/properties/events that aren't in the version that's running at runtime. You have to check for the runtime version.

    You can still use the method you were using, but my guess is that you're really not familiar with that sort of thing from your unfamiliarity with the concept of querying an object using a QI call.


    Ken Slovak MVP - Outlook

    Tuesday, August 5, 2014 5:53 PM
  • First thing, let me thank you for the guidelines you gave me. I feel I owe some explanation, though, in order to my questions make a little more sense. I'm aware that my questions might seem strange out of their context.

    Actually, I'm working in a non-commercial project, which means that it won't be sold nor provide me any kind of profit. However, it would help to relief a painful workload from dozens of people, including myself. Nevertheless, I have no official support from my organization. This means that I am free to write and use any personal .NET application I want and be able to, as long as I do not invoke administrative privileges and do not tamper with the software already installed in each workstation. And, you must believe me: I'm not sure wether we still have Office 2003 anywhere or not, but I do know for sure that we have Windows Vista plus Office 2007 two aisles ahead of my desk, because the software we have is OEM licensed with the computer, and each computer stays in use until it melts in stress, or whatever alike.

    My own workstation is far better, it has Windows 8 (not 8.1) and Office 2013. But this prevents me to early bind Interop Objects , because if I do it, my application loses backward compatibility in lots of computers.

    That said, you are right when you say I'm actually not familiar with these things. I learned how to listen to events raised by a late bound object just reproducing sample code found on web. To be exact, my knowledge came from this source: http://www.codeproject.com/Articles/10262/Receiving-Events-from-late-bound-COM-servers

    But I can do better than being a naive code copier-and-paster. I'm just an amateur programmer, but I do this for like fifteen years now im my spare time besides my main job (that is out of IT area), and the small solutions I'me made using Clipper, Excel, VBA, HTML, etc. were almost quite important to save huge amounts of time for people who work under heavy demanding and lack of resources. When I ascended from VBA to VB.NET, I gathered my knowledge by reading several books and websites and now I think I can do a lot of interesting things. Of course, I'm no expert, but I like to understand what I do, and what the code I write does.

    But in spite of all that, and of having posted this same question in several forums, no one could yet explain why the line below runs with Word.Application object and fails, raising an error (System.InvalidCastException), with Outlook.Application object:

    mConnectionPoint.Advise(Me, mSinkCookie)

    Therefore, I can see no solution but these two: 1. I figure out how to solve this problem with the late-bound event sink, or 2. I find out how to compile my application pointing to a version of MS-Office that is different from the one installed in my computer.

    And I really would like to understand the cause behind the error I'm experiencing.

    Thank you very much for your patience.

    Wednesday, August 6, 2014 6:09 PM
  • The Word code you're using assumes that Word supports the IConnectionPointContainer interface )the "I" stands for interface).

    Your code also assumes that Outlook.Application supports that same interface. It probably doesn't, or you're not getting the Application object in your code. You might be getting an Explorer window, which is a COM object that displays folder views, and not Application.

    You might want to add a check for the type of object you're getting, using a typeof() call. http://msdn.microsoft.com/en-us/library/58918ffs.aspx

    The way to determine if the object supports IConnectionPointContainer is to issue a QueryInterface call to determine the capabilities of the object. This link is probably more than you want to know, but what you should at least be aware of for the sort of code you're using: http://msdn.microsoft.com/en-us/library/aa645736(v=vs.71).aspx.

    These 2 links to articles by former members of the VSTO team are also informative:  http://blogs.msdn.com/b/mshneer/archive/2008/10/28/better-eventing-support-in-clr-4-0-using-nopia-support.aspx and http://blogs.msdn.com/b/mshneer/archive/2008/06/03/targeting-multiple-versions-of-office-without-pias.aspx.

    Personally I'd develop in VS 2010 or later, use Framework 4 or later and use the Outlook 2013 you have now. I'd embed the Office and Outlook PIA's (and any others I needed). At runtime I'd check Outlook.Application.Version and determine the capabilities of the available object model from that.

    version.Substring(0, 2) == "11" == 2003

         == "12" == 2007

         == "14" == 2010

         == "15" == 2013

    See http://msdn.microsoft.com/en-us/library/ee317478.aspx for more information on embedding PIAs.


    Ken Slovak MVP - Outlook

    Wednesday, August 6, 2014 7:47 PM
  • Nice! Thank you very much! I'll put my hands at work now, and research all the links you posted. If I sort out some conclusion, I'll post it here. For now, thanks again!

    Kind regards.

    Wednesday, August 6, 2014 9:13 PM