Update: Per qualche ragione, il codice precedente avevo postato aveva lavorato solo quando il formato di file di Access 2000. Era mi hanno confermato che questo nuovo codice funziona anche con Access 2002 e file 2010.
Il codice:
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();
}
}
}
}
I Dettagli:
Secondo the thread I had linked to da Mike Rosenblum, CallByName() può eseguire codice Ufficio in C#, e può eccezioni VBA trap (Application.Run()
e Application.Eval()
sembra che vengano catturati solo dopo che l'utente interagisce con la finestra di debug). Il problema è che CallByName() richiede un oggetto [istanziato] per chiamare un metodo. Per impostazione predefinita, Excel ha l'oggetto ThisWorkbook
, che viene istanziato all'apertura di una cartella di lavoro. L'accesso non ha un oggetto simile che sia accessibile, per quanto ne so.
A subsequent post on the same thread suggerisce di aggiungere il codice dinamicamente alla cartella di lavoro di Excel per consentire la chiamata di metodi in moduli standard. Farlo su ThisWorkbook
è relativamente banale, perché ThisWorkbook ha un code-behind e viene istanziato automaticamente. Ma come possiamo farlo in Access?
La soluzione combina le due tecniche sopra nel seguente modo:
- creare Programatically un nuovo temporanea modulo classe nel file di accesso di destinazione, con cui chiamare la funzione di destinazione nel database di Access. Tenere presente che la proprietà
Instancing
della classe deve essere impostata su 2 - PublicNotCreatable
. Ciò significa che la classe non è creabile al di fuori di quel progetto, ma è accessibile pubblicamente.
- Aggiungere un nuovo modulo standard al file di destinazione e creare una funzione pubblica per istanziare la classe e restituirla.
- Ottieni un riferimento all'oggetto nel tuo codice C#, chiamando il codice VBA al punto (2). Questo può essere fatto usando Application.Run() di Access Access.
- Chiama il metodo sull'oggetto da (3) utilizzando CallByName-- che chiama il metodo nel modulo standard ed è intercettabile.
- Quando si chiude il database, si chiama
Application.Quit()
con acQuitSaveNone
, quindi nessuno del codice VBA appena creato viene salvato.
Per ottenere la descrizione dell'errore VBA, utilizzare "e.Message", dove "e" è l'oggetto COMException.
Assicurati di aggiungere i seguenti riferimenti .NET al progetto C#:
Microsoft.Office.Interop.Access
Microsoft.Vbe.Interop
Microsoft.VisualBasic
ho cercato un try/catch attorno al metodo Eval, e sembra ignorare completamente. Stai usando Eval per chiamare le tue funzioni di Excel? – transistor1
No, non lo sono. Avrei pensato che l'interop lo avrebbe catturato e fatto saltare in aria al suo consumatore. – squillman
Quindi, che cosa usi all'interno del tuo metodo 'DoSomething()' per chiamare le funzioni VBA dalla cartella di lavoro di Excel? Spero di perdere qualcosa di ovvio! – transistor1