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

时间:2015-08-12 00:57:15

标签: c# reflection com ms-office vbe

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

代码是用户在标准(.bas)模块中编写的单元测试代码,如下所示:

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<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();
    }
}

像魅力一样工作。我也有WordAppPowerPointAppAccessApp的类似代码。

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

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

This answer指向blog post on MSDN that looks promising的链接,因此我尝试了这个:

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);
    }
}

但是我得到的最好的是COMException表示“未知名称”,OUTLOOK.EXE进程退出代码-1073741819(0xc0000005)“访问冲突” - 它就像爆炸一样很好地使用Excel。

更新

如果我将TestMethod1放在ThisOutlookSession

中,则此VBA代码可以正常运行
Outlook.Application.TestMethod1

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

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

3 个答案:

答案 0 :(得分:5)

更新3:

我在MSDN forums: Call Outlook VBA sub from VSTO找到了这篇文章。

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

  

COMException(0x80040154):检索COM类工厂   CLSID {55F88893-7708-11D1-ACEB-006008961DA5}组件失败到期   出现以下错误:80040154未注册的课程

无论如何,这是家伙的回答谁认为他的工作:

MSDN论坛帖子的开始

  

我找到了办法!什么可以从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:

  

为资源管理器事件创建一个类并捕获此事件:

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论坛帖子结尾

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

更新3a:

80040154未注册的类我得到的是因为尽管针对x86平台我将代码从VSTO VB.Net转换为VBE C#我正在实例化项目,例如:

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

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

enter image description here

VBE C#代码(来自我的回答make a VBE AddIn answer here):

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 VBA单元测试。

我将离线与您谈论为RubberDuck GitHub项目做出贡献,我编写的代码与Prodiance's Workbook Relationship Diagram完全相同,然后Microsoft将其购买并将其产品包含在Office Audit和Version Control Server中

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

ps你会想到以逆时针方向跳到一条腿上,顺时针揉搓我的头部时点击我的手指like Workaround Method 2 in this KB Article我会把它钉在上面... nup我只是丢了更多的头发!

更新2:

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

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

更新1:

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

不幸的是,下面介绍的oApp.GetType().InvokeMember("Run"...)方法适用于所有 Office应用程序(Outlook除外) - 基于此知识库文章中的“属性”部分:https://support.microsoft.com/en-us/kb/306683,抱歉我不知道到目前为止,我发现尝试和the MSDN article misleading非常令人沮丧,最终微软锁定了它:

enter image description here ** 请注意,SendKeys受支持且使用ThisOutlookSession的唯一其他已知方法不是: https://groups.google.com/forum/?hl=en#!topic/microsoft.public.outlook.program_vba/cQ8gF9ssN3g - 即使Sue不是Microsoft PSS she would've asked and found out its unsupported

OLD ...以下方法适用于除Outlook以外的Office应用程序

  

问题是Outlook的Application对象没有公开Run方法,所以我很好,卡住了。这个答案链接到MSDN上看起来很有前途的博客帖子,所以我尝试了这个...但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项目对象模型的宏安全性和信任访问:

enter image description here

答案 1 :(得分:4)

尝试这个帖子,看起来Outlook有所不同,但我想你已经知道了。给出的黑客可能就足够了。

  

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

Social MSDN: < Application.Run > equivalent for Microsoft Outlook

答案 2 :(得分:2)

编辑 - 此方法使用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很难直接调用任何东西......

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

我正在使用ThisOutlookSession,但您可以使用触发TaskItem的{​​{1}}事件的任何Item。同样,我正在使用ApplicationItemLoad属性,但您可以选择不同的属性(实际上,body属性存在问题,因为Outlook似乎添加了空格,但就目前而言,我'处理那个)。

将此代码添加到Subject

Body

然后,创建一个名为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

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

TestModule1

备注

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

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

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

  1. 您可以使用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#\ Rubberduck,然后C#可以通过函数指针调用这些过程,使用类似Win32的TaskItem taskitem = Application.CreateItem(OlItemType.olTaskItem); taskitem.Subject = "Rubberduck"; taskitem.Body = "TestMethod1";

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

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

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

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