c# reflection com ms-office vbe

c# - ¿Cómo puedo ejecutar una macro desde un complemento VBE, sin Application.Run?



reflection ms-office (3)

Prueba este hilo, parece que Outlook es diferente, pero creo que ya lo sabes. El hack dado tal vez sea suficiente.

Cree su código como Public Subs y coloque el código en el módulo de la clase ThisOutlookSession. Luego puede usar Outlook.Application.MySub () para llamar a su sub nombre llamado MySub. Por supuesto cambiar eso por el nombre correcto.

Social MSDN: <Application.Run> equivalente para Microsoft Outlook

Estoy escribiendo un complemento COM para el VBE, y una de las funciones principales consiste en ejecutar el código VBA existente al hacer clic en el botón de la barra de comandos.

El código es un código de prueba de unidad escrito por el usuario, en un módulo estándar (.bas) que se parece a esto:

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

Así que tengo este código que obtiene la instancia actual del objeto de Application host:

protected HostApplicationBase(string applicationName) { Application = (TApplication)Marshal.GetActiveObject(applicationName + ".Application"); }

Aquí está la clase de 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(); } }

Funciona de maravilla. También tengo un código similar para WordApp , PowerPointApp y AccessApp .

El problema es que el objeto de Application de Outlook no expone un método de Run , por lo tanto, estoy atascado.

¿Cómo puedo ejecutar el código VBA desde un complemento COM para el VBE, sin Application.Run ?

Esta respuesta se vincula a una publicación de blog en MSDN que parece prometedora , así que intenté esto:

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); } }

Pero lo mejor que COMException es una COMException que dice "nombre desconocido", y el proceso OUTLOOK.EXE sale con el código -1073741819 (0xc0000005) ''Violación de acceso'', y también funciona con Excel.

ACTUALIZAR

Este código VBA funciona, si pongo ThisOutlookSession dentro de ThisOutlookSession :

Outlook.Application.TestMethod1

Tenga en cuenta que TestMethod1 no aparece como miembro de Outlook.Application en VBA IntelliSense .. pero de alguna manera funciona.

La pregunta es, ¿cómo puedo hacer este trabajo con la reflexión?


Actualización 3:

Encontré esta publicación en los foros de MSDN: llame a Outlook VBA sub desde VSTO .

Obviamente, utiliza VSTO y traté de convertirlo a un complemento de VBE , pero tuve problemas en el trabajo con Windows x64 con un problema de clase de registro:

COMException (0x80040154): error al recuperar la fábrica de clases COM para el componente con CLSID {55F88893-7708-11D1-ACEB-006008961DA5} debido al siguiente error: 80040154 Clase no registrada

De todos modos, esta es la respuesta de los chicos que cree que funcionó:

Inicio de la publicación en el foro de MSDN

¡Encontre un camino! ¿Qué podría activarse tanto de VSTO como de VBA? El Portapapeles !!

Así que utilicé el portapapeles para pasar mensajes de un entorno a otro. Aquí hay algunos códigos que explicarán mi truco:

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:

Cree una clase para los eventos de Explorer y atrape este evento:

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

Fin de la publicación en el foro de MSDN

Así que el autor de este código agrega una propiedad de usuario a un elemento de correo y pasa el nombre de la función de esa manera. De nuevo, esto requeriría un código de placa de caldera en Outlook y al menos 1 elemento de correo.

Actualización 3a:

La clase 80040154 no registrada que estaba recibiendo era porque a pesar de apuntar a la plataforma x86 cuando traduje el código de VSTO VB.Net a VBE C # I estaba creando una instancia de elementos, por ejemplo:

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

Después de gastar varias horas más en esto, se me ocurrió este código, ¡que se ejecutó!

El código VBE C # (de mi respuesta, haga una respuesta VBE AddIn aquí ):

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 } }

El código Form1 que instalo y cargo desde el método 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"); } } } }

El código de esta sesión de sesión:

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

El método de Outlook VBA:

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

Lamentablemente, lamento informarle que mis esfuerzos no tuvieron éxito. Tal vez funcione con VSTO (no lo he intentado), pero después de intentar como un perro recogiendo un hueso, ¡ahora estoy dispuesto a rendirme!

Sin embargo, como consuelo, puede encontrar una idea loca en el Historial de revisiones de esta respuesta (muestra una manera de burlarse de un modelo de objetos de Office) para ejecutar pruebas de unidades de VBA de Office que son privadas con parámetros.

Le hablaré sin conexión sobre la contribución al proyecto RubberDuck GitHub, escribí un código que hace lo mismo que el Diagrama de relaciones del libro de Prodiance antes de que Microsoft los comprara e incluyera su producto en Office Audit y Version Control Server.

Es posible que desee examinar este código antes de descartarlo por completo, ni siquiera pude hacer que funcionara el evento mpubObj_Explorer_BeforeItemCopy, por lo que si puede hacer que funcione normalmente en Outlook, es posible que le vaya mejor. (Estoy usando Outlook 2013 en casa, por lo que 2010 podría ser diferente).

Pensarías que después de saltar sobre una pierna en sentido contrario a las agujas del reloj, al hacer clic en mis dedos mientras frotaba mi cabeza en el sentido de las agujas del reloj como en el Método 2 en este artículo de KB , lo habría clavado ... ¡hasta que acabo de perder más cabello!

Actualización 2:

Dentro de su aplicación Outlook.Application.TestMethod1 ¿no puede usar el método clásico de VB CallName de VB para que no necesite reflexión? Necesitaría establecer una propiedad de cadena "Sub / FunctionNameToCall" antes de llamar al método que contiene el CallByName para especificar a qué sub / function llamar.

Desafortunadamente, los usuarios deberán insertar algún código de placa de caldera en uno de sus módulos.

Actualización 1:

Esto va a sonar realmente poco fiable, pero como el modelo de objetos de Outlook ha restringido completamente su método de ejecución, podría recurrir a ... SendKeys (sí, lo sé, pero funcionará) .

Desafortunadamente, el oApp.GetType().InvokeMember("Run"...) que se describe a continuación funciona para todas las aplicaciones de Office excepto para Outlook, según la sección de propiedades de este artículo de KB: https://support.microsoft.com/en-us/kb/306683 , lo siento, no lo sabía hasta ahora y me resultó muy frustrante intentarlo y el artículo de MSDN es engañoso . En última instancia, Microsoft lo ha bloqueado:

** Tenga en cuenta que SendKeys es compatible y la única otra forma conocida de usar ThisOutlookSession no es: https://groups.google.com/forum/?hl=en#!topic/microsoft.public.outlook.program_vba/cQ8gF9ssN3g - aunque Sue no es Microsoft PSS , habría preguntado y descubierto que no está respaldado .

ANTIGUO ... El método siguiente funciona con las aplicaciones de Office, a excepción de Outlook

El problema es que el objeto de aplicación de Outlook no expone un método de ejecución, por lo tanto, estoy atascado. Esta respuesta se vincula a una publicación de blog en MSDN que parece prometedora, así que intenté esto ... pero el proceso OUTLOOK.EXE se cierra con el código -1073741819 (0xc0000005) ''Violación de acceso''

La pregunta es, ¿cómo puedo hacer este trabajo con la reflexión?

1) Este es el código que uso que funciona para Excel (debería funcionar para Outlook de la misma manera), usando la referencia .Net: Microsoft.Office.Interop.Excel v14 (no la referencia COM ActiveX):

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) asegúrese de colocar el código de macro en un módulo (un archivo Global BAS).

Public Sub TestMsg() MsgBox ("Hello ") End Sub

3) asegúrese de habilitar el acceso de seguridad y confianza de macros al modelo de objetos del proyecto VBA:


EDITAR: este nuevo enfoque utiliza un control CommandBar como proxy y evita la necesidad de eventos y tareas, pero puede leer más sobre el enfoque anterior más adelante.

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();

Vale la pena señalar que a Outlook parece que solo le gusta ProjectName.ModuleName.MethodName o MethodName cuando asigna el valor de OnAction. No se ejecutó cuando se asignó como ModuleName.MethodName

Respuesta original ...

ÉXITO - Parece que Outlook VBA y Rubberduck pueden comunicarse entre sí, pero solo después de que Rubberduck pueda activar algún código VBA para ejecutarse. Pero sin Application.Run , y sin ningún método en ThisOutlookSession con DispIDs o cualquier cosa que se parezca a una biblioteca de tipos formal, es difícil para Rubberduck llamar directamente a cualquier cosa ...

Afortunadamente, los controladores de eventos de la Application para ThisOutlookSession nos permiten activar un evento desde un C # DLL / Rubberduck, y luego podemos usar ese evento para abrir las líneas de comunicación. Y, este método no requiere la presencia de elementos, reglas o carpetas preexistentes. Es alcanzable únicamente mediante la edición de la VBA.

Estoy usando un TaskItem , pero probablemente podrías usar cualquier Item que ItemLoad evento ItemLoad la Application . Del mismo modo, estoy usando los atributos de Subject y Body , pero podría elegir diferentes propiedades (de hecho, el atributo de cuerpo es problemático porque Outlook parece agregar espacios en blanco, pero por ahora, estoy manejando eso).

Agregue este código a 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

Luego, cree un módulo de clase llamado clsNamedMethods y agregue los métodos nombrados a los que desea llamar.

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

Y luego implemente los métodos reales en un módulo estándar llamado 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

Luego, desde el código C #, puede activar el código VBA de Outlook con:

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

Notas

Esta es una prueba de concepto, así que sé que hay algunos problemas que deben ser resueltos. Por un lado, cualquier TaskITem nuevo que tenga un Asunto de "Rubberduck" se tratará como una carga útil.

Estoy usando una clase de VBA estándar aquí, pero la clase podría ser estática (editando los atributos), y el método CallByName aún debería funcionar.

Una vez que la DLL es capaz de ejecutar el código VBA de esta manera, hay otros pasos que se pueden tomar para reforzar la integración:

  1. Podría pasar los punteros de método a C # / Rubberduck usando el operador AddressOf , y luego C # podría llamar a esos procedimientos por sus punteros de función, usando algo como el CallWindowProc de Win32 CallWindowProc

  2. Podría crear una clase de VBA con un miembro predeterminado y luego asignar una instancia de esa clase a una propiedad DLL de C # que requiera un controlador de devolución de llamada. (similar a la propiedad OnReadyStateChange del objeto MSXML2.XMLHTTP60)

  3. Podría pasar detalles usando un objeto COM, como Rubberduck ya está haciendo con la clase Assert.

  4. No he pensado en esto, pero me pregunto si definió una clase de VBA con PublicNotCreatable instancias de PublicNotCreatable , si luego podría pasar eso a C #.

Y finalmente, aunque esta solución involucra una pequeña cantidad de repetitivo, tendría que jugar bien con los controladores de eventos existentes, y no he tratado con eso.