IExtensionApplication - подводные камни.

Дата публикации: 23.12.2010
Состояние: завершена
Дата редактирования: не редактировалась

Краткая справка

    Для тех, кто не знает, что это за интерфейс, поясню: Autodesk.AutoCAD.Runtime.IExtensionApplication предназначен для автоматического старта вашего кода, как только будет произведена загрузка в AutoCAD сборки, в составе которой определён класс, реализующий этот интерфейс. После загрузки такой библиотеки, AutoCAD автоматически запускает на выполнение код, находящийся в методе Initialize(), который определён в составе указанного интерфейса.

Предыстория

    Потребовалось мне создать библиотеку, в составе которой были бы определены абстрактные классы с реализованным в них базовым функционалом и различными виртуальными методами, которые необходимы мне почти в любом плагине AutoCAD. Одним из таких классов был ExtensionApplicationTemplate, реализовывающий IExtensionApplication. Созданную библиотеку протестировал и дал ей название - AutoCAD2009_PluginBaseClasses.

    После этого, я создал шаблон проекта MS Visual Studio 2010, ориентированный на разработку плагинов AutoCAD. Помимо всего прочего, в составе указанного шаблона, мною была подключена ссылка на AutoCAD2009_PluginBaseClasses и написаны два класса, унаследованные от абстрактных классов, определённых в составе AutoCAD2009_PluginBaseClasses. Один из таких классов - AcadExtension, унаследован от ExtensionApplicationTemplate, Шаблону назначил имя - AutoCAD_2009_Plugin.

История

    На основе шаблона AutoCAD_2009_Plugin создаю новый проект. В методе Initialize() класса AcadExtension размещаю нужный мне код. Ставлю брэйкпоинт на вход в Initialize(). В режиме отладки запускаю AutoCAD и с помощью Netload загружаю свой код. Результат меня несколько удивил - брэйкпоинт не сработал. Это означает одно - код, упакованный в метод Initialize() не был запущен AutoCAD'ом, несмотря на то, что класс AcadExtension реализовывает нужный интерфейс. Нужно разобраться с этой проблемой...
    Добавляю в проект новый класс (MyClass), реализующий интерфейс IExtensionApplication. Теперь код выглядит так:

   1:  namespace Bushman.Autodesk.AutoCAD {
   2:   
   3:      /// <summary>
   4:      /// AutoCAD extension plugin (runing with loading)
   5:      /// </summary>
   6:      public sealed class AcadExtension : ExtensionApplicationTemplate {
   7:   
   8:          public AcadExtension()
   9:              : base() {
  10:   
  11:          }
  12:          /// <summary>
  13:          /// Initialize method
  14:          /// </summary>
  15:          public override void Initialize() {
  16:              base.Initialize();
  17:              //put your code here if you need it
  18:          }
  19:      }
  20:   
  21:      public class MyClass : IExtensionApplication {
  22:   
  23:          public void Initialize() {
  24:              //throw new NotImplementedException();
  25:          }
  26:   
  27:          public void Terminate() {
  28:              //throw new NotImplementedException();
  29:          }
  30:      }    
  31:  }

    Ставлю брэйкпоинты в строках 15 и 23. Снова запускаю AutoCAD и загружаю в него библиотеку. В этот раз брэйкпоинт сработал в строке 15. Обратите внимание - в этот раз AutoCAD смог благополучно распознать в нашем классе интерфейс IExtensionApplication и запустить на исполнение именно то, что нам нужно!

    Завершаю отладку, после чего комментирую код строк 21-30. Снова в режиме отладки запускаю AutoCAD и загружаю в него библиотеку - брэйкпоинт не срабатывает. 

    Идём далее... Меняем в редакторе кода местами определения классов. Теперь это выглядит так:

   1:  namespace Bushman.Autodesk.AutoCAD {
   2:   
   3:      public class MyClass : IExtensionApplication {
   4:   
   5:          public void Initialize() {
   6:              //throw new NotImplementedException();
   7:          }
   8:   
   9:          public void Terminate() {
  10:              //throw new NotImplementedException();
  11:          }
  12:      }    
  13:   
  14:      /// <summary>
  15:      /// AutoCAD extension plugin (runing with loading)
  16:      /// </summary>
  17:      public sealed class AcadExtension : ExtensionApplicationTemplate {
  18:   
  19:          public AcadExtension()
  20:              : base() {
  21:   
  22:          }
  23:          /// <summary>
  24:          /// Initialize method
  25:          /// </summary>
  26:          public override void Initialize() {
  27:              base.Initialize();
  28:              //put your code here if you need it
  29:          }
  30:      }
  31:  }

    Ставлю брэйкпоинты в строках 5 и 26. Снова запускаю AutoCAD и загружаю в него библиотеку. В этот раз брэйкпоинт сработал в строке 5. Т.е. AutoCAD подхватывает тот класс, который в редакторе кода определён раньше (среди классов, реализующих IExtensionApplication).

    Завершаю отладку, после чего в исходный код добавляю ещё несколько классов реализующих тот же интерфейс:

   1:  namespace Bushman.Autodesk.AutoCAD {
   2:   
   3:      /// <summary>
   4:      /// AutoCAD extension plugin (runing with loading)
   5:      /// </summary>
   6:      public sealed class AcadExtension : ExtensionApplicationTemplate {
   7:   
   8:          public AcadExtension()
   9:              : base() {
  10:                  bool hasInterface = typeof(AcadExtension).GetInterface("Autodesk.AutoCAD.Runtime.IExtensionApplication") == null ? false : true;
  11:   
  12:          }
  13:          /// <summary>
  14:          /// Initialize method
  15:          /// </summary>
  16:          public override void Initialize() {
  17:              base.Initialize();
  18:              //put your code here if you need it
  19:          }
  20:   
  21:          /// <summary>
  22:          /// It code will run if document is first time activated
  23:          /// </summary>
  24:          protected override void FirstActivation() {
  25:              base.FirstActivation();
  26:              //put your code here if you need it
  27:              Drawing.Editor.WriteMessage("\nПервая активация документа\n");
  28:          }
  29:      }
  30:   
  31:      public class MyClass : IExtensionApplication {
  32:   
  33:          public void Initialize() {
  34:              //throw new NotImplementedException();
  35:          }
  36:   
  37:          public void Terminate() {
  38:              //throw new NotImplementedException();
  39:          }
  40:      }
  41:      public class MyClass2 : IExtensionApplication {
  42:   
  43:          public void Initialize() {
  44:              //throw new NotImplementedException();
  45:          }
  46:   
  47:          public void Terminate() {
  48:              //throw new NotImplementedException();
  49:          }
  50:      }
  51:      public class MyClass3 : IExtensionApplication {
  52:   
  53:          public void Initialize() {
  54:              //throw new NotImplementedException();
  55:          }
  56:   
  57:          public void Terminate() {
  58:              //throw new NotImplementedException();
  59:          }
  60:      }
  61:  }

    Во всех классах ставлю брэйкпоинты напротив метода Initialize(). Снова запускаю AutoCAD и загружаю в него библиотеку. Брэйкпоинт снова сработал в том классе, который в редакторе кода стоял первым.

Анализирование ситуации

    В составе .net-плагина AutoCAD потенциально может быть определено сколько угодно классов, реализующих интерфейс IExtensionApplication, но следует учитывать то, что при загрузке будет запущен код только того класса, который будет найден первым. Т.о. нет смысла создавать в составе одной сборки несколько реализаций IExtensionApplication. Кроме того, если таких классов всё же несколько, и они к тому же размещаются по нескольким файлам исходного кода, то определить, какой именно класс будет использоваться автокадом - можно только экспериментально, расставив брэйкпоинты по методам Initialize(). 

    Отсюда напрашивается логический вывод - в библиотеке плагина следует определять не более одного невиртуального, public-класса, реализующего интерфейс IExtensionApplication. Кроме того, по хорошему следовало бы, чтобы AutoCAD генерировал исключение в том случае, если бы он обнаруживал несколько таких классов. Но, к сожалению, разработчиками AutoCAD этот момент не реализован. Ну да ладно, вернёмся к сути нашей проблемы... 

Вопрос

    Как сделать так, чтобы код моего класса, унаследованного от класса, реализующего IExtensionApplication, автоматически запускался при его загрузке в AutoCAD, не требуя извращений, типа определения ещё одного класса, реализующего IExtensionApplication и размещение его в редакторе кода ниже, чем мой класс?

Ответ

    Для того, чтобы AutoCAD смог распознать наш класс правильно, придётся пойти на маленькую хитрость:

   1:  public sealed class AcadExtension : bush.ExtensionApplicationTemplate, IExtensionApplication

    Нужно повторно указать в определении нашего класса, что он наследует IExtensionApplication, при этом в коде самого класса реализовывать интерфейс не нужно, поскольку он и так уже реализован в базовом классе. В этом случае AutoCAD сможет благополучно распознать наш класс, как реализующий IExtensionApplication и запустить его код после загрузки библиотеки.

Послесловие

    Необходимость принудительного указания интерфейса обусловлена тем, что разработчики AutoCAD криво реализовали проверку класса на то, реализует ли он определённый интерфейс. Обратите внимание - если в библиотеке имеется ещё хотя бы один класс, реализующий IExtensionApplication и наш класс определён раньше них, то в этом случае AutoCAD всё же способен подхватить и загрузить именно наш класс. Т.о. налицо баг разработчиков. Это нужно очень постараться, чтобы не суметь определить наличие/отсутствие реализации классом интерфейса. Делается это элементарно:

1:  bool hasInterface = typeof(AcadExtension).GetInterface("Autodesk.AutoCAD.Runtime.IExtensionApplication") == null ? false : true;

    Почему разработчики Autodesk не смогли это сделать корректно и вообще зачем, имея на руках Reflection, они изобретали свой "велосипед", который к тому же работает криво (в чём мы смогли убедиться выше) - это для меня загадка...

Comments