Actualización: Por alguna razón, el código anterior había publicado solamente había trabajado cuando el formato de archivo de Access fue 2000. He confirmado que este nuevo código también funciona con Access 2002 y 2010 archivos.
El código:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using VBA = Microsoft.Vbe.Interop;
namespace CaptureVBAErrorsTest
{
class CaptureVBAErrors
{
public void runApp(string databaseName, string function)
{
VBA.VBComponent f = null;
VBA.VBComponent f2 = null;
Microsoft.Office.Interop.Access.Application app = null;
object Missing = System.Reflection.Missing.Value;
Object tempObject = null;
try
{
app = new Microsoft.Office.Interop.Access.Application();
app.Visible = true;
app.OpenCurrentDatabase(databaseName, false, "");
//Step 1: Programatically create a new temporary class module in the target Access file, with which to call the target function in the Access database
//Create a Guid to append to the object name, so that in case the temporary class and module somehow get "stuck",
//the temp objects won't interfere with other objects each other (if there are multiples).
string tempGuid = Guid.NewGuid().ToString("N");
f = app.VBE.ActiveVBProject.VBComponents.Add(VBA.vbext_ComponentType.vbext_ct_ClassModule);
//We must set the Instancing to 2-PublicNotCreatable
f.Properties.Item("Instancing").Value = 2;
f.Name = "TEMP_CLASS_" + tempGuid;
f.CodeModule.AddFromString(
"Public Sub TempClassCall()\r\n" +
" Call " + function + "\r\n" +
"End Sub\r\n");
//Step 2: Append a new standard module to the target Access file, and create a public function to instantiate the class and return it.
f2 = app.VBE.ActiveVBProject.VBComponents.Add(VBA.vbext_ComponentType.vbext_ct_StdModule);
f2.Name = "TEMP_MODULE_" + tempGuid
f2.CodeModule.AddFromString(string.Format(
"Public Function instantiateTempClass_{0}() As Object\r\n" +
" Set instantiateTempClass_{0} = New TEMP_CLASS_{0}\r\n" +
"End Function"
,tempGuid));
//Step 3: Get a reference to a new TEMP_CLASS_* object
tempObject = app.Run("instantiateTempClass_" + tempGuid, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing, ref Missing);
//Step 4: Call the method on the TEMP_CLASS_* object.
Microsoft.VisualBasic.Interaction.CallByName(tempObject, "TempClassCall", Microsoft.VisualBasic.CallType.Method);
}
catch (COMException e)
{
MessageBox.Show("A VBA Exception occurred in file:" + e.Message);
}
catch (Exception e)
{
MessageBox.Show("A general exception has occurred: " + e.StackTrace.ToString());
}
finally
{
//Clean up
if (f != null)
{
app.VBE.ActiveVBProject.VBComponents.Remove(f);
Marshal.FinalReleaseComObject(f);
}
if (f2 != null)
{
app.VBE.ActiveVBProject.VBComponents.Remove(f2);
Marshal.FinalReleaseComObject(f2);
}
if (tempObject != null) Marshal.FinalReleaseComObject(tempObject);
if (app != null)
{
//Step 5: When you close the database, you call Application.Quit() with acQuitSaveNone, so none of the VBA code you just created gets saved.
app.Quit(Microsoft.Office.Interop.Access.AcQuitOption.acQuitSaveNone);
Marshal.FinalReleaseComObject(app);
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
}
Los detalles:
Según the thread I had linked to por Mike Rosenblum, CallByName() puede ejecutar código Oficina en C#, y pueden excepciones VBA trampa (Application.Run()
y Application.Eval()
parece que solo se atrapa después de que el usuario interactúa con la ventana de depuración). El problema es que CallByName() requiere un objeto [instanciado] para invocar un método. De forma predeterminada, Excel tiene el objeto ThisWorkbook
, que se crea una instancia al abrir un libro de trabajo. El acceso no tiene un objeto similar que sea accesible, hasta donde yo sé.
A subsequent post on the same thread sugiere agregar código dinámicamente al libro de Excel para permitir la llamada de métodos en módulos estándar. Hacerlo en ThisWorkbook
es relativamente trivial, porque ThisWorkbook tiene un código subyacente y se crea una instancia automáticamente. Pero, ¿cómo podemos hacer esto en Access?
La solución combina las dos técnicas antes de la siguiente manera:
- Programatically crear un nuevo módulo de clase temporal en el archivo de acceso de destino, con el que llama a la función objetivo en la base de datos Access. Tenga en cuenta que la propiedad
Instancing
de la clase debe establecerse en 2 - PublicNotCreatable
. Esto significa que la clase no se puede crear fuera de ese proyecto, pero es accesible públicamente.
- Agregue un nuevo módulo estándar al archivo de acceso de destino y cree una función pública para crear una instancia de la clase y devolverla.
- Obtenga una referencia al objeto en su código C#, llamando al código VBA en el paso (2). Esto se puede hacer usando la Aplicación.Run de Access interop().
- Llamar al método en el objeto desde (3) usando CallByName-- que llama al método en el módulo estándar, y es interceptable.
- Cuando cierra la base de datos, llama al
Application.Quit()
con acQuitSaveNone
, por lo que ninguno de los códigos VBA que acaba de crear se guarda.
Para obtener la descripción del error de VBA, use "e.Message", donde "e" es el objeto COMException.
asegúrese de agregar las siguientes referencias .NET a su proyecto de C#:
Microsoft.Office.Interop.Access
Microsoft.Vbe.Interop
Microsoft.VisualBasic
He probado un try/catch alrededor del método Eval, y parece ignorar por completo. ¿Estás utilizando Eval para llamar a tus funciones de Excel? – transistor1
No, no lo soy. Pensé que la interoperabilidad lo atraparía y lo burbujearía sobre su consumidor. – squillman
Entonces, ¿qué usa dentro de su método 'DoSomething()' para llamar a las funciones de VBA desde su libro de Excel? ¡Espero que me esté perdiendo algo obvio! – transistor1