23

我正在为 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();
    }
}

奇迹般有效。我也有类似的WordApp,PowerPointApp和代码AccessApp

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


如何从 VBE 的 COM 加载项执行 VBA 代码,而无需Application.Run?

这个答案链接到MSDN 上看起来很有希望的博客文章,所以我尝试了这个:

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 一样好。


更新

如果我把这个 VBA 代码放在TestMethod1里面ThisOutlookSession

Outlook.Application.TestMethod1

请注意,TestMethod1它没有被列为Outlook.ApplicationVBA IntelliSense 的成员。但不知何故,它碰巧起作用了。

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

4

3 回答 3

5

更新 3:

我在MSDN 论坛上找到了这篇文章:Call Outlook VBA sub from VSTO

显然它使用 VSTO,我尝试将其转换为VBE AddIn,但在使用 x64 Windows 时遇到了 Register Class 问题:

COMException (0x80040154):检索具有 CLSID {55F88893-7708-11D1-ACEB-006008961DA5} 的组件的 COM 类工厂失败,原因是以下错误: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:

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

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

更新 3a:

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

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

我将离线与您讨论如何为 RubberDuck GitHub 项目做出贡献,在 Microsoft 购买 Prodiance 的工作簿关系图并将其产品包含在 Office 审计和版本控制服务器中之前,我编写的代码与Prodiance 的工作簿关系图相同。

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

ps 你会想,在逆时针方向跳上一条腿后,点击我的手指,同时顺时针揉我的头,就像这篇知识库文章中的解决方法 2 一样,我会搞定的......我只是失去了更多的头发!


更新 2:

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

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


更新1:

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

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

在此处输入图像描述 ** 请注意,这SendKeys是受支持的,唯一已知的使用ThisOutlookSession方式不是: https ://groups.google.com/forum/?hl=en#!topic/microsoft.public.outlook.program_vba/ cQ8gF9ssN3g - 即使 Sue 不是't Microsoft PSS她会问并发现它不受支持


OLD... 以下方法适用于除 Outlook 之外的 Office Apps

问题是 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 项目对象模型的宏安全和信任访问:

在此处输入图像描述

于 2015-08-25T13:23:56.667 回答
4

编辑 - 这种方法使用 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 似乎只喜欢ProjectName.ModuleName.MethodNameMethodName在分配 OnAction 值时。当它被分配为时它没有执行ModuleName.MethodName

原答案...

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

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

我正在使用TaskItem,但您可能可以使用任何Item触发Application'ItemLoad事件的方法。同样,我正在使用SubjectandBody属性,但您可以选择不同的属性(实际上,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";

笔记

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

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

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

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

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

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

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

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

于 2016-04-27T12:20:36.457 回答
4

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

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

社交 MSDN:<Application.Run> 等效于 Microsoft Outlook

于 2015-08-12T10:16:09.623 回答