Как запустить макрос из надстройки VBE без Application.Run?

Я пишу надстройку COM для VBE, и одна из основных функций заключается в выполнении существующего кода 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 метод, так что я, ну, застрял.


Как я могу выполнить код VBA из надстройки COM для VBE, без 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.Application в VBA IntelliSense.. но как-то это работает.

Вопрос в том, как мне сделать эту работу с Reflection?

3 ответа

Обновление 3:

Я нашел этот пост на форумах MSDN: Позвоните в VBA сабвуфера Outlook из VSTO.

Очевидно, что он использует VSTO, и я попытался преобразовать его в VBE AddIn, но столкнулся с проблемами при работе с 64-разрядной версией Windows с проблемой Register Class:

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:

Создайте класс для событий 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 и хотя бы одного почтового сообщения

Обновление 3а:

Класс 80040154, который я не зарегистрировал, был получен потому, что, несмотря на то, что я нацелен на платформу x86, когда я переводил код из 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
    }
}

Код Form1, который я создаю и загружаю из метода VBE IDE InitializeAddIn():

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, до того как Microsoft выкупила их и включила их продукт в Office Audit и Version Control Server.

Возможно, вы захотите изучить этот код перед тем, как полностью его исключить, я даже не смог заставить работать событие mpubObj_Explorer_BeforeItemCopy, поэтому, если вы сможете нормально работать в Outlook, вам будет лучше. (Я использую Outlook 2013 дома, поэтому 2010 может отличаться).

ps. Можно было бы подумать, прыгнув на одной ноге в направлении против часовой стрелки, щелкнув пальцами, потирая мою голову по часовой стрелке, как метод 2 в этой статье базы знаний, что я бы прибил его... но я просто потерял больше волос!


Обновление 2:

Внутри вашего Outlook.Application.TestMethod1 Вы не можете просто использовать метод CallByName VB классики, так что вам не нужно отражение? Вам нужно было бы установить строковое свойство "Sub/FunctionNameToCall" перед вызовом метода, содержащего CallByName, чтобы указать, какую подчиненную функцию / функцию вызывать.

К сожалению, пользователи должны будут вставить код котельной плиты в один из своих модулей.


Обновление 1:

Это будет звучать очень странно, но, поскольку объектная модель Outlooks полностью закрыла свой метод Run, вы можете прибегнуть к... SendKeys (да, я знаю, но это будет работать).

К сожалению oApp.GetType().InvokeMember("Run"...) Описанный ниже метод работает для всех приложений Office, кроме Outlook, - на основе раздела "Свойства" в этой статье базы знаний: https://support.microsoft.com/en-us/kb/306683, извините, я не знал этого до сих пор и нашел Это очень разочаровывает попытки и вводит в заблуждение статью MSDN, в конечном счете Microsoft заблокировала это:

** Обратите внимание, что SendKeys поддерживается и единственный другой известный способ использования ThisOutlookSession это не: https://groups.google.com/forum/?hl=en#!topic/microsoft.public.outlook.program_vba/cQ8gF9ssN3g - даже если Сью не Microsoft PSS, она бы спросила и выяснила его не поддерживается.


СТАРЫЙ... Приведенный ниже метод работает с Office Apps, за исключением Outlook

Проблема в том, что объект приложения Outlook не предоставляет метод Run, поэтому я застрял. Этот ответ ссылается на сообщение в блоге на MSDN, которое выглядит многообещающим, поэтому я попробовал это... но процесс OUTLOOK.EXE завершается с кодом -1073741819 (0xc0000005) "Нарушение доступа"

Вопрос в том, как мне сделать эту работу с Reflection?

1) Вот код, который я использую, который работает для Excel (должен работать для Outlook точно так же), используя ссылку.Net: Microsoft.Office.Interop.Excel v14 (не ActiveX COM Reference):

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) убедитесь, что вы поместили код макроса в модуль (файл Global BAS).

Public Sub TestMsg()

MsgBox ("Hello Stackru")

End Sub

3) убедитесь, что вы включили Macro Security и Trust доступ к объектной модели проекта VBA:

РЕДАКТИРОВАТЬ - Этот новый подход использует элемент управления 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.MethodName или же MethodName при назначении значения OnAction. Это не выполнялось, когда это было назначено как ModuleName.MethodName

Оригинальный ответ...

УСПЕХ - Кажется, что Outlook VBA и Rubberduck могут общаться друг с другом, но только после того, как Rubberduck может запустить некоторый код VBA для запуска. Но без Application.Runи без каких-либо методов в ThisOutlookSession, имеющих DispID или чего-либо похожего на формальную библиотеку типов, для Rubberduck трудно вызывать что-либо напрямую...

К счастью, Application обработчики событий для ThisOutlookSession позволяют нам запускать событие из C# DLL/Rubberduck, и затем мы можем использовать это событие, чтобы открыть линии связи. И этот метод не требует наличия каких-либо ранее существующих элементов, правил или папок. Это достижимо только путем редактирования VBA.

Я использую TaskItem, но вы могли бы использовать любой Item что вызывает Application"s ItemLoad событие. Аналогично, я использую Subject а также Body атрибуты, но вы можете выбрать другие свойства (на самом деле, атрибут 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";

Заметки

Это доказательство концепции, поэтому я знаю, что есть некоторые проблемы, которые необходимо исправить. Во-первых, любой новый TaskITem, имеющий тему "Rubberduck", будет рассматриваться как полезная нагрузка.

Я использую стандартный класс VBA здесь, но класс можно сделать статическим (путем редактирования атрибутов), и метод CallByName все еще должен работать.

Как только DLL сможет выполнить код VBA таким образом, можно предпринять дальнейшие шаги для усиления интеграции:

  1. Вы можете передать указатели метода обратно в C#\Rubberduck, используя AddressOf оператор, а затем C# может вызывать эти процедуры с помощью указателей на функции, используя что-то вроде Win32 CallWindowProc

  2. Вы можете создать класс VBA с членом по умолчанию, а затем назначить экземпляр этого класса свойству DLL C#, для которого требуется обработчик обратного вызова. (аналогично свойству OnReadyStateChange объекта MSXML2.XMLHTTP60)

  3. Вы можете передавать детали, используя COM-объект, как Rubberduck уже делает с классом Assert.

  4. Я не продумал это, но мне интересно, если вы определили класс VBA с PublicNotCreatable Например, можете ли вы передать это в C#?

И, наконец, хотя это решение включает в себя небольшое количество шаблонного кода, оно должно было бы хорошо работать с любыми существующими обработчиками событий, и я не занимался этим.

Попробуйте эту ветку, похоже, Outlook другой, но я думаю, вы уже это знаете. Взлом дано, может быть, достаточно.

Создайте свой код как Public Subs и поместите код в модуль класса ThisOutlookSession. Затем вы можете использовать Outlook.Application.MySub(), чтобы вызвать вашу подпрограмму MySub. Конечно, измените это на правильное имя.

Социальный MSDN: эквивалент для Microsoft Outlook

Другие вопросы по тегам