Как я могу зафиксировать ошибку отладки Microsoft Access VBA из моего кода C #? - PullRequest
7 голосов
/ 12 марта 2012

У меня есть программа на 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возвращает ненулевое значение).Мне не нравится этот метод, и я не хочу продолжать его использовать.

Я нашел этот поток , который применяется к Microsoft Excel.По сути, он использует функцию Microsoft.VisualBasic.CallByName(), которая, очевидно, может быть захвачена в блоке try / catch без взаимодействия с пользователем.Однако я не смог заставить это работать с Microsoft Access - главным образом потому, что я не могу понять, как вызвать функцию / подпрограмму с помощью этой команды.

Любые предложения будут искренне приветствоваться!

Редактировать: Как я упоминал в одном из ответов ниже, я пытался обернуть Eval() в блок try / catch, и моя программа на C #, похоже, игнорирует его, пока пользователь не нажмет "Кнопка «Завершить» в диалоговом окне ошибки «Microsoft Visual Basic».Я не хочу никакого взаимодействия с пользователем, а хочу перехватить ошибку VBA для обработки в моей программе на C #.

Ответы [ 2 ]

5 голосов
/ 14 марта 2012

Обновление: По какой-то причине предыдущий код, который я разместил, работал, только когда формат файла 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();
            }
        }
    }
}

Подробности:

Согласно потоку, который я связал с с помощью Майка Розенблюма , CallByName () может выполнять код Office в C # и может перехватывать исключения VBA (Application.Run() и Application.Eval() кажется, только пойман после того, как пользователь взаимодействует с окном отладки). Проблема в том, что CallByName () требует объекта [instantiated] для вызова метода. По умолчанию в Excel имеется объект ThisWorkbook, который создается при открытии книги. Насколько мне известно, у Access нет похожего объекта, который доступен.

Последующее сообщение в той же теме предлагает динамически добавлять код в книгу Excel, чтобы разрешить вызов методов в стандартных модулях. Выполнение этого на ThisWorkbook является относительно тривиальным, потому что ThisWorkbook имеет программный код и создается автоматически. Но как мы можем сделать это в Access?

Решение объединяет две вышеописанные методики следующим образом:

  1. Программно создайте новый модуль временного класса в целевом файле Access, с помощью которого можно вызвать целевую функцию в базе данных Access. Помните, что для свойства Instancing класса должно быть установлено значение 2 - PublicNotCreatable. Это означает, что класс нельзя создать за пределами этого проекта, но он доступен публично.
  2. Добавьте новый стандартный модуль к целевому файлу Access и создайте публичную функцию для создания экземпляра класса и его возврата.
  3. Получите ссылку на объект в вашем коде C #, вызвав код VBA на шаге (2). Это можно сделать с помощью приложения Access interop's Application.Run ().
  4. Вызвать метод объекта из (3), используя CallByName--, который вызывает метод в стандартном модуле и является trappable.
  5. Когда вы закрываете базу данных, вы вызываете Application.Quit() с acQuitSaveNone, поэтому ни один из только что созданного вами кода VBA не сохраняется.

Чтобы получить описание ошибки VBA, используйте «e.Message», где «e» - это объект COMException.

Убедитесь, что вы добавили следующие ссылки .NET в ваш проект C #:

Microsoft.Office.Interop.Access
Microsoft.Vbe.Interop
Microsoft.VisualBasic
3 голосов
/ 12 марта 2012

Я не знаю, является ли это лучшим методом, но вы должны быть в состоянии поймать COMException в блоке try / catch и проверить исключение, чтобы увидеть, что именно было выброшено.Я делаю это с помощью взаимодействия с Excel.

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