вторник, 5 августа 2014 г.

Последовательность действий 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() { }
    }
}

Комментариев нет: