Unit Testing of .NET Add-In for Microsoft Office - vb.net

Has anyone got any suggestions for unit testing a Managed Application Add-In for Office? I'm using NUnit but I had the same issues with MSTest.
The problem is that there is a .NET assembly loaded inside the Office application (in my case, Word) and I need a reference to that instance of the .NET assembly. I can't just instantiate the object because it wouldn't then have an instance of Word to do things to.
Now, I can use the Application.COMAddIns("Name of addin").Object interface to get a reference, but that gets me a COM object that is returned through the RequestComAddInAutomationService. My solution so far is that for that object to have proxy methods for every method in the real .NET object that I want to test (all set under conditional-compilation so they disappear in the released version).
The COM object (a VB.NET class) actually has a reference to the instance of the real add-in, but I tried just returning that to NUnit and I got a nice p/Invoke error:
System.Runtime.Remoting.RemotingException : This remoting proxy has no channel sink which means either the server has no registered server channels that are listening, or this application has no suitable client channel to talk to the server.
at System.Runtime.Remoting.Proxies.RemotingProxy.InternalInvoke(IMethodCallMessage reqMcmMsg, Boolean useDispatchMessage, Int32 callType)
at System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(IMessage reqMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
I tried making the main add-in COM visible and the error changes:
System.InvalidOperationException : Operation is not valid due to the current state of the object.
at System.RuntimeType.ForwardCallToInvokeMember(String memberName, BindingFlags flags, Object target, Int32[] aWrapperTypes, MessageData& msgData)
While I have a work-around, it's messy and puts lots of test code in the real project instead of the test project - which isn't really the way NUnit is meant to work.

This is how I resolved it.
Just about everything in my add-in runs from the Click method of a button in the UI. I have changed all those Click methods to consist only of a simple, parameterless call.
I then created a new file (Partial Class) called EntryPoint that had lots of very short Friend Subs, each of which was usually one or two calls to parameterised worker functions, so that all the Click methods just called into this file. So, for example, there's a function that opens a standard document and calls a "save as" into our DMS. The function takes a parameter of which document to open, and there are a couple of dozen standard documents that we use.
So I have
Private Sub btnMemo_Click(ByVal Ctrl As Microsoft.Office.Core.CommandBarButton, ByRef CancelDefault As Boolean) Handles btnMemo.Click
DocMemo()
End Sub
in the ThisAddin and then
Friend Sub DocMemo()
OpenDocByNumber("Prec", 8862, 1)
End Sub
in my new EntryPoints file.
I add a new AddInUtilities file which has
Public Interface IAddInUtilities
#If DEBUG Then
Sub DocMemo()
#End If
End Interface
Public Class AddInUtilities
Implements IAddInUtilities
Private Addin as ThisAddIn
#If DEBUG Then
Public Sub DocMemo() Implements IAddInUtilities.DocMemo
Addin.DocMemo()
End Sub
#End If
Friend Sub New(ByRef theAddin as ThisAddIn)
Addin=theAddin
End Sub
End Class
I go to the ThisAddIn file and add in
Private utilities As AddInUtilities
Protected Overrides Function RequestComAddInAutomationService() As Object
If utilities Is Nothing Then
utilities = New AddInUtilities(Me)
End If
Return utilities
End Function
And now it's possible to test the DocMemo() function in EntryPoints using NUnit, something like this:
<TestFixture()> Public Class Numbering
Private appWord As Word.Application
Private objMacros As Object
<TestFixtureSetUp()> Public Sub LaunchWord()
appWord = New Word.Application
appWord.Visible = True
Dim AddIn As COMAddIn = Nothing
Dim AddInUtilities As IAddInUtilities
For Each tempAddin As COMAddIn In appWord.COMAddIns
If tempAddin.Description = "CobbettsMacrosVsto" Then
AddIn = tempAddin
End If
Next
AddInUtilities = AddIn.Object
objMacros = AddInUtilities.TestObject
End Sub
<Test()> Public Sub DocMemo()
objMacros.DocMemo()
End Sub
<TestFixtureTearDown()> Public Sub TearDown()
appWord.Quit(False)
End Sub
End Class
The only thing you can't then unit test are the actual Click events, because you're calling into EntryPoints in a different way, ie through the RequestComAddInAutomationService interface rather than through the event handlers.
But it works!

Consider the various mocking frameworks NMock, RhinoMocks, etc. to fake the behavior of Office in your tests.

Related

VB.NET DLL does not trigger methods in VB6

I have created a DLL in VB.NET and loaded in VB6.
All variables and methods are working very well.
In the DLL, I have a RaiseEvent.
The RaiseEvent from .NET DLL cannot trigger the methods in VB6.
Option Explicit On
Option Strict On
Imports System.Windows.Forms
Imports System.Net.Sockets
Imports System.IO
Imports System.Text
Imports System.Net
Imports System.ComponentModel
Imports System.Threading
Imports System.Runtime.InteropServices
<ComClass(TestDLL.ClassId, TestDLL.InterfaceId, TestDLL.EventsId)>
Public Class TestDLL
Public Const ClassId As String = "6E9AB173-14BD-4DE4-9AE0-A9638FCE40B3"
Public Const InterfaceId As String = "E659D166-F952-489F-899F-0104553B44E4"
Public Const EventsId As String = "1C38AB4A-84B9-4CC2-A090-0C272177ECED"
Public Event Disconnected()
Public Event FirstConnect()
Public Event Waagerecht()
Private Sub Received(ByVal msg As String) Handles Me.Receive
RaiseEvent Waagerecht()
End Sub
This DLL is working in C#, VB.NET and in Labview amazing. Only not in VB6
RaiseEvent Part in VB.NET DLL
And the code in VB6:
Option Explicit
Private WithEvents MyNetClass As TestDll
Private Sub Form_Load()
Set MyNetClass = New TestDll
End Sub
Private Sub Form_Terminate()
Set MyNetClass = Nothing
End Sub
And the methods for triggering:
Private Sub MyNetClass_Waagerecht()
MsgBox "Ich werde angezeigt, sobald dll mir was sagt"
End Sub
Disclaimer: The following is based on issues I have experienced with Office VBA and events being raised on a secondary thread. Due to their similar underlying architecture, I am assuming that VB6 will have similar issues. I do not have VB6 so I can not verify the this.
With that stated, it appears that your code is using NetworkStream.BeginRead and the callback events will arrive on a secondary thread, you may be experiencing problems due to the event being raised on the secondary thread.
To provide thread synchronization to the COM class, I utilize a System.Windows.Forms.Control. The control is created in the class constructor to capture the thread it is created on. Events are defined as Custom Event so that the RaiseEvent method can be defined to use the InvokeRequired property of the synchronizing control. I have only shown the pattern for your Waagerecht event; if this works, you will need to implement the same pattern for your other Public events.
Private synch As ISynchronizeInvoke
Public Sub New()
Dim marshalingControl As New System.Windows.Forms.Control
marshalingControl.CreateControl() ' the handle must be created
synch = marshalingControl
End Sub
Private _Waagerecht As Action
Public Custom Event Waagerecht As Action
AddHandler(value As Action)
_Waagerecht = CType([Delegate].Combine(_Waagerecht, value), Action)
End AddHandler
RemoveHandler(value As Action)
_Waagerecht = CType([Delegate].Remove(_Waagerecht, value), Action)
End RemoveHandler
RaiseEvent()
If _Waagerecht IsNot Nothing Then
If synch.InvokeRequired Then
synch.Invoke(_Waagerecht, {})
Else
_Waagerecht.Invoke()
End If
End If
End RaiseEvent
End Event
Edit: Recommendations For Debugging
A first step add the following to the VB.Net TestDLL class.
Public Sub TestWaagerecht()
RaiseEvent Waagerecht()
End Sub
Then in your VB6 code, add a button to your form and in its Click handler, call MyNetClass.TestWaagerecht. This will verify whether or not the VB6 code is receiving the event.
Hopefully this will work with VB6. Try setting up VB.Net project for debugging. If the project does not have an app.config, add one (Project Menu->Add New Item->"Application Configuration File"). I don't know why, but without the app.Config file, break-points are not hit.
Next go to Project Properties->Debug Tab and set the "Start External Program" to point to the VB6 program. Add a break-point in the Sub TestWaagerecht that I asked you to add above.
When you click on the "Start" button in VS to start debugging, it should launch VB6. Now load your VB6 project and start debugging it. Click on the button that calls TestWaagerecht. Hopefully, the VS break-point will be hit. If it is hit, then all is well and you can start debugging your code to hopefully find the issue.

Raising an event with NSubstitute in VB.NET

I'm having trouble raising an event in a unit test using VB.NET and NSubstitute. The interface being mocked defines an event:
Event BlockOfVehiclesProcessed(source As Object, stats As ProcessingStats)
The class under test registers a handler for the event. In the unit test, I want to raise the event so the handler in the class under test gets invoked. Based on the NSubstitute documentation (all C#, unfortunately) and various answers on Stackoverflow, etc., I tried various permutations of this:
AddHandler mock.BlockOfVehiclesProcessed, Raise.EventWith(New ProcessingStats(50))
But I haven't found anything that compiles. One error message:
Value of type 'EventHandlerWrapper(...)' cannot be converted to '...BlockOfVehiclesProcessedEventHandler'
I tried passing 0 args and 2 args to EventWith(), I tried specifying the type arg explicitly for EventWith(), and I tried Raise.Event(), but I can't find the magic sequence to make the compiler happy. Does anyone have an example of a working VB unit test that raises an event?
Then problem is that NSubstitute doesn't support anonymous event handler types created by vb.net when event declared without explicitly provided event handler type.
If use of NSubstitute is mandatory (and as an answer for question) declaring event of explicitly provided event handler type will solve your problem.
' Example with Action as event handler type
Public Interface IVehicleProcessor
Event BlockOfVehiclesProcessed As Action(Of Object, String)
End Interface
Public Class System
Private ReadOnly _processor As IVehicleProcessor
Public Property ProcessedStats As String
Public Sub New(IVehicleProcessor processor)
_processor = processor
AddHandler _processor.BlockOfVehiclesProcessed, Sub(sender, stats) ProcessedStats = stats
End Sub
End System
' Test
Public Sub ShouldNotifyProcessedStats()
Dim fakeProcessor = Substitute.For(Of IVehicleProcessor)
Dim system As New System(fakeProcessor)
' Raise an event with known event handler type
AddHandler fakeProcessor.BlockOfVehiclesProcessed,
Raise.Event(Of Action(Of Object, String))(fakeProcessor, "test-stats")
system.ProcessedStats.Should().Be("test-stats") ' Pass
End Sub
Another approach is to create own fake implementation of interface with an event. I found this approach much better, only because you don't need to change your production code, because some testing framework not able to support vb.net language features.
Public Class FakeVehicleProcessor
Implements IVehicleProcessor
Public Event BlockOfVehiclesProcessed(source As Object, stats As String) Implements IVehicleProcessor.BlockOfVehiclesProcessed
' Use this method to raise an event with required arguments
Public Sub RaiseEventWith(stats As String)
RaiseEvent BlockOfVehiclesProcessed(Me, stats)
End Sub
End Class
' Test
Public Sub ShouldNotifyProcessedStats()
Dim fakeProcessor As New FakeVehicleProcessor()
Dim system As New System(fakeProcessor)
fakeProcessor.RaiseEventWith("test-stats")
system.ProcessedStats.Should().Be("test-stats") ' Pass
End Sub

Can we use Interfaces and Events together at the same time?

I'm still trying to wrap my head around how Interfaces and Events work together (if at all?) in VBA. I'm about to build a large application in Microsoft Access, and I want to make it as flexible and extendable as possible. To do this, I want to make use of MVC, Interfaces (2) (3) , Custom Collection Classes, Raising Events Using Custom Collection Classes, finding better ways to centralize and manage the events triggered by the controls on a form, and some additional VBA design patterns.
I anticipate that this project is going to get pretty hairy so I want to try to grok the limits and benefits of using interfaces and events together in VBA since they are the two main ways (I think) to really implement loose-coupling in VBA.
To start with, there is this question about an error raised when trying to use interfaces and events together in VBA. The answer states "Apparently Events are not allowed to be passed through an interface class into the concrete class like you want to using 'Implements'."
Then I found this statement in an answer on another forum: "In VBA6 we can only raise events declared in a class's default interface - we can't raise events declared in an Implemented interface."
Since I'm still groking interfaces and events (VBA is the first language I've really had a chance to try out OOP in a real-world setting, I know shudder), I can't quite work through in my mind what all this means for using events and interfaces together in VBA. It kinda sounds like you can use them both at the same time, and it kinda sounds like you can't. (For instance, I'm not sure what is meant above by "a class's default interface" vs "an Implemented interface.")
Can someone give me some basic examples of the real benefits and limitations of using Interfaces and Events together in VBA?
This is a perfect use-case for an Adapter: internally adapting the semantics for a set of contracts (interfaces) and exposing them as its own external API; possibly according to some other contract.
Define class modules IViewEvents:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "IViewEvents"
Public Sub OnBeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean): End Sub
Public Sub OnAfterDoSomething(ByVal Data As Object): End Sub
Private Sub Class_Initialize()
Err.Raise 5, mModuleName, AccessError(5) & "-Interface class must not be instantiated."
End Sub
IViewCommands:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "IViewCommands"
Public Sub DoSomething(ByVal arg1 As String, ByVal arg2 As Long): End Sub
Private Sub Class_Initialize()
Err.Raise 5, mModuleName, AccessError(5) & "-Interface class must not be instantiated."
End Sub
ViewAdapter:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "ViewAdapter"
Public Event BeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
Public Event AfterDoSomething(ByVal Data As Object)
Private mView As IViewCommands
Implements IViewCommands
Implements IViewEvents
Public Function Initialize(View As IViewCommands) As ViewAdapter
Set mView = View
Set Initialize = Me
End Function
Private Sub IViewCommands_DoSomething(ByVal arg1 As String, ByVal arg2 As Long)
mView.DoSomething arg1, arg2
End Sub
Private Sub IViewEvents_OnBeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
RaiseEvent BeforeDoSomething(Data, Cancel)
End Sub
Private Sub IViewEvents_OnAfterDoSomething(ByVal Data As Object)
RaiseEvent AfterDoSomething(Data)
End Sub
and Controller:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "Controller"
Private WithEvents mViewAdapter As ViewAdapter
Private mData As Object
Public Function Initialize(ViewAdapter As ViewAdapter) As Controller
Set mViewAdapter = ViewAdapter
Set Initialize = Me
End Function
Private Sub mViewAdapter_AfterDoSomething(ByVal Data As Object)
' Do stuff
End Sub
Private Sub mViewAdapter_BeforeDoSomething(ByVal Data As Object, ByRef Cancel As Boolean)
Cancel = Data Is Nothing
End Sub
plus Standard Modules Constructors:
Option Compare Database
Option Explicit
Option Private Module
Private Const mModuleName As String = "Constructors"
Public Function NewViewAdapter(View As IViewCommands) As ViewAdapter
With New ViewAdapter: Set NewViewAdapter = .Initialize(View): End With
End Function
Public Function NewController(ByVal ViewAdapter As ViewAdapter) As Controller
With New Controller: Set NewController = .Initialize(ViewAdapter): End With
End Function
and MyApplication:
Option Compare Database
Option Explicit
Private Const mModuleName As String = "MyApplication"
Private mController As Controller
Public Function LaunchApp() As Long
Dim frm As IViewCommands
' Open and assign frm here as instance of a Form implementing
' IViewCommands and raising events through the callback interface
' IViewEvents. It requires an initialization method (or property
' setter) that accepts an IViewEvents argument.
Set mController = NewController(NewViewAdapter(frm))
End Function
Note how use of the Adapter Pattern combined with programming to interfaces results in a very flexible structure, where different Controller or View implementations can be substituted in at run time. Each Controller definition (in the case of different implementations being required) uses different instances of the same ViewAdapter implementation, as Dependency Injection is being used to delegate the event-source and command-sink for each instance at run time.
The same pattern can be repeated to define the relationship between the Controller/Presenter/ViewModel and the Model, though implementing MVVM in COM can get rather tedious. I have found MVP or MVC is usually better suited for COM-based applications.
A production implementation would also add proper error handling (at a minimum) to the extent supported by VBA, which I have only hinted at with the definition of the mModuleName constant in each module.
An interface is, strictly speaking and only in OOP terms, what an object exposes to the outside world (i.e. its callers/"clients").
So you can define an interface in a class module, say ISomething:
Option Explicit
Public Sub DoSomething()
End Sub
In another class module, say Class1, you can implement the ISomething interface:
Option Explicit
Implements ISomething
Private Sub ISomething_DoSomething()
'the actual implementation
End Sub
When you do exactly that, notice how Class1 doesn't expose anything; the only way to access its DoSomething method is through the ISomething interface, so the calling code would look like this:
Dim something As ISomething
Set something = New Class1
something.DoSomething
So ISomething is the interface here, and the code that actually runs is implemented in the body of Class1. This is one of the fundamental pillars of OOP: polymorphism - because you could very well have a Class2 that implements ISomething in a wildly different way, yet the caller wouldn't ever need to care at all: the implementation is abstracted behind an interface - and that's a beautiful and refreshing thing to see in VBA code!
There are a number of things to keep in mind though:
Fields are normally considered as implementation details: if an interface exposes public fields, implementing classes must implement a Property Get and a Property Let (or Set, depending on the type) for it.
Events are considered implementation details, too. Therefore they need to be implemented in the class that Implements the interface, not the interface itself.
That last point is rather annoying. Given Class1 that looks like this:
'#Folder StackOverflowDemo
Public Foo As String
Public Event BeforeDoSomething()
Public Event AfterDoSomething()
Public Sub DoSomething()
End Sub
The implementing class would look like this:
'#Folder StackOverflowDemo
Implements Class1
Private Sub Class1_DoSomething()
'method implementation
End Sub
Private Property Let Class1_Foo(ByVal RHS As String)
'field setter implementation
End Property
Private Property Get Class1_Foo() As String
'field getter implementation
End Property
If it's any easier to visualize, the project looks like this:
So Class1 might define events, but the implementing class has no way of implementing them - that's one sad thing about events and interfaces in VBA, and it stems from the way events work in COM - events themselves are defined in their own "event provider" interface; so a "class interface" can't expose events in COM (as far as I understand it), and therefore in VBA.
So the events must be defined on the implementing class to make any sense:
'#Folder StackOverflowDemo
Implements Class1
Public Event BeforeDoSomething()
Public Event AfterDoSomething()
Private foo As String
Private Sub Class1_DoSomething()
RaiseEvent BeforeDoSomething
'do something
RaiseEvent AfterDoSomething
End Sub
Private Property Let Class1_Foo(ByVal RHS As String)
foo = RHS
End Property
Private Property Get Class1_Foo() As String
Class1_Foo = foo
End Property
If you want to handle the events Class2 raises while running code that implements the Class1 interface, you need a module-level WithEvents field of type Class2 (the implementation), and a procedure-level object variable of type Class1 (the interface):
'#Folder StackOverflowDemo
Option Explicit
Private WithEvents SomeClass2 As Class2 ' Class2 is a "concrete" implementation
Public Sub Test(ByVal implementation As Class1) 'Class1 is the interface
Set SomeClass2 = implementation ' will not work if the "real type" isn't Class2
foo.DoSomething ' runs whichever implementation of the Class1 interface was supplied
End Sub
Private Sub SomeClass2_AfterDoSomething()
'handle AfterDoSomething event of Class2 implementation
End Sub
Private Sub SomeClass2_BeforeDoSomething()
'handle BeforeDoSomething event of Class2 implementation
End Sub
And so we have Class1 as the interface, Class2 as the implementation, and Class3 as some client code:
...which arguably defeats the purpose of polymorphism, since that class is now coupled with a specific implementation - but then, that's what VBA events do: they are implementation details, inherently coupled with a specific implementation... as far as I know.
Because bounty is already headed for Pieter's answer I'll not attempt to answer the MVC aspect of the question but instead the headline question. The answer is Events have limits.
It would be harsh to call them "syntactic sugar" because they save a lot of code but at some point if your design gets too complex then you have to bust out and manually implement the functionality.
But first, a callback mechanism (for that is what events are)
modMain, the entry/starting point
Option Explicit
Sub Main()
Dim oClient As Client
Set oClient = New Client
oClient.Run
End Sub
Client
Option Explicit
Implements IEventListener
Private Sub IEventListener_SomethingHappened(ByVal vSomeParam As Variant)
Debug.Print "IEventListener_SomethingHappened " & vSomeParam
End Sub
Public Sub Run()
Dim oEventEmitter As EventEmitter
Set oEventEmitter = New EventEmitter
oEventEmitter.ServerDoWork Me
End Sub
IEventListener, the interface contract that describes the events
Option Explicit
Public Sub SomethingHappened(ByVal vSomeParam As Variant)
End Sub
EventEmitter, the server class
Option Explicit
Public Sub ServerDoWork(ByVal itfCallback As IEventListener)
Dim lLoop As Long
For lLoop = 1 To 3
Application.Wait Now() + CDate("00:00:01")
itfCallback.SomethingHappened lLoop
Next
End Sub
So how does WithEvents work? One answer is to look in the type library, here is some IDL from Access (Microsoft Access 15.0 Object Library) defining the events to be raised.
[
uuid(0EA530DD-5B30-4278-BD28-47C4D11619BD),
hidden,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "Microsoft.Office.Interop.Access._FormEvents")
]
dispinterface _FormEvents2 {
properties:
methods:
[id(0x00000813), helpcontext(0x00003541)]
void Load();
[id(0x0000080a), helpcontext(0x00003542)]
void Current();
'/* omitted lots of other events for brevity */
};
Also from Access IDL here is the class detailing what its main interface is and what is event interface is, look for source keyword, and VBA needs a dispinterface so ignore one of them.
[
uuid(7398AAFD-6527-48C7-95B7-BEABACD1CA3F),
helpcontext(0x00003576)
]
coclass Form {
[default] interface _Form3;
[source] interface _FormEvents;
[default, source] dispinterface _FormEvents2;
};
So what that is saying to a client is that operate me via the _Form3 interface but if you want to receive events then you, the client, must implement _FormEvents2. And believe it or not VBA will when WithEvents is met spin up an object that implements the source interface for you and then route incoming calls to your VBA handler code. Pretty amazing actually.
So VBA generates a class/object implementing the source interface for you but questioner has met the limits with the interface polymorphism mechanism and events. So my advice is to abandon WithEvents and implement you own callback interface and this is what the given code above does.
For more information then I recommend reading a C++ book that implements events using the connection point interfaces, your google search terms are connection points withevents
Here is a good quote from 1994 highlighting the work VBA does I mentioned above
After slogging through the preceding CSink code, you'll find that intercepting events in Visual Basic is almost dishearteningly easy. You simply use the WithEvents keyword when you declare an object variable, and Visual Basic dynamically creates a sink object that implements the source interface supported by the connectable object. Then you instantiate the object using the Visual Basic New keyword. Now, whenever the connectable object calls methods of the source interface, Visual Basic's sink object checks to see whether you have written any code to handle the call.
EDIT: Actually, mulling my example code you could simplify and abolish the intermediate interface class if you do not want to replicate the way COM does things and you are not bothered by coupling. It is after all just a glorified callback mechanism. I think this is an example of why COM got a reputation for being overly complicated.
Implemented Class
' clsHUMAN
Public Property Let FirstName(strFirstName As String)
End Property
Derived Class
' clsEmployee
Implements clsHUMAN
Event evtNameChange()
Private Property Let clsHUMAN_FirstName(RHS As String)
UpdateHRDatabase
RaiseEvent evtNameChange
End Property
Using in form
Private WithEvents Employee As clsEmployee
Private Sub Employee_evtNameChange()
Me.cmdSave.Enabled = True
End Sub

Avoiding File Not Found Exception with Outlook Interop

I've got a simple singleton pattern class by which my application integrates with outlook, and several of the computers that run my application do not have outlook installed. I've wrapped all the interop stuff in try-catches to avoid raising exceptions when outlook is not available, but I'm still getting automated bug reports with FileNotFound exceptions.
Here is (the relevant code) in my class:
Imports Microsoft.Office.Interop
Public Class OutlookIntegration
Private Shared _instance As OutlookIntegration
Public Shared Sub Initialize()
_instance = New OutlookIntegration()
End Sub
Private _outlookApp As Outlook.Application
Private _outlookNs As Outlook.NameSpace
Private ReadOnly _outlookEnabled As Boolean
Private Sub New()
Try
_outlookApp = New Outlook.Application
_outlookNs = _outlookApp.GetNamespace("mapi")
_outlookNs.Logon()
Catch ex As Exception
_outlookApp = Nothing
_outlookEnabled = False
Exit Sub
End Try
_outlookEnabled = True
End Sub
End Class
And the error I'm getting is this:
Message: Could not load file or assembly 'office, Version=11.0.0.0,
Culture=neutral, PublicKeyToken=71e9bce111e9429c' or one of its
dependencies. The system cannot find the file specified.
System.IO.FileNotFoundException
at OutlookIntegration..ctor()
at OutlookIntegration.Initialize() in OutlookIntegration.vb:line 7
at MyApplication_Startup(Object sender, StartupEventArgs e) in ApplicationEvents.vb:line 139
(System.IO.FileNotFoundException) Could not load file or assembly 'office, Version=11.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' or one of its dependencies. The system cannot find the file specified.
It seems like i'm missing something simple here. Is the stack trace a red herring?
The exception is being thrown when your private members are being loaded into memory (Outlook.Application, Outlook.NameSpace, etc.) during a call to the OutlookIntegration constructor (OutlookIntegration..ctor()) and the appropriate Outlook dependencies don't exist on the client machine.
There are several ways you could handle this:
Handle the error when the constructor throws this exception (try/catch around ctor declaration in your Initialize() method)
Create another class that just tests whether Outlook is installed (avoid using private members)
Read the Windows registry keys which tell you whether Outlook is present or not
It's always best not to throw exceptions for performance reasons - so #3 would be my preference, but as long as you aren't continually performing this check you should be ok to use any of these options.

Handling VB.NET events in VB6 code

I have some VB6 code that instantiates a class which handles events that are being raised from a VB.NET component. The VB6 is pretty straightforward:
private m_eventHandler as new Collection
...
public sub InitSomething()
dim handler as EventHandler
set handler = new EventHandler
m_eventHandler.Add handler
...
m_engine.Start
end sub
Note that the event handler object has to live beyond the scope of the init method (which is why it is being stored in a Collection). Note also that m_engine.Start indicates the point in the program where the VB.NET component would start raising events.
The actual event handler (as requested):
Private WithEvents m_SomeClass As SomeClass
Private m_object as Object
...
Private Sub m_SomeClass_SomeEvent(obj As Variant)
Set obj = m_object
End Sub
Note that m_object is initialized when an instance of EventHandler is created.
The VB.NET code which raises the event is even simpler:
Public ReadOnly Property SomeProp() As Object
Get
Dim obj As Object
obj = Nothing
RaiseEvent SomeEvent(obj)
SomeProp = obj
End Get
End Property
My problem is that when I debug the VB6 program, the first time InitSomething gets called, the event will not be handled (the VB6 event handler is never entered). Subsequent calls to InitSomething does work.
Everything works as I would have expected when I run the program outside the debugger. At this point, I'm not even sure if this is something I should be worried about.
It may or may not be relevant but the VB.NET was converted from a VB6 using the Visual Studio code conversion tool (and subsequently manually cleaned up).
I've found that if you are writing .Net Components for Consumption in VB6 (or any other COM environment) the utilisation of Interfaces is absolutely criticial.
The COM templates that comes out of the box with VStudio leave a lot to be desired especially when you are trying to get Events to work.
Here's what I've used.
Imports System.Runtime.InteropServices
Imports System.ComponentModel
<InterfaceType(ComInterfaceType.InterfaceIsDual), Guid(ClientAction.InterfaceId)> Public Interface IClientAction
<DispId(1), Description("Make the system raise the event")> sub SendMessage(ByVal theMessage As String)
End Interface
<InterfaceType(ComInterfaceType.InterfaceIsIDispatch), Guid(ClientAction.EventsId)> Public Interface IClientActionEvents
<DispId(1)> Sub TestEvent(ByVal sender As Object, ByVal e As PacketArrivedEventArgs)
End Interface
<ComSourceInterfaces(GetType(IClientActionEvents)), Guid(ClientAction.ClassId), ClassInterface(ClassInterfaceType.None)> _
Public Class ClientAction
Implements IClientAction
Public Delegate Sub TestEventDelegate(ByVal sender As Object, ByVal e As PacketArrivedEventArgs)
Public Event TestEvent As TestEventDelegate
public sub New()
//Init etc
end sub
public sub SendMessage(theMessage as string) implements IClientAction.SendMessage
onSendMessage(theMessage)
end sub
Protected Sub onSendMessage(message as string)
If mRaiseEvents Then
RaiseEvent TestEvent(Me, New PacketArrivedEventArgs(theMessage))
End If
End Sub
end Class
I've been able to get COM and .Net consumers of the Assembly/Component to work properly with events and be able to debug in and out of the component.
Hope this helps.
Just something to try - I have an inherent distrust of "As New .."
Can you try
private m_eventHandler as Collection
public sub InitSomething()
dim handler as EventHandler
set handler = new EventHandler
If m_eventHandler Is Nothing Then
Set m_eventHandler = New Collection
End if
m_eventHandler.Add handler
...
m_engine.Start
end sub
Alas, I've got no idea why this works in normal execution and not in debug except some vague suspicions that it's to do with .NET being unable to instantiate the VBA.Collection object (MS recommends that you write a quick VB6 component to do so), but since you're not creating collections in .NET code, it is still just a vague suspicion.