Raising events in a class library exposed to COM - vb.net

I'm trying to write a wrapper to a service, which will be used by an existing VB6 project. I've got most of the basic framework working, except for one important aspect: I can reference the wrapper in a VB6 project and subs/function calls etc. work as expected, but events do not. The events are visible in the VB6 app, but they never fire.
VB.NET Code:
Public Event Action_Response(ByVal Status as String)
Public Function TestEvent()
RaiseEvent Action_Response("Test Done")
Return "Done"
End Function
VB6 Code:
Dim WithEvents my_Wrapper as Wrapper_Class
Private Sub cmdTest_Click()
Set my_Wrapper = New Wrapper_Class
Debug.Print my_Wrapper.TestEvent()
End Sub
Private Sub my_Wrapper_Action_Response(ByVal Status As String)
Debug.Print Status
Set my_Wrapper = Nothing
End Sub
So, the cmdTest button code prints 'Done' as expected, but the Action_Response event doesn't fire. Is there something else do I need to do to get the event to fire?

Its too much to write in a comment, so I'll make it an answer....
First, identify the .net class you want to expose to COM. I'll pick a class called CORE.
Create an interface that describes the EVENTS that the CORE object will source (ie generate).
<ComVisible(True)> _
<Guid("some guid here...use guidgen, I'll call it GUID1")> _
<InterfaceType(ComInterfaceType.InterfaceIsIDispatch)> _
Public Interface ICoreEvents
<System.Runtime.InteropServices.DispId(1)> _
Sub FileLoaded(ByVal Message As String)
End Interface
Next, Create an interface for the COM exposed properties and methods of your class.
<ComVisible(True)> _
<Guid("another GUID, I'll call it guid2")> _
<InterfaceType(ComInterfaceType.InterfaceIsDual)> _
Public Interface ICore
ReadOnly Property Property1() As Boolean
ReadOnly Property AnotherProperty() As ISettings
ReadOnly Property Name() As String
ReadOnly Property Phone() As String
End Interface
Now, create your actual .net class
<ComVisible(True)> _
<ClassInterface(ClassInterfaceType.None)> _
<ComDefaultInterface(GetType(ICore))> _
<ComSourceInterfaces(GetType(ICoreEvents))> _
<Guid("a third GUID, I'll call it GUID3")> _
Public Class Core
Implements ICore
<System.Runtime.InteropServices.ComVisible(False)> _
Public Delegate Sub OnFileLoaded(ByVal Message As String)
Public Event FileLoaded As OnFileLoaded
End Class
Now, when you need to raise the FileLoaded event, just RAISEEVENT FILELOADED(Message) from within your class. .NET will forward the event out to COM because you've hooked up the COMSourceInterfaces attribute.
The attribute is shorthand for much of of this, but unfortunately doesn't give you quite the control that you need to do certain things (for instance retain version compatibility on your com interfaces).

Related

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

Managed method for SetParent() on form

How can I show a form as a child of a window that isn't in my program?
I have a window handle to what should be the parent, but I don't see any managed method for SetParent() on a form. Is there one? It also seems that the form.Show() method only accepts managed objects implementing IWin32Window.
If there isn't a managed method, what is the preferred method for declaring the API for maximum compatibility with future systems? Like this?:
<DllImport("user32.dll")> _
Private Shared Function SetParent(hWndChild As IntPtr, hWndNewParent As IntPtr) As IntPtr
End Function
Is it possible to build a class that implements IWin32Window and somehow wraps up a window? It would be handy do something like this, but I am not familiar with IWin32Window:
frmMyForm.Show(New NativeWindowWrapper(12345)) 'Where 12345 is the hWnd of the window I want to wrap
Oh wow, I just found the documentation on IWin32Window, and see that it is only one property... Handle. Yes, then of course I can easily make this NativeWindowWrapper class...
I haven't tested it yet, but I am sure it will work just fine...
Public Class NativeWindowWrapper
Implements IWin32Window
Private _Handle As IntPtr
Public ReadOnly Property Handle As System.IntPtr Implements System.Windows.Forms.IWin32Window.Handle
Get
Return _Handle
End Get
End Property
Public Sub New(ByVal Handle As IntPtr)
Me._Handle = Handle
End Sub
End Class

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.

Unit Testing of .NET Add-In for Microsoft Office

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.

How to Access Custom Add-In Ribbon Check Box in VBA?

I've spent 2 days now trying and searching and nothing seems to be working...
I created a custom ribbon Add-In for Visio in VSTO that installs and buttons work fine. I just recently added a couple of checkboxes to the ribbon whose states I want to read from within a VBA project.
I can't for the life of me figure out how to access the checkbox state in VBA. I tried a bunch of things with CommandBars and ToolBars but got nowhere and then I found this walkthrough which seemed promising and followed it to make the Add-In's methods visible to VBA: https://msdn.microsoft.com/en-us/library/bb608614
The VBA code does recognize the add-in and I assign the add-in object but when I try to call the object's function (getIOPressedState which refers to the state of one of the checkboxes), I get "object doesn't support this property or method".
Am I missing something here??
This is my ribbon class I want to make visible
<ComVisible(True)> _
Public Interface IAddInUtilities
Function getIOPressed() As Boolean
Function getDDPressed() As Boolean
Sub doNothing()
End Interface
<Runtime.InteropServices.ComVisible(True)> _
<ClassInterface(ClassInterfaceType.None)> _
Public Class StructuredAnalysisRibbon
Implements Office.IRibbonExtensibility, IAddInUtilities
Public ioPressedState As Boolean = False
Public ddPressedState As Boolean = False
Public ribbon As Office.IRibbonUI
Public Function GetCustomUI(ByVal ribbonID As String) As String Implements Office.IRibbonExtensibility.GetCustomUI
Return getResourceText("SAVisioAddIn.StructuredAnalysisRibbon.xml")
End Function
Public Function getIOPressed() As Boolean Implements IAddInUtilities.getIOPressed
Return ioPressedState
End Function
Public Function getDDPressed() As Boolean Implements IAddInUtilities.getDDPressed
Return ddPressedState
End Function
Public Sub doNothing() Implements IAddInUtilities.doNothing
'do nothing-added this to see if function As boolean in interface was causing issues
End Sub
ThisAddIn.vb
Public SARibbon As StructuredAnalysisRibbon
Protected Overrides Function CreateRibbonExtensibilityObject() As Microsoft.Office.Core.IRibbonExtensibility
Return SARibbon
End Function
Protected Overrides Function RequestComAddInAutomationService() As Object
If SARibbon Is Nothing Then
SARibbon = New StructuredAnalysisRibbon
End If
Return SARibbon
End Function
Visio VBA Code
Public Sub bloop()
Dim addIn As COMAddIn
Dim addInObject As Object
Dim ioPressed As Boolean
ioPressed = False
Set addIn = Application.COMAddIns.Item("SAVisioAddIn")
Set addInObject = addIn.Object
ioPressed = addInObject.getIOPressed 'fails here bc method not recognized for object
'Also tried addIn.Object.doNothing and still didn't work
If ioPressed = True Then
MsgBox "checked"
Else
MsgBox "not checked"
End If
End Sub
I think the problem has nothing to do with checkboxes, the point is VBA by default returns you the default object interface (which in your code is NOT the IAddInUtilities). Just swap the interfaces. The IAddInUtilities should be default (first). Or remove IAddInUtilities at all, along with fancy COM stuff like ClassInterface(ClassInterfaceType.None) which is considered harmful :) Anyways, the easiest may be:
Implements IAddInUtilities, Office.IRibbonExtensibility