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

Mat*_*don 22 c# com reflection 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
Run Code Online (Sandbox Code Playgroud)

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

protected HostApplicationBase(string applicationName)
{
    Application = (TApplication)Marshal.GetActiveObject(applicationName + ".Application");
}
Run Code Online (Sandbox Code Playgroud)

这是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();
    }
}
Run Code Online (Sandbox Code Playgroud)

奇迹般有效.我有类似的代码WordApp,PowerPointAppAccessApp也.

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


如何从VB的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,我得到的最好的是一个COMException说"未知名称",OUTLOOK.EXE进程退出代码-1073741819(0xc0000005)'访问违规' - 它也与Excel一样好用.


UPDATE

如果我放入TestMethod1内部,这个VBA代码可以工作ThisOutlookSession:

Outlook.Application.TestMethod1
Run Code Online (Sandbox Code Playgroud)

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

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

Jer*_*son 5

更新3:

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

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

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

MSDN论坛帖子结束

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

更新3a:

没有注册80040154类是因为尽管我将代码从VSTO VB.Net转换为VBE C#而我的目标是x86平台,我实例化了项目,例如:

Microsoft.Office.Core.CommandBars mObj_of_CommandBars = new Microsoft.Office.Core.CommandBars();
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述

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
    }
}
Run Code Online (Sandbox Code Playgroud)

我实例化并从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");
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

Outlook VBA方法:

Public Sub Test()
MsgBox ("Will this be called?")
End Sub
Run Code Online (Sandbox Code Playgroud)

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

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

我将在线下与您讨论为RubberDuck GitHub项目做出贡献,我编写的代码与Prodiance的工作簿关系图表相同,然后Microsoft将其购买并将其产品包含在Office Audit和Version Control Server中.

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

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


更新2:

在你的内部你Outlook.Application.TestMethod1不能只使用VB经典的CallByName方法,所以你不需要反射?在调用包含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: //groups.google.com/forum/?hl = en#!topic/microsoft.public.outlook.program_vba ​​/ cQ8gF9ssN3g - 即使Sue isn微软PSS 她会问,并发现它不受支持.


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

问题是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);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)

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

Public Sub TestMsg()

MsgBox ("Hello Stackoverflow")

End Sub
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述


S M*_*den 4

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

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

社交 MSDN:<Application.Run> 相当于 Microsoft Outlook