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

7
我是一名有用的助手,可以为您进行翻译。以下是需要翻译的内容:

我有一个C#程序,可以打开多个Microsoft Access文件,并在每个文件中执行函数。

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

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调试窗口的句柄的线程(FindWindowEx Win32函数返回非零值)。我不喜欢这种方法,也不想继续使用它。我找到了this thread,它适用于Microsoft Excel。实质上,它使用Microsoft.VisualBasic.CallByName()函数,显然可以在try/catch块中捕获,无需用户交互。然而,我无法在Microsoft Access中使其正常工作,主要是因为我无法弄清如何使用此命令调用函数/子程序。真诚地感谢您的任何建议!

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

2个回答

5

更新: 由于某些原因,我之前发布的代码只能在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 代码,并且可以捕获 VBA 异常(Application.Run()Application.Eval() 在用户与调试窗口交互后才能被捕获)。问题在于 CallByName() 需要一个 [实例化的] 对象来调用其上的方法。默认情况下,Excel 有 ThisWorkbook 对象,该对象在打开工作簿时实例化。据我所知,Access 没有类似的可访问对象。

同一线程上的后续帖子 建议动态向 Excel 工作簿添加代码以允许在标准模块中调用方法。在 ThisWorkbook 上这样做相对简单,因为 ThisWorkbook 有代码后台并且自动实例化。但是在 Access 中怎么做呢?

解决方案将上述两种技术结合起来:

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

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

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

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

3

我不确定这是否是最好的方法,但您可以在try/catch块中捕获COMException并检查异常以查看确切抛出了什么。我使用Excel互操作执行此操作。

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

我已经在Eval方法周围尝试了一个try/catch,但它似乎完全忽略它。你是在使用Eval来调用你的Excel函数吗? - transistor1
1
不,我不是。我本以为Interop会捕获它并将其传递给其使用者。 - squillman
那么,在你的“DoSomething()”方法中,你使用什么来从Excel工作簿内调用VBA函数?我希望我没有漏掉什么显而易见的东西! - transistor1
1
抱歉...我的意思是这就是我为任何交互调用所做的事情,但它们中没有一个在调用内部VBA函数。我猜我只是假设交互会捕捉到这些并像它对其他所有东西一样通过COMException向您报告... - squillman
好的 - 稍作更正。我决定再次尝试您的方法,它并不完全忽略VBA错误,但是在您点击Visual Basic弹出框上的“结束”之前,它不会捕获COMException。问题是,我不希望用户进行交互。我赞同了您的答案。 - transistor1
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接