I'm writing a COM add-in for the VBE, and one of the core features involves executing existing VBA code upon clicking a commandbar button.
The code is unit testing code written by the user, in a standard (.bas) module that looks something like this:
Option Explicit Option Private Module '@TestModule Private Assert As New Rubberduck.AssertClass '@TestMethod Public Sub TestMethod1() 'TODO: Rename test On Error GoTo TestFail 'Arrange: 'Act: 'Assert: Assert.Inconclusive TestExit: Exit Sub TestFail: Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description End Sub
So I have this code that gets the current instance of the host Application
object:
protected HostApplicationBase(string applicationName)
{
Application = (TApplication)Marshal.GetActiveObject(applicationName + ".Application");
}
Here's the ExcelApp
class:
public class ExcelApp : HostApplicationBase<Microsoft.Office.Interop.Excel.Application>
{
public ExcelApp() : base("Excel") { }
public override void Run(QualifiedMemberName qualifiedMemberName)
{
var call = GenerateMethodCall(qualifiedMemberName);
Application.Run(call);
}
protected virtual string GenerateMethodCall(QualifiedMemberName qualifiedMemberName)
{
return qualifiedMemberName.ToString();
}
}
Works like a charm. I have similar code for WordApp
, PowerPointApp
and AccessApp
, too.
The problem is that Outlook's Application
object doesn't expose a Run
method, so I'm, well, stuck.
How can I execute VBA code from a COM add-in for the VBE, without Application.Run
?
This answer links to a blog post on MSDN that looks promising, so I tried this:
public class OutlookApp : HostApplicationBase<Microsoft.Office.Interop.Outlook.Application>
{
public OutlookApp() : base("Outlook") { }
public override void Run(QualifiedMemberName qualifiedMemberName)
{
var app = Application.GetType();
app.InvokeMember(qualifiedMemberName.MemberName, BindingFlags.InvokeMethod, null, Application, null);
}
}
But then the best I'm getting is a COMException
that says "unknown name", and the OUTLOOK.EXE process exiting with code -1073741819 (0xc0000005) 'Access violation' - and it blows up just as nicely with Excel, too.
UPDATE
This VBA code works, if I put TestMethod1
inside ThisOutlookSession
:
Outlook.Application.TestMethod1
Note that TestMethod1
isn't listed as a member of Outlook.Application
in VBA IntelliSense.. but somehow it happens to work.
The question is, how do I make this work with Reflection?
Update 3:
I found this post on MSDN forums: Call Outlook VBA sub from VSTO.
Obviously it uses VSTO and I tried converting it to a VBE AddIn, but ran into issues at work with x64 Windows with a Register Class issue:
Anyway this is the guys answer who reckons he got it working:
Start Of MSDN Forum Post
VSTO:
VBA:
End Of MSDN Forum Post
So the author of this code adds a UserProperty to a mail item and passes the function name that way. Again this would require some boiler plate code in Outlook and at least 1 mail item.
Update 3a:
The 80040154 Class not registered I was getting was because despite targeting x86 platform when I translated the code from VSTO VB.Net to VBE C# I was instantiating items, eg:
After wasting several more hours on it, I came up with this code, that ran!!!
The VBE C# Code (from my answer make a VBE AddIn answer here):
The Form1 code that I instantiate and load from the VBE IDE InitializeAddIn() method:
The ThisOutlookSession Code:
The Outlook VBA Method:
Very sadly, I regret to inform you that my efforts were unsuccessful. Maybe it does work from VSTO (I haven't tried) but after trying like a dog fetching a bone, I am now willing to give up!
Never the less as a consolation you can find a crazy idea in the Revision History of this answer (it shows a way of Mocking an Office Object Model) to run Office VBA unit tests that are private with parameters.
I will speak to you offline about contributing to the RubberDuck GitHub project, I wrote code that does the same thing as Prodiance's Workbook Relationship Diagram before Microsoft bought them out and included their product in Office Audit and Version Control Server.
You may wish to examine this code before dismissing it entirely, I couldn't even get the mpubObj_Explorer_BeforeItemCopy event to work, so if you can get that working normally in Outlook you might fare better. (I'm using Outlook 2013 at home, so 2010 might be different).
ps You would think after hopping on one leg in an anti-clockwise direction, clicking my fingers while rubbing my head clockwise like Workaround Method 2 in this KB Article that I would have nailed it... nup I just lost more hair!
Update 2:
Inside your
Outlook.Application.TestMethod1
can't you just use VB classics CallByName method so you dont need reflection? You'd need to set a string property "Sub/FunctionNameToCall" before calling the method containing the CallByName to specify what sub/function to call.Unfortunately users would be required to insert some boiler plate code in one of their Module's.
Update 1:
This is going to sound really dodgy, but since Outlooks' object model has fully clamped down its Run method you could resort to...
SendKeys
(yeah I know, but it will work).Unfortunately the
oApp.GetType().InvokeMember("Run"...)
method described below works for all Office Apps except Outlook - based on the Properties section in this KB Article: https://support.microsoft.com/en-us/kb/306683, sorry I didn't know that until now and found it very frustrating trying and the MSDN article misleading, ultimately Microsoft has locked it:** Note that
SendKeys
is supported and the only other known way usingThisOutlookSession
is not: https://groups.google.com/forum/?hl=en#!topic/microsoft.public.outlook.program_vba/cQ8gF9ssN3g - even though Sue isn't Microsoft PSS she would've asked and found out its unsupported.OLD... The below method works with Office Apps except for Outlook
1) Here is the code I use that works for Excel (should work for Outlook just the same), using the .Net reference: Microsoft.Office.Interop.Excel v14 (not the ActiveX COM Reference):
2) make sure you put the Macro code in a Module (a Global BAS file)..
3) make sure you enable Macro Security and Trust access to the VBA Project object model:
Try this thread, it looks like Outlook is different but then I think you know this already. The hack given maybe sufficient.
Social MSDN: < Application.Run > equivalent for Microsoft Outlook
EDIT - This new approach uses a CommandBar control as a proxy and avoids the need for events and tasks, but you can read more about the old approach further below.
It's worth noting that Outlook seems to only like
ProjectName.ModuleName.MethodName
orMethodName
when assigning the OnAction value. It didn't execute when it was assigned asModuleName.MethodName
Original Answer...
SUCCESS - It seems that Outlook VBA and Rubberduck can talk to each other, but only after Rubberduck can trigger some VBA code to run. But without
Application.Run
, and without any methods in ThisOutlookSession having DispIDs or anything that resembles a formal Type Library, it's hard for Rubberduck to call anything directly...Fortunately, the
Application
event handlers forThisOutlookSession
allow us to trigger an event from a C# DLL/Rubberduck, and we can then use that event to open up the lines of communication. And, this method doesn't require the presence of any pre-existing items, rules or folders. It's achievable solely by editing the VBA.I'm using a
TaskItem
, but you could probably use anyItem
that triggers theApplication
'sItemLoad
event. Likewise, I'm using theSubject
andBody
attributes, but you could choose different properties (in fact, the body attribute is problematic because Outlook seems to add white-space, but for now, I'm handling that).Add this code to
ThisOutlookSession
Then, create a class module called
clsNamedMethods
and add the named methods you'd like to call.And then implement the real methods in a standard module called
TestModule1
Then, from the C# code, you can trigger the Outlook VBA code with:
Notes
This is a proof of concept, so I know there are some issues that need to be tidied up. For one, any new TaskITem that has a Subject of "Rubberduck" is going to be treated as a payload.
I'm using a standard VBA class here, but the class could be made to be static (by editing the attributes), and the CallByName method should still work.
Once the DLL is able to execute VBA code in this manner, there are further steps that can be taken to tighten the integration:
You could pass method pointers back to C#\Rubberduck using the
AddressOf
operator, and then C# could call those procedures by their function pointers, using something like Win32'sCallWindowProc
You could create a VBA class with a default member, and then assign an instance of that class to a C# DLL property that requires a callback handler. (similar to the OnReadyStateChange property of the MSXML2.XMLHTTP60 object)
You could pass details using a COM object, like Rubberduck is already doing with the Assert class.
I haven't thought this one through, but I wonder if you defined a VBA class with
PublicNotCreatable
instancing, whether you could then pass that to C#?And finally, while this solution does involve a small amount of boilerplate, it would have to play nice with any existing event handlers, and I haven't dealt with that.