Подтвердить что ты не робот

Как запустить макрос из надстройки VBE без приложения. 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

Итак, у меня есть этот код, который получает текущий экземпляр объекта host 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 тоже.


UPDATE

Этот код VBA работает, если я помещаю TestMethod1 внутрь ThisOutlookSession:

Outlook.Application.TestMethod1

Обратите внимание, что TestMethod1 не указан как член Outlook.Application в VBA IntelliSense.. но как-то это срабатывает.

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

4b9b3361

Ответ 1

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

Я нашел этот пост на Форумы MSDN: Вызовите Outlook VBA sub из VSTO.

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

COMException (0x80040154): получение COM-класса factory для компонент с CLSID {55F88893-7708-11D1-ACEB-006008961DA5} не удалось выполнить к следующей ошибке: 80040154 Класс не зарегистрирован

В любом случае это ребята отвечают, кто считает, что он заработал:

Начало публикации форума MSDN

Я нашел способ! Что может быть вызвано как VSTO, так и VBA? Clipboard!!

Поэтому я использовал буфер обмена для передачи сообщений из одной среды в Другие. Вот несколько кодов, которые объяснят мой трюк:

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:

Я получаю это потому, что, несмотря на таргетинг на платформу x86, когда я переводил код с VSTO VB.Net на VBE С#, я создавал объекты, например:

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

Потеряв еще несколько часов, я придумал этот код, который побежал!!!

введите описание изображения здесь

Код VBE С# (из моего ответа сделайте ответ 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, который я создаю и загружаю из метода InitializeAddIn() VBE IDE:

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

Метод VBA Outlook:

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

К сожалению, я с сожалением сообщаю вам, что мои усилия не увенчались успехом. Может быть, это работает от VSTO (я не пробовал), но, попробовав, как собака, достающая кость, теперь я готов отказаться!

Тем не менее, как утешение, вы можете найти сумасшедшую идею в истории изменений этого ответа (он показывает способ Mocking для объектной модели Office) для запуска модульных тестов Office VBA, которые являются частными с параметрами.

Я поговорю с вами в автономном режиме о том, чтобы внести свой вклад в проект RubberDuck GitHub, я написал код, который делает то же самое, что Диаграмма взаимоотношений рабочей группы Prodiance прежде чем Microsoft купила их и включила их продукт в Office Audit и Version Control Server.

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

ps Вы могли бы подумать после прыжка на одной ноге в направлении против часовой стрелки, щелкая пальцами, потирая мою голову по часовой стрелке как метод Обходной путь 2 в этом KB Статья, что я бы прибил ее... nup Я просто потерял больше волос!


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

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

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


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

Это будет звучать действительно изворотливо, но поскольку объектная модель Outlook полностью зажала свой метод 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 она бы "спросил и узнал его неподдерживаемый.


OLD... Метод ниже работает с приложениями Office, за исключением Outlook

Проблема заключается в том, что объект Outlook Application не предоставляет метод 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) убедитесь, что вы поместили код макроса в модуль (глобальный файл BAS).

Public Sub TestMsg()

MsgBox ("Hello Stackoverflow")

End Sub

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

enter image description here

Ответ 2

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

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

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

Ответ 3

EDIT - этот новый подход использует элемент управления 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();

Стоит отметить, что при присвоении значения OnAction Outlook выглядит как ProjectName.ModuleName.MethodName или MethodName. Он не выполнялся, когда он был назначен как ModuleName.MethodName

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

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

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

Я использую TaskItem, но вы, вероятно, можете использовать любой Item, который запускает событие Application 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 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

Затем из кода С# вы можете вызвать код Outlook VBA с помощью:

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

Примечания

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

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

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

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

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

  • Вы можете передавать данные с помощью COM-объекта, например Rubberduck уже с классом Assert.

  • Я не думал об этом, но мне интересно, определил ли вы класс VBA с помощью PublicNotCreatable instancing, можете ли вы передать это на С#?

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