Архив рубрики: IExtensionApplication

Последовательность действий AutoCAD при загрузке управляемого расширения

Маленькая шпаргалка на тему того, как AutoCAD обрабатывает загруженное в него управляемое расширение.


При загрузке управляемой сборки в AutoCAD сначала проверяются атрибуты сборки. Если найден атрибут ExtensionApplication, указывающий на публичный класс, реализующий интерфейс IExtensionApplication, то создаётся экземпляр данного класса и вызывается его метод Initialize. В дальнейшем, при завершении сеанса AutoCAD, обязательно будет вызван метод Terminate этого же экземпляра. К моменту вызова Terminate все документы приложения уже закрыты. 

В сборке должно быть не более одного атрибута ExtensionApplication. Если же сборка не содержит атрибута ExtensionApplication, то тогда производится анализ всех публичных классов этой сборки. В случае успешного результата поиска создаётся экземпляр первого найденного публичного класса, реализующего IExtensionApplication. При этом обязательно должна присутствовать возможность создания экземпляра данного класса при помощи конструктора по умолчанию.

Затем AutoCAD проверяет сборку на наличие атрибутов CommandClass, указывающих на её публичные классы, в которых определены команды AutoCAD. Таких атрибутов может быть столько, сколько потребуется разработчику. Если у сборки обнаружены атрибуты CommandClass, то выполняется аналитика содержимого только тех публичных классов, на которые указывают эти атрибуты - все остальные классы игнорируются.

Если в сборка не имеет атрибутов CommandClass, то производится поиск всех публичных классов, в которых определены команды AutoCAD.

Команды, определённые в составе сборки, могут быть как статическими, так и экземплярными. Если команда экземплярная, то объект данного класса создаётся применительно к документу, из контекста которого команда вызвана. Это означает, что значение свойств и полей у такого объекта для каждого документа будут свои. В таком классе обязательно должна присутствовать возможность создать его экземпляр при помощи конструктора по умолчанию. Причём создаётся этот объект в момент первого вызова любой из экземплярных команд данного класса. Время жизни такого объекта определяется временем жизни документа, к которому он привязан. Удаление этого объекта происходит при закрытии документа. Т.о. если класс удаляемого объекта реализует IDisposable, то будет вызываться метод Dispose.

Для каждого документа создаётся свой объект класса, содержащий экземплярные команды. Причём создаётся он в момент вызова одной из этих команд выполненного именно в контексте этого документа. Если в документе не вызывались команды нашей сборки, то и объекты, содержащие экземплярные методы наших команд создаваться для этого документа не будут.

Если команды определены в статических методах класса, то тогда экземпляры такого класса создаваться не будут. Код статического конструктора такого класса будет запущен на исполнение один раз, при первом запуске команды этого класса.

Назначение сборке атрибутов ExtensionApplication и CommandClass позволяет AutoCAD быстро найти нужную ему информацию, не анализируя все классы сборки и, соответственно, быстрее загрузить её.

Далее небольшой код, демонстрирующий описанный выше материал. В комментариях показан результат работы кода - содержимое файла MyLog.txt.

/* MyClasses.cs
 * Андрей Бушман, 2014
 * 
 * Код демонстрирует последовательность вызовов
 * методов, конструкторов и деструкторов 
 * различных классов команд, а так же класса, 
 * реализующего интерфейс IExtensionApplication.
 * 
 * Последовательность действий:
 * ---------------------------------------------
 * 1. Запускаем AutoCAD 2015 (или любую др. 
 *      версию).
 * 2. Создаём три новых документа: Drawing1.dwg, 
 *      Drawing2.dwg и Drawing3.dwg.
 * 3. Устанавливаем текущим документ Drawing1.dwg
 * 4. Загружаем сборку при помощи команды NETLOAD
 * 5. Последовательно запускаем команды: TEST, 
 *      TEST2 и снова TEST.
 * 6. Переключаемся в документ Drawing2.dwg.
 * 7. Последовательно запускаем команды: TEST, 
 *      TEST2 и снова TEST.
 * 8. Закрываем все документы и завершаем работу 
 *      AutoCAD.
 * 9. Смотрим содержимое файла MyLog.txt. 
 *      ВНИМАНИЕ! acad.exe ещё некоторое время 
 *      работает даже после того, как все окна 
 *      закрылись. Прежде чем открывать файл 
 *      MyLog.txt дождитесь завершения процесса 
 *      acad.exe, т.к. нам нужно дождаться, когда
 *      выполнится код метода Terminate().
 * ---------------------------------------------
 * 
 * Содержимое сгенерированного файла MyLog.txt: 
 * ==============================================
 * Метод: ExtensionApplication.Initialize(); 
 *  Документ: Drawing1.dwg; Время: 05.08.2014 
 *  11:56:55
 * Метод: Commands.Commands() [статический]; 
 *  Документ: Drawing1.dwg; Время: 05.08.2014 
 *  11:57:27
 * Метод: Commands.Commands(); 
 *  Документ: Drawing1.dwg; Время: 05.08.2014 
 *  11:57:41
 * Метод: StaticCommands.StaticCommands() 
 *  [статический]; Документ: Drawing1.dwg; 
 *  Время: 05.08.2014 11:57:53
 * Метод: Commands.Commands(); 
 *  Документ: Drawing2.dwg; Время: 05.08.2014 
 *  11:58:15
 * Метод: Commands.Dispose(); 
 *  Документ: Drawing2.dwg; Время: 05.08.2014 
 *  11:58:31
 * Метод: Commands.Finalize(); 
 *  Документ: Drawing2.dwg; Время: 05.08.2014 
 *  11:58:33
 * Метод: Commands.Dispose(); 
 *  Документ: Drawing1.dwg; Время: 05.08.2014 
 *  11:58:33
 * Метод: Commands.Finalize(); 
 *  Документ: Drawing1.dwg; Время: 05.08.2014 
 *  11:58:34
 * Метод: ExtensionApplication.Terminate(); 
 *  Документ: null; Время: 05.08.2014 11:59:04
 *  =============================================
 */
using System;
using System.Reflection;
using System.IO;

using cad = Autodesk.AutoCAD.ApplicationServices
.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Rt = Autodesk.AutoCAD.Runtime;

[assembly: Rt.ExtensionApplication(typeof(Bushman
    .CAD.Logging.ExtApp))]

[assembly: Rt.CommandClass(typeof(Bushman.CAD
    .Logging.Commands))]

[assembly: Rt.CommandClass(typeof(Bushman.CAD
    .Logging.StaticCommands))]

namespace Bushman.CAD.Logging {

    static class LogWritter {
        static String logFileName = Path.Combine(
            Environment.GetFolderPath(Environment
            .SpecialFolder.MyDocuments),
            "MyLog.txt");

        public static void WriteLine(
            String methodName,
            String documentName) {
            using (StreamWriter sw =
                File.AppendText(logFileName)) {
                String line = String.Format(
                "Метод: {0}; Документ: {1}; " +
                "Время: {2}", methodName,
                documentName, DateTime.Now
                .ToString("dd.MM.yyyy hh:mm:ss"))
                ;
                sw.WriteLine(line);
                sw.Flush();
                sw.Close();
            }
        }
    }

    public sealed class ExtApp :
        Rt.IExtensionApplication {

        // Можно дополнительно создать 
        // статический и|или экземплярный 
        // конструкторы класса, однако вряд ли в
        // данном случае это будет иметь смысл, 
        // посколько равно как и метод Initialize
        // они будут вызваны лишь один раз. В 
        // виду этого можно ограничиться 
        // использованием лишь Initialize.
        // Аналогичная ситуация обстоит и с 
        // Dispose (в случае реализации
        // IDisposable) - вполне достаточно 
        // использовать Terminate.

        // Этот метод выполняется один раз при 
        // загрузке сборки в AutoCAD.
        public void Initialize() {
            Ap.Document doc = cad.DocumentManager
                .MdiActiveDocument;
            String className = MethodBase
                .GetCurrentMethod().ReflectedType
                .Name;
            String methodName = MethodBase
                .GetCurrentMethod().Name;
            String name = className + "." +
                methodName + "()";
            LogWritter.WriteLine(name, doc.Name);
        }

        // Этот метод выполняется один раз при 
        // завершении работы AutoCAD. К этому 
        // моменту все документы уже закрыты.
        public void Terminate() {
            Ap.Document doc = cad.DocumentManager
                .MdiActiveDocument;
            String documentName = (null == doc) ?
                "null" : doc.Name;
            String className = MethodBase
                .GetCurrentMethod().ReflectedType
                .Name;
            String methodName = MethodBase
                .GetCurrentMethod().Name;
            String name = className + "." +
                methodName + "()";
            LogWritter.WriteLine(name,
                documentName);
        }
    }

    // Инстанцирование объекта данного класса 
    // происходит индивидуально для каждого 
    // документа и только в том случае, если была
    // вызвана команда, определённая в данном 
    // классе.
    public sealed class Commands : IDisposable {

        String documentName;

        // статический конструктор. Вызывается 
        // один раз при первом вызове из любого 
        // документа любой из команд, 
        // определённых в данном классе.
        static Commands() {
            Ap.Document doc = cad.DocumentManager
                .MdiActiveDocument;
            String documentName = (null == doc) ?
                "null" : doc.Name;
            String className = MethodBase
                .GetCurrentMethod().ReflectedType
                .Name;
            String methodName = MethodBase
                .GetCurrentMethod().Name;
            if (methodName == ".cctor")
                methodName = className;
            String name = className + "." +
                methodName + "() [статический]";
            LogWritter.WriteLine(name,
                documentName);
        }

        // Конструктор, код которого выполняется 
        // только при первом запуске любой из 
        // команд, определённых в составе данного
        // класса. Если в процессе работы над 
        // документом команды данного класса не 
        // запускались, то и экземпляр класса не 
        // создаётся.
        public Commands() {
            Ap.Document doc = cad.DocumentManager
                .MdiActiveDocument;
            documentName = (null == doc) ? "null"
                : doc.Name;
            String className = MethodBase
                .GetCurrentMethod().ReflectedType
                .Name;
            String methodName = MethodBase
                .GetCurrentMethod().Name;
            if (methodName == ".ctor")
                methodName = className;
            String name = className + "." +
                methodName + "()";
            LogWritter.WriteLine(name,
                documentName);
        }

        // Тестовая экземплярная команда
        [Rt.CommandMethod("Test")]
        public void Test() { }

        // Деструктор вызывается сборщиком мусора
        // GC после того, как объект данного 
        // класса будет уничтожен. Т.е. в нашем 
        // случае, это произойдёт только после 
        // вызова метода Dispose. Когда именно 
        // вызывается деструктор - решает GC.
        // Вряд ли имеет смысл в расширениях 
        // AutoCAD прибегать к вызову 
        // деструктора. Данный код предназначен 
        // лишь для демонстрации 
        // последовательностей вызовов.
        ~Commands() {
            String className = MethodBase
                .GetCurrentMethod().ReflectedType
                .Name;
            String methodName = MethodBase
                .GetCurrentMethod().Name;
            String name = className + "." +
                methodName + "()";
            LogWritter.WriteLine(name,
                documentName);
        }

        // При закрытии документа объект данного 
        // класса, ранее созданный в контексте 
        // закрываемого документа (см. 
        // комментарий к конструктору) 
        // уничтожается. Соответственно, будет 
        // вызван Dispose, при условии реализации
        // IDisposable. 
        public void Dispose() {
            String className = MethodBase
                .GetCurrentMethod().ReflectedType
                .Name;
            String methodName = MethodBase
                .GetCurrentMethod().Name;
            String name = className + "." +
                methodName + "()";
            LogWritter.WriteLine(name,
                documentName);
        }
    }

    // В этом классе определена статическая 
    // команда. При этом экземпляры данного 
    // класса не создаются.
    public sealed class StaticCommands {

        static String documentName;

        // статический конструктор. Вызывается 
        // один раз при первом вызове из любого 
        // документа любой из команд, 
        // определённых в данном классе.
        static StaticCommands() {
            Ap.Document doc = cad.DocumentManager
                .MdiActiveDocument;
            documentName = (null == doc) ? "null"
                : doc.Name;
            String className = MethodBase
                .GetCurrentMethod().ReflectedType
                .Name;
            String methodName = MethodBase
                .GetCurrentMethod().Name;
            if (methodName == ".cctor")
                methodName = className;
            String name = className + "." +
                methodName + "() [статический]";
            LogWritter.WriteLine(name,
                documentName);
        }

        // Тестовая статическая команда
        [Rt.CommandMethod("Test2")]
        public static void Test2() { }
    }
}

Единая "точка входа" (DLL файл) в плагин .NET, ARX или VBA, не зависящая от версии AutoCAD

Поскольку .NET плагины AutoCAD (и не только они) имеют зависимость от версий обозначенной САПР, то нередко приходится один и тот же исходный код компилировать отдельно под разные версии AutoCAD. Результаты компиляции я, как правило, размещаю либо в одном и том же каталоге плагина, либо по специальным его подкаталогам, имена которых указывают на целевую версию ядра AutoCAD, а так же зависимость от разрядности приложения (если таковая присутствует). Какой из двух перечисленных вариантов использовать - зависит от конкретного случая.

Т.о. результат может выглядеть, к примеру, либо так:


либо так:


Имена файлов, зависящих от версии AutoCAD, так же содержат в виде суффикса версию ядра приложения, а так же, в случае необходимости, разрядность целевой платформы. Например, согласно обозначенному выше скрину, в подкаталоге .R17.2x64 будет находиться файл RegionTools.17.2x64.dll, а в подкаталоге .R20.0 - файл RegionTools.20.0.dll.

Я использую версию ядра AutoCAD, а не год (2009, 2010, 2011 и т.д.), обозначенный в имени САПР, т.к. программно нужную версию DLL файла удобней находить именно по версии ядра AutoCAD - это наиболее надёжная и точная информация.

Конечно, при такой системе наименований рядовому пользователю может быть сложно понять, какой именно DLL файл из набора имеющихся подкаталогов, ему следует загружать в установленную у него версию AutoCAD. Как вариант: можно в файле readme.txt разместить текстовую информацию о том, для какой версии AutoCAD какой DLL файл следует загружать.

Однако можно эту задачу решить иначе: непосредственно в подкаталоге .bin создавать единственный DLL файл, который предназначен для загрузки в любую версию AutoCAD. Предназначение этого файла заключается в том, чтобы загрузить в AutoCAD наиболее подходящую версию плагина, найдя её либо в текущем каталоге сборки, либо в соответствующих подкаталогах. На скрине показанном выше эта роль возложена на файл RegionTools.dll.

Т.о. Какую бы версию AutoCAD пользователь не имел на своей машине, ему всегда нужно загружать только файл RegionTools.dll

Аналогичная проблема актуальна так же для плагинов ARX и VBA (VBA "хромает" по части разрядности: x86x64). Поэтому было бы вполне логично расширить решение таким образом, чтобы оно работало не только для .NET, но так же и для C++, VBA. Для ARX и VBA плагинов систему наименований, а так же структуру подкаталогов использую такие же, как и для .NET (указана мною выше).

Поскольку данный подход удобно применять во всех плагинах,  то его можно оформить в виде шаблона проекта:



Далее приводится подробно комментированный код обозначенной выше логики. Проект компилирую с опцией AnyCPU под .NET 3.5,  хотя AutoCAD 2009 по умолчанию использует 3.0. Мой выбор обусловлен тем, что AutoCAD 2009 может вместо 3.0 использовать 3.5, в которой присутствует технология LINQ, активно мною используемая. Версии ниже чем AutoCAD 2009 мне не интересны. Однако тут возникает один нюанс: на исходном компьютере так же должен быть установлен .NET Framework 3.5 SP1.

/* EntryPoint.cs
 * © Andrey Bushman, 2014
 * Поиск и загрузка версии плагина .NET, ARX или VBA, наиболее пригодной для 
 * текущей версии AutoCAD.
 * http://bushman-andrey.blogspot.ru/2014/06/dll-autocad.html
 */
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

#if AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Rt = Autodesk.AutoCAD.Runtime;
#endif

[assembly: Rt.ExtensionApplication(typeof(Bushman.CAD.EntryPoint
  .EntryPoint))]

namespace Bushman.CAD.EntryPoint {
  /// <summary>
  /// Задачей данного класса является поиск и загрузка в AutoCAD наиболее 
  /// подходящей для него версии плагина.
  /// </summary>
  public sealed class EntryPoint : Rt.IExtensionApplication {
    const String netPluginExtension = ".dll";
    static readonly String[] extensions = new String[] { ".arx"".dvb" };
    static readonly String[] methodNames = new String[] { "LoadArx""LoadDVB" 
    };

    /// <summary>
    /// Код этого метода будет запущен на исполнение при загрузке сборки в 
    /// AutoCAD. В результате его работы происходит попытка найти и загрузить в
    /// AutoCAD наиболее подходящую версию плагина из имеющихся в наличии.
    /// </summary>
    public void Initialize() {
      // Для начала извлекаем информацию о текущей версии AutoCAD и ищем
      // соответствующую ей версию файла. Имя такого файла должно 
      // формироваться по правилу: 
      //    ИмяТекущейСборки.Major.Minor[x86|x64].(dll|arx|dvb).
      // Где <Major> и <Minor> - это значения одноимённых свойств объекта 
      // Version, полученного из Application.Version.
      Version version = cad.Version;

      String fileFullName = GetType().Assembly.Location;

      Version minVersion = new Version(17, 2);

      FileInfo targetDllFullName = FindFile(fileFullName, version, minVersion);

      if(targetDllFullName == null)
        return;

      // Если найден файл, соответствующий нашей версии AutoCAD, то 
      // загружаем его.
      Assembly asm = null;
      try {
        if(targetDllFullName.Extension.Equals(netPluginExtension,
          StringComparison.CurrentCultureIgnoreCase))
          asm = Assembly.LoadFrom(targetDllFullName.FullName);
        else {
          Int32 index = Array.IndexOf(extensions, targetDllFullName.Extension);

          if(index >= 0) {
            Object application = cad.AcadApplication;

            application.GetType().InvokeMember(methodNames[index], BindingFlags
              .InvokeMethod, null, application, new Object[] { 
                targetDllFullName.FullName });
          }
        }
      }
      catch {
      }
    }

    /// <summary>
    /// Получить имя наиболее подходящего файла, для его последующей загрузки в
    /// AutoCAD. Если такой файл не будет найден, то возвращается null.
    /// </summary>
    /// <param name="fileFullName">"Базовое" имя файла, т.е. полное имя 
    /// файла без указания в нём версий ядра и разрядности платформы.</param>
    /// <param name="expectedVersion">Версия AutoCAD, для которой следует 
    /// выполнить поиск соответствующей версии файла.</param>
    /// <param name="minVersion">Наименьшая версия AutoCAD, ниже которой не 
    /// следует выполнять поиск.</param>
    /// <returns>Возвращается FileInfo наиболее подходящего файла, для его 
    /// последующей загрузки в AutoCAD. Если такой файл не будет найден, то 
    /// возвращается null.</returns>
    private FileInfo FindFile(String fileFullName, Version expectedVersion,
      Version minVersion) {

      if(fileFullName == null)
        throw new ArgumentNullException("fileFullName");

      if(fileFullName.Trim() == String.Empty)
        throw new ArgumentException(
          "fileFullName.Trim() == String.Empty");

      if(expectedVersion < minVersion)
        throw new ArgumentException(
          "expectedVersion < minVersion");

      Int32 major = expectedVersion.Major;
      Int32 minor = expectedVersion.Minor;

      String directory = Path.GetDirectoryName(fileFullName);
      String fileName = Path.GetFileNameWithoutExtension(fileFullName);

      String coreString = String.Format("{0}.{1}", major.ToString(),
        minor.ToString());

      String subDirectoryName = "R" + coreString;
      String subDirectoryName_xPlatform = subDirectoryName + (IntPtr.Size == 4
        ? "x86" : "x64");

      String targetFileName = String.Empty;
      String targetFileName_xPlatform = String.Empty;
      String targetFileFullName = String.Empty;
      String targetFileFullName_xPlatform = String.Empty;

      List<String> items = new List<String>(extensions);
      items.Insert(0, netPluginExtension);

      String name = String.Empty;

      foreach(String extension in items) {

        targetFileName = String.Format("{0}.{1}{2}", fileName, coreString,
          extension);
        targetFileName_xPlatform = String.Format("{0}.{1}{2}{3}", fileName,
          coreString, (IntPtr.Size == 4 ? "x86" : "x64"), extension);

        // Сначала выполняем поиск в текущем каталоге
        targetFileFullName = Path.Combine(directory, targetFileName);
        if(File.Exists(targetFileFullName)) {
          name = targetFileFullName;
          break;
        }
        targetFileFullName_xPlatform = Path.Combine(directory,
          targetFileName_xPlatform);
        if(File.Exists(targetFileFullName_xPlatform)) {
          name = targetFileFullName_xPlatform;
          break;
        }

        // Если в текущем каталоге подходящий файл не найден, то продолжаем
        // поиск по соответствующим подкаталогам
        targetFileFullName = directory + "\" + subDirectoryName +
          "\" + targetFileName;
        if(File.Exists(targetFileFullName)) {
          name = targetFileFullName;
          break;
        }

        targetFileFullName_xPlatform = directory + "\" +
          subDirectoryName_xPlatform + "\" + targetFileName_xPlatform;
        if(File.Exists(targetFileFullName_xPlatform)) {
          name = targetFileFullName_xPlatform;
          break;
        }
      }

      // Если найден файл, соответствующий нашей версии AutoCAD, то возвращаем 
      // соответтствующий ему объект FileInfo.
      if(File.Exists(name)) {
        return new FileInfo(name);
      }
      // Если соответствия не найдено, то продолжаем поиск, последовательно 
      // проверяя наличие подходящего файла для более ранних версий AutoCAD
      else {
        if(minor == 0) {
          minor = 3;
          --major;
        }
        else {
          --minor;
        }

        Version version = new Version(major, minor);
        if(version < minVersion)
          return null;
        FileInfo file = FindFile(fileFullName, new Version(major, minor),
          minVersion);
        return file;
      }
    }

    /// <summary>
    /// Код данного метода выполняется при завершении работы AutoCAD.
    /// </summary>
    public void Terminate() {
    }
  }
}