Как запустить макрос из надстройки 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 таким образом, можно предпринять дальнейшие шаги для усиления интеграции:
Вы можете передать указатели метода обратно в C#\Rubberduck, используя
AddressOf
оператор, а затем C# может вызывать эти процедуры с помощью указателей на функции, используя что-то вроде Win32CallWindowProc
Вы можете создать класс VBA с членом по умолчанию, а затем назначить экземпляр этого класса свойству DLL C#, для которого требуется обработчик обратного вызова. (аналогично свойству OnReadyStateChange объекта MSXML2.XMLHTTP60)
Вы можете передавать детали, используя COM-объект, как Rubberduck уже делает с классом Assert.
Я не продумал это, но мне интересно, если вы определили класс VBA с
PublicNotCreatable
Например, можете ли вы передать это в C#?
И, наконец, хотя это решение включает в себя небольшое количество шаблонного кода, оно должно было бы хорошо работать с любыми существующими обработчиками событий, и я не занимался этим.
Попробуйте эту ветку, похоже, Outlook другой, но я думаю, вы уже это знаете. Взлом дано, может быть, достаточно.
Создайте свой код как Public Subs и поместите код в модуль класса ThisOutlookSession. Затем вы можете использовать Outlook.Application.MySub(), чтобы вызвать вашу подпрограмму MySub. Конечно, измените это на правильное имя.