How can I capture a Microsoft Access VBA debug error from my C# code?

孤人 提交于 2019-11-29 04:27:40
transistor1

Update: For some reason, the previous code I had posted had only worked when the Access file format was 2000. I have confirmed that this new code also works with Access 2002 and 2010 files.

The code:

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

The Details:

According to the thread I had linked to by Mike Rosenblum, CallByName() can execute Office code in C#, and can trap VBA exceptions (Application.Run() and Application.Eval() seem to only get caught after the user interacts with the debug window). The problem is that CallByName() requires an [instantiated] object to call a method on. By default, Excel has the ThisWorkbook object, which is instantiated upon opening a workbook. Access does not have a similar object that is accessible, as far as I know.

A subsequent post on the same thread suggests adding code dynamically to the Excel workbook to allow calling of methods in standard modules. Doing so on ThisWorkbook is relatively trivial, because ThisWorkbook has a code-behind and is automatically instantiated. But how can we do this in Access?

The solution combines the two techniques above in the following way:

  1. Programatically create a new temporary class module in the target Access file, with which to call the target function in the Access database. Keep in mind that the Instancing property of the class must be set to 2 - PublicNotCreatable. This means that the class is not creatable outside of that project, but it is accessible publicly.
  2. Append a new standard module to the target Access file, and create a public function to instantiate the class and return it.
  3. Get a reference to the object in your C# code, by calling the VBA code in step (2). This can be done using Access interop's Application.Run().
  4. Call the method on the object from (3) using CallByName-- which calls the method in the standard module, and is trappable.
  5. When you close the database, you call Application.Quit() with acQuitSaveNone, so none of the VBA code you just created gets saved.

To get the VBA error description, use "e.Message", where "e" is the COMException object.

Make sure you add the following .NET references to your C# project:

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

I don't know if it's the best method, but you should be able to catch COMException in a try/catch block and inspect the exception to see what exactly was thrown. I do this using the Excel interop.

try {
  DoSomething();
} catch (COMException ex) {
  Console.WriteLine ex.ErrorCode.ToString();
  Console.WriteLine ex.Message;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!