如何从C#代码中捕获Microsoft Access VBA调试错误?

我有一个C#程序,它打开几个Microsoft Access文件,并从每个文件中执行function。

本质上,代码看起来像这样:

Microsoft.Office.Interop.Access.Application app = new Microsoft.Office.Interop.Access.Application(); app.Visible = true; app.OpenCurrentDatabase(accessFileFullPath, false, ""); //Call the function app.Eval(function); 

但是,当VBA代码中发生调试错误时,我想将其捕获到我的C#程序中。

不要回答:“在您的VBA程序中捕获错误”。 由于我不会涉及的原因,这是不可能的。

我过去使用的一种方法是让一个线程间歇性地监视任何Visual Basic Debug窗口的句柄(FindWindowEx Win32函数返回非零值)。 我不喜欢这种方法,也不想继续使用它。

我找到了这个适用于Microsoft Excel的线程 。 本质上,它使用Microsoft.VisualBasic.CallByName()函数,它显然可以被捕获在try / catch块中,而无需用户交互。 但是,我无法使用Microsoft Access – 主要是因为我无法弄清楚如何使用此命令调用函数/ sub。

任何建议都将真诚地感谢!

编辑:正如我在下面的一个答案中提到的,我尝试将Eval()包装在try / catch块中,我的C#程序似乎忽略它,直到用户点击“Microsoft Visual Basic”上的“结束”按钮“错误对话框。 我不希望任何用户交互,而是想要捕获VBA错误以便在我的C#程序中处理。

更新:出于某种原因,我发布的上一个代码仅在Access文件格式为2000时才有效。我已确认此新代码也适用于Access 2002和2010文件。

代码:

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

细节:

根据Mike Rosenblum 链接的线程 ,CallByName()可以在C#中执行Office代码,并且可以捕获VBAexception( Application.Run()Application.Eval()似乎只有在用户与之交互后才被捕获调试窗口)。 问题是CallByName()需要一个[实例化]对象来调用方法。 默认情况下,Excel具有ThisWorkbook对象,该对象在打开工作簿时实例化。 据我所知,Access没有类似的可访问对象。

同一线程上的后续post建议将代码动态添加到Excel工作簿,以允许调用标准模块中的方法。 在ThisWorkbook上执行此操作相对简单,因为ThisWorkbook具有代码隐藏function并自动实例化。 但是我们怎样才能在Access中做到这一点?

该解决方案以下列方式结合了上述两种技术:

  1. 以编程方式在目标Access文件中创建一个新的临时类模块,用于在Access数据库中调用目标函数。 请记住,类的Instancing属性必须设置为2 - PublicNotCreatable 。 这意味着该类不能在该项目之外创建,但可以公开访问。
  2. 将新标准模块附加到目标Access文件,并创建一个公共函数来实例化该类并将其返回。
  3. 通过在步骤(2)中调用VBA代码来获取C#代码中对象的引用。 这可以使用Access interop的Application.Run()来完成。
  4. 使用CallByName–在(3)中调用对象上的方法,CallByName–调用标准模块中的方法,并且是可捕获的。
  5. 当您关闭数据库时,使用acQuitSaveNone调用Application.Quit() ,因此您刚刚创建的VBA代码都不会被保存。

要获取VBA错误描述,请使用“e.Message”,其中“e”是COMException对象。

确保将以下.NET引用添加到C#项目中:

 Microsoft.Office.Interop.Access Microsoft.Vbe.Interop Microsoft.VisualBasic 

我不知道它是否是最好的方法,但你应该能够在try / catch块中捕获COMException并检查exception以查看究竟是什么抛出的。 我这样做是使用Excel互操作。

 try { DoSomething(); } catch (COMException ex) { Console.WriteLine ex.ErrorCode.ToString(); Console.WriteLine ex.Message; }