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:
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 elCallWindowProc
de Win32CallWindowProc
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)
Podría pasar detalles usando un objeto COM, como Rubberduck ya está haciendo con la clase Assert.
No he pensado en esto, pero me pregunto si definió una clase de VBA con
PublicNotCreatable
instancias dePublicNotCreatable
, 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.