如何在没有Application.Run的情况下从VBE加载项运行宏?

我正在为VBE编写COM加载项,其中一个核心function是在单击命令栏按钮时执行现有的VBA代码。

代码是用户在标准(.bas)模块中编写的unit testing代码,其模块如下所示:

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 

所以我有这个代码获取主机Application对象的当前实例:

 protected HostApplicationBase(string applicationName) { Application = (TApplication)Marshal.GetActiveObject(applicationName + ".Application"); } 

这是ExcelApp类:

 public class ExcelApp : HostApplicationBase { 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(); } } 

奇迹般有效。 我也有类似的WordAppPowerPointAppAccessApp代码。

问题是Outlook的Application对象没有暴露Run方法,所以我很好,卡住了。


如何在没有Application.Run情况下从VBE的COM加载项执行VBA代码?

这个答案链接到MSDN上看起来很有前途的博客post ,所以我尝试了这个:

 public class OutlookApp : HostApplicationBase { public OutlookApp() : base("Outlook") { } public override void Run(QualifiedMemberName qualifiedMemberName) { var app = Application.GetType(); app.InvokeMember(qualifiedMemberName.MemberName, BindingFlags.InvokeMethod, null, Application, null); } } 

但是我得到的最好的是一个COMException ,它表示“未知名称”,并且OUTLOOK.EXE进程退出代码-1073741819(0xc0000005)“访问冲突” – 它也与Excel一样好用。


UPDATE

如果我将TestMethod1放在ThisOutlookSession ,这个VBA代码可以工作:

 Outlook.Application.TestMethod1 

请注意, TestMethod1未在VBA IntelliSense中列为Outlook.Application的成员..但不知何故,它恰好起作用。

问题是, 如何使用Reflection进行此工作?

更新3:

我在MSDN论坛上发现了这篇文章:从VSTO调用Outlook VBA sub 。

显然它使用VSTO,我尝试将其转换为VBE AddIn ,但在使用带有注册类问题的x64 Windows时遇到了问题:

COMException(0x80040154):由于以下错误,检索具有CLSID {55F88893-7708-11D1-ACEB-006008961DA5}的组件的COM类工厂失败:80040154类未注册

无论如何,这是谁回答谁认为他得到它的工作:

MSDN论坛post的开始

我找到了一个方法! 什么可以从VSTO和VBA触发? 剪贴板!!

所以我使用剪贴板将消息从一个环境传递到另一个环境。 这里有一些代码可以解释我的诀窍:

VSTO:

 'p_Procedure is the procedure name to call in VBA within Outlook 'mObj_ou_UserProperty is to create a custom property to pass an argument to the VBA procedure Private Sub p_Call_VBA(p_Procedure As String) Dim mObj_of_CommandBars As Microsoft.Office.Core.CommandBars, mObj_ou_Explorer As Outlook.Explorer, mObj_ou_MailItem As Outlook.MailItem, mObj_ou_UserProperty As Outlook.UserProperty mObj_ou_Explorer = Globals.Menu_AddIn.Application.ActiveExplorer 'I want this to run only when one item is selected If mObj_ou_Explorer.Selection.Count = 1 Then mObj_ou_MailItem = mObj_ou_Explorer.Selection(1) mObj_ou_UserProperty = mObj_ou_MailItem.UserProperties.Add("COM AddIn-Azimuth", Outlook.OlUserPropertyType.olText) mObj_ou_UserProperty.Value = p_Procedure mObj_of_CommandBars = mObj_ou_Explorer.CommandBars 'Call the clipboard event Copy mObj_of_CommandBars.ExecuteMso("Copy") End If End Sub 

VBA:

为Explorer事件创建一个类并捕获此事件:

 Public WithEvents mpubObj_Explorer As Explorer 'Trap the clipboard event Copy Private Sub mpubObj_Explorer_BeforeItemCopy(Cancel As Boolean) Dim mObj_MI As MailItem, mObj_UserProperty As UserProperty 'Make sure only one item is selected and of type Mail If mpubObj_Explorer.Selection.Count = 1 And mpubObj_Explorer.Selection(1).Class = olMail Then Set mObj_MI = mpubObj_Explorer.Selection(1) 'Check to see if the custom property is present in the mail selected For Each mObj_UserProperty In mObj_MI.UserProperties If mObj_UserProperty.Name = "COM AddIn-Azimuth" Then Select Case mObj_UserProperty.Value Case "Example_Add_project" '... Case "Example_Modify_planning" '... End Select 'Remove the custom property, to keep things clean mObj_UserProperty.Delete 'Cancel the Copy event. It makes the call transparent to the user Cancel = True Exit For End If Next Set mObj_UserProperty = Nothing Set mObj_MI = Nothing End If End Sub 

MSDN论坛post结束

因此,此代码的作者将一个UserProperty添加到邮件项目并以这种方式传递函数名称。 再次,这将需要Outlook中的一些锅炉板代码和至少1个邮件项目。

更新3a:

没有注册80040154类是因为尽管我将代码从VSTO VB.Net转换为VBE C#,但我正在实例化项目,例如:

 Microsoft.Office.Core.CommandBars mObj_of_CommandBars = new Microsoft.Office.Core.CommandBars(); 

在浪费了几个小时后,我想出了这个代码!

在此处输入图像描述

VBE C#代码(从我的回答中得到一个VBE AddIn答案 ):

 namespace VBEAddin { [ComVisible(true), Guid("3599862B-FF92-42DF-BB55-DBD37CC13565"), ProgId("VBEAddIn.Connect")] public class Connect : IDTExtensibility2 { private VBE _VBE; private AddIn _AddIn; #region "IDTExtensibility2 Members" public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom) { try { _VBE = (VBE)application; _AddIn = (AddIn)addInInst; switch (connectMode) { case Extensibility.ext_ConnectMode.ext_cm_Startup: break; case Extensibility.ext_ConnectMode.ext_cm_AfterStartup: InitializeAddIn(); break; } } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } private void onReferenceItemAdded(Reference reference) { //TODO: Map types found in assembly using reference. } private void onReferenceItemRemoved(Reference reference) { //TODO: Remove types found in assembly using reference. } public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom) { } public void OnAddInsUpdate(ref Array custom) { } public void OnStartupComplete(ref Array custom) { InitializeAddIn(); } private void InitializeAddIn() { MessageBox.Show(_AddIn.ProgId + " loaded in VBA editor version " + _VBE.Version); Form1 frm = new Form1(); frm.Show(); //<-- HERE I AM INSTANTIATING A FORM WHEN THE ADDIN LOADS FROM THE VBE IDE! } public void OnBeginShutdown(ref Array custom) { } #endregion } } 

我实例化并从VBE IDE InitializeAddIn()方法加载的Form1代码:

 namespace VBEAddIn { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Call_VBA("Test"); } private void Call_VBA(string p_Procedure) { var olApp = new Microsoft.Office.Interop.Outlook.Application(); Microsoft.Office.Core.CommandBars mObj_of_CommandBars; Microsoft.Office.Core.CommandBars mObj_of_CommandBars = new Microsoft.Office.Core.CommandBars(); Microsoft.Office.Interop.Outlook.Explorer mObj_ou_Explorer; Microsoft.Office.Interop.Outlook.MailItem mObj_ou_MailItem; Microsoft.Office.Interop.Outlook.UserProperty mObj_ou_UserProperty; //mObj_ou_Explorer = Globals.Menu_AddIn.Application.ActiveExplorer mObj_ou_Explorer = olApp.ActiveExplorer(); //I want this to run only when one item is selected if (mObj_ou_Explorer.Selection.Count == 1) { mObj_ou_MailItem = mObj_ou_Explorer.Selection[1]; mObj_ou_UserProperty = mObj_ou_MailItem.UserProperties.Add("JT", Microsoft.Office.Interop.Outlook.OlUserPropertyType.olText); mObj_ou_UserProperty.Value = p_Procedure; mObj_of_CommandBars = mObj_ou_Explorer.CommandBars; //Call the clipboard event Copy mObj_of_CommandBars.ExecuteMso("Copy"); } } } } 

ThisOutlookSession代码:

 Public WithEvents mpubObj_Explorer As Explorer 'Trap the clipboard event Copy Private Sub mpubObj_Explorer_BeforeItemCopy(Cancel As Boolean) Dim mObj_MI As MailItem, mObj_UserProperty As UserProperty MsgBox ("The mpubObj_Explorer_BeforeItemCopy event worked!") 'Make sure only one item is selected and of type Mail If mpubObj_Explorer.Selection.Count = 1 And mpubObj_Explorer.Selection(1).Class = olMail Then Set mObj_MI = mpubObj_Explorer.Selection(1) 'Check to see if the custom property is present in the mail selected For Each mObj_UserProperty In mObj_MI.UserProperties If mObj_UserProperty.Name = "JT" Then 'Will the magic happen?! Outlook.Application.Test 'Remove the custom property, to keep things clean mObj_UserProperty.Delete 'Cancel the Copy event. It makes the call transparent to the user Cancel = True Exit For End If Next Set mObj_UserProperty = Nothing Set mObj_MI = Nothing End If End Sub 

Outlook VBA方法:

 Public Sub Test() MsgBox ("Will this be called?") End Sub 

很遗憾,我很遗憾地通知你,我的努力没有成功。 也许它确实可以从VSTO(我还没有尝试过)起作用,但在尝试像狗一样取骨之后,我现在愿意放弃!

从来没有作为一种安慰,您可以在此答案的修订历史中找到一个疯狂的想法(它显示了一种模拟Office对象模型的方法)来运行私有参数的Office VBAunit testing。

我将离线与你谈论为RubberDuck GitHub项目做出贡献,我编写的代码与Prodiance的工作簿关系图做同样的事情,然后微软将它们买下并将其产品包含在Office Audit和Version Control Server中。

您可能希望在完全解除此代码之前检查此代码,我甚至无法使mpubObj_Explorer_BeforeItemCopy事件起作用,因此如果您可以在Outlook中正常工作,则可能会更好。 (我在家里使用Outlook 2013,所以2010年可能会有所不同)。

ps你会想到在逆时针方向跳一条腿后,顺时针方向揉搓我的头, 像这篇KB文章中的解决方法2那样点击我的手指,我会把它钉在它上面... nup我只是丢了更多的头发!


更新2:

你的Outlook.Application.TestMethod1里面不能只使用VB经典的CallByName方法,所以你不需要reflection? 在调用包含CallByName的方法之前,您需要设置字符串属性“Sub / FunctionNameToCall”以指定要调用的子/函数。

不幸的是,用户需要在其中一个模块中插入一些锅炉板代码。


更新1:

这听起来真的很狡猾,但是由于Outlook的对象模型完全限制了它的Run方法,你可以诉诸... SendKeys (是的,我知道,但它会起作用)

遗憾的是,下面描述的oApp.GetType().InvokeMember("Run"...)方法适用于除Outlook之外的所有 Office应用程序 - 基于此知识库文章中的“属性”部分: https : //support.microsoft.com/en- us / kb / 306683 ,对不起我直到现在都不知道,并且发现它非常令人沮丧的尝试和MSDN文章误导 ,最终微软锁定了它:

在此处输入图像描述 **请注意,支持SendKeys ,使用ThisOutlookSession的唯一其他已知方式不是: https : ThisOutlookSession ?hl = ThisOutlookSession - 即使苏不是微软PSS, 她会问,并发现它不受支持 。


旧...以下方法适用于Office应用程序,Outlook除外

问题是Outlook的Application对象没有暴露Run方法,所以我很好,卡住了。 这个答案链接到MSDN上看起来很有前途的博客post,所以我尝试了这个...但OUTLOOK.EXE进程退出代码-1073741819(0xc0000005)'访问冲突'

问题是, 如何使用Reflection进行此工作?

1)以下是我使用的代码,适用于Excel(应该适用于Outlook),使用.Net参考: Microsoft.Office.Interop.Excel v14(不是ActiveX COM参考):

 using System; using Microsoft.Office.Interop.Excel; namespace ConsoleApplication5 { class Program { static void Main(string[] args) { RunVBATest(); } public static void RunVBATest() { Application oExcel = new Application(); oExcel.Visible = true; Workbooks oBooks = oExcel.Workbooks; _Workbook oBook = null; oBook = oBooks.Open("C:\\temp\\Book1.xlsm"); // Run the macro. RunMacro(oExcel, new Object[] { "TestMsg" }); // Quit Excel and clean up (its better to use the VSTOContrib by Jake Ginnivan). oBook.Saved = true; oBook.Close(false); System.Runtime.InteropServices.Marshal.ReleaseComObject(oBook); System.Runtime.InteropServices.Marshal.ReleaseComObject(oBooks); System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcel); } private static void RunMacro(object oApp, object[] oRunArgs) { oApp.GetType().InvokeMember("Run", System.Reflection.BindingFlags.Default | System.Reflection.BindingFlags.InvokeMethod, null, oApp, oRunArgs); //Your call looks a little bit wack in comparison, are you using an instance of the app? //Application.GetType().InvokeMember(qualifiedMemberName.MemberName, BindingFlags.InvokeMethod, null, Application, null); } } } } 

2)确保将宏代码放入模块(全局BAS文件)中..

 Public Sub TestMsg() MsgBox ("Hello Stackoverflow") End Sub 

3)确保启用对VBA Project对象模型的宏安全性和信任访问:

在此处输入图像描述

尝试这个线程,看起来Outlook是不同的,但我认为你已经知道了。 给出的黑客可能就足够了。

将代码创建为Public Subs,并将代码放在ThisOutlookSession类模块中。 然后,您可以使用Outlook.Application.MySub()来调用名为MySub的子。 当然要改变正确的名称。

社交MSDN:等效于Microsoft Outlook

编辑 – 这种方法使用CommandBar控件作为代理,避免了对事件和任务的需要,但您可以在下面进一步阅读有关旧方法的更多信息。

 var app = Application; var exp = app.ActiveExplorer(); CommandBar cb = exp.CommandBars.Add("CallbackProxy", Temporary: true); CommandBarControl btn = cb.Controls.Add(MsoControlType.msoControlButton, 1); btn.OnAction = "MyCallbackProcedure"; btn.Execute(); cb.Delete(); 

值得注意的是,Outlook在分配OnAction值时似乎只喜欢ProjectName.ModuleName.MethodNameMethodName 。 它被指定为ModuleName.MethodName时没有执行

原始答案……

成功 – 似乎Outlook VBA和Rubberduck 可以相互通信,但只有在Rubberduck可以触发一些VBA代码运行之后。 但是如果没有 Application.Run ,并且ThisOutlookSession中没有任何方法有DispID或类似于正式类型库的任何东西,那么Rubberduck很难直接调用任何东西……

幸运的是, ThisOutlookSessionApplication事件处理程序允许我们从C#DLL / Rubberduck触发事件,然后我们可以使用该事件打开通信线。 并且,此方法不需要存在任何预先存在的项目,规则或文件夹。 只有编辑VBA才能实现。

我正在使用TaskItem ,但你可以使用任何触发ApplicationItemLoad事件的ItemLoad 。 同样,我正在使用SubjectBody属性,但你可以选择不同的属性(事实上,body属性是有问题的,因为Outlook似乎添加了空格,但是现在,我正在处理它)。

将此代码添加到ThisOutlookSession

 Option Explicit Const RUBBERDUCK_GUID As String = "Rubberduck" Public WithEvents itmTemp As TaskItem Public WithEvents itmCallback As TaskItem Private Sub Application_ItemLoad(ByVal Item As Object) 'Save a temporary reference to every new taskitem that is loaded If TypeOf Item Is TaskItem Then Set itmTemp = Item End If End Sub Private Sub itmTemp_PropertyChange(ByVal Name As String) If itmCallback Is Nothing And Name = "Subject" Then If itmTemp.Subject = RUBBERDUCK_GUID Then 'Keep a reference to this item Set itmCallback = itmTemp End If 'Discard the original reference Set itmTemp = Nothing End If End Sub Private Sub itmCallback_PropertyChange(ByVal Name As String) If Name = "Body" Then 'Extract the method name from the Body Dim sProcName As String sProcName = Trim(Replace(itmCallback.Body, vbCrLf, "")) 'Set up an instance of a class Dim oNamedMethods As clsNamedMethods Set oNamedMethods = New clsNamedMethods 'Use VBA's CallByName method to run the method On Error Resume Next VBA.CallByName oNamedMethods, sProcName, VbMethod On Error GoTo 0 'Discard the item, and destroy the reference itmCallback.Close olDiscard Set itmCallback = Nothing End If End Sub 

然后,创建一个名为clsNamedMethods的类模块,并添加您要调用的命名方法。

  Option Explicit Sub TestMethod1() TestModule1.TestMethod1 End Sub Sub TestMethod2() TestModule1.TestMethod2 End Sub Sub TestMethod3() TestModule1.TestMethod3 End Sub Sub ModuleInitialize() TestModule1.ModuleInitialize End Sub Sub ModuleCleanup() TestModule1.ModuleCleanup End Sub Sub TestInitialize() TestModule1.TestInitialize End Sub Sub TestCleanup() TestModule1.TestCleanup End Sub 

然后在名为TestModule1的标准模块中实现实际方法

 Option Explicit Option Private Module '@TestModule '' uncomment for late-binding: 'Private Assert As Object '' early-binding requires reference to Rubberduck.UnitTesting.tlb: Private Assert As New Rubberduck.AssertClass '@ModuleInitialize Public Sub ModuleInitialize() 'this method runs once per module. '' uncomment for late-binding: 'Set Assert = CreateObject("Rubberduck.AssertClass") End Sub '@ModuleCleanup Public Sub ModuleCleanup() 'this method runs once per module. End Sub '@TestInitialize Public Sub TestInitialize() 'this method runs before every test in the module. End Sub '@TestCleanup Public Sub TestCleanup() 'this method runs afer every test in the module. End Sub '@TestMethod Public Sub TestMethod1() 'TODO Rename test On Error GoTo TestFail 'Arrange: 'Act: 'Assert: Assert.AreEqual True, True TestExit: Exit Sub TestFail: Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description End Sub '@TestMethod Public Sub TestMethod2() '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 '@TestMethod Public Sub TestMethod3() 'TODO Rename test On Error GoTo TestFail 'Arrange: 'Act: 'Assert: Assert.Fail TestExit: Exit Sub TestFail: Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description End Sub 

然后,从C#代码,您可以使用以下命令触发Outlook VBA代码:

 TaskItem taskitem = Application.CreateItem(OlItemType.olTaskItem); taskitem.Subject = "Rubberduck"; taskitem.Body = "TestMethod1"; 

笔记

这是一个概念certificate,所以我知道有一些问题需要整理。 首先,任何具有“Rubberduck”主题的新TaskITem将被视为有效载荷。

我在这里使用标准的VBA类,但是可以使类成为静态(通过编辑属性),并且CallByName方法仍然可以工作。

一旦DLL能够以这种方式执行VBA代码,就可以采取进一步的步骤来加强集成:

  1. 您可以使用AddressOf运算符将方法指针传递回C#\ Rubberduck,然后C#可以通过函数指针调用这些过程,使用类似Win32的CallWindowProc

  2. 您可以使用默认成员创建VBA类,然后将该类的实例分配给需要回调处理程序的C#DLL属性。 (类似于MSXML2.XMLHTTP60对象的OnReadyStateChange属性)

  3. 您可以使用COM对象传递详细信息,例如Rubberduck已经在使用Assert类。

  4. 我没有想过这个,但我想知道你是否用PublicNotCreatable实例化定义了一个VBA类,你是否可以将它传递给C#?

最后,虽然这个解决方案确实涉及少量样板,但它必须与任何现有的事件处理程序一起使用,我还没有处理过。