Dependency Injection и создание классов команд

Автор Тема: Dependency Injection и создание классов команд  (Прочитано 15025 раз)

0 Пользователей и 4 Гостей просматривают эту тему.

Оффлайн StalsoАвтор темы

  • ADN OPEN
  • Сообщений: 30
  • Карма: 0
Здравствуйте. Интересует вот какой вопрос. Хотелось бы по правильному организовать написание крупных плагинов. Для этого хочется иметь модульную структуру приложения. То есть, разбить функции плагина на набор сервисов (или подмодулей), которые я регистрирую с помощью какого-нибудь IoC контейнера  в методе Initialize() сборки. Предположим, что у нас в плагине есть три сервиса. Один отвечает за какую-нибудь хитрую подсветку и зуммирование на чертеже и реализует интерфейс IHighlighter, остальные два (Module1, Module2) выполняют какую то работу в командах (то есть, у них есть методы, помеченные атрибутом  [CommandMethod("")]), выбирая элементы чертежа и подвечивая их с помощью объекта класса, реализующего IHighlighter. Хотелось бы иметь возможность регистрировать их и проводить необходимую инициализацию:

Код - C# [Выбрать]
  1.         public class MISAEApp : IExtensionApplication
  2.     {
  3.         public void Initialize()
  4.         {
  5.              var container = new SomeServiceContainer();                
  6.              container.Register<IHighlighter,MyHighlighter>();
  7.              container.Register<Module1>();
  8.         }
  9.  
  10.  
  11.         public void Terminate()
  12.         {
  13.            
  14.         }
  15.     }
  16.  

Первый вопрос заключается в том, что хотелось бы иметь возможность указать автокаду не регистрировать команды Module2,не смотря на то, что у него есть методы, помеченные атрибутом  [CommandMethod("")] , если он не подключен мною в методе Initialize(). На сегодняшний день я знаю только один способ сделать это. Пометить нужные мне классы команд атрибутом CommandClass. Но мне кажется , что это костыль. Ведь если я хочу по какой то причине выключить модуль, мне надо лезть в код класса команд и убирать этот аттрибут. А потом, если я хочу включить модуль, то, несмотря на мои настройки инициализации мне надо слазить в этот класс и пометить его этим аттрибутом. 

Второй вопрос сложнее . Хотелось бы привести пример желаемого и не желаемого кода Module1 :

Код - C# [Выбрать]
  1.         // Круто
  2.         public class Module1
  3.         {
  4.                 IHighlighter Highlighter;
  5.                
  6.                 public ModuleCommands(IHighlighter highlighter)
  7.                 {
  8.                         Highlighter  = highlighter;
  9.                 }
  10.                
  11.                 [CommandMethod("cmd")]
  12.                 public void MyCmd()
  13.                 {
  14.                         var res = doSomeWork();
  15.                         Highlighter.Show(res);
  16.                 }
  17.                 private object doSomeWork()
  18.                 {
  19.                         // dosomework;
  20.                 }
  21.         }
  22.  
  23.  
  24.         // Не круто
  25.         public class Module1
  26.         {
  27.                 IHighlighter Highlighter
  28.                 {
  29.                         get
  30.                         {
  31.                                 return SomeServiceContainer.GetInstance<IHighlighter>();
  32.                         }
  33.                 }      
  34.                
  35.                 [CommandMethod("cmd")]
  36.                 public void MyCmd()
  37.                 {
  38.                         var res = doSomeWork();
  39.                         Highlighter.Show(res);
  40.                 }
  41.                 private object doSomeWork()
  42.                 {
  43.                         // dosomework;
  44.                 }
  45.         }
  46.  

Второй вариант меня не очень устраивает, поскольку это прямой пример антипаттерна ServiceLocator. Хочу инжектировать зависимости в конструкторе. Тобишь, иметь какую нибудь фабрику, которая при вызове команды создает нужный класс команд. Но у меня с этим две проблемы:

  • Я вообще не знаю, как управлять созданием класса команд.
  • Автокад создает класс команд один раз, при первом вызове команды, содержащейся в нем. А потом держит в памяти экземпляр этого класса. А мне надо, чтоб класс создавался по новой, при каждом вызове команды.

Я считаю, что если решить этот вопрос (если возможно) и сделать из классов команд подобие контроллеров в ASP.Net MVC (если кто знаком с технологией), то можно сделать разработку плагинов понятнее и улучшить тестируемость самих плагинов.

Оффлайн Александр Ривилис

  • Administrator
  • *****
  • Сообщений: 13882
  • Карма: 1787
  • Рыцарь ObjectARX
  • Skype: rivilis
Из всего, что ты написал, я понял, что тебе нужно:
1) Иметь возможность динамически регистрировать команды.
2) Иметь возможность динамически удалять зарегистрированные команды.
В описанном и документированном AutoCAD .NET API этой возможности нет. Всё построено на атрибутах CommandClassAttribute, и команды регистрируются при загрузке сборки в AutoCAD.
Однако, есть недокументированные методы:
Код - C# [Выбрать]
  1. // Autodesk.AutoCAD.Internal.Utils
  2. public unsafe static void AddCommand(string cmdGroupName, string cmdGlobalName, string cmdLocalName, CommandFlags cmdFlags, CommandCallback func);
  3. public unsafe static void RemoveCommand(string cmdGroupName, string cmdGlobalName);
С их помощью (аналогично тому, как это делается в ObjectARX) можно динамически регистрировать и удалять команды.
Не забывайте про правильное Форматирование кода на форуме
Создание и добавление Autodesk Screencast видео в сообщение на форуме
Если Вы задали вопрос и на форуме появился правильный ответ, то не забудьте про кнопку Решение

Отмечено как Решение Александр Ривилис 17-07-2019, 14:54:31

Оффлайн Александр Ривилис

  • Administrator
  • *****
  • Сообщений: 13882
  • Карма: 1787
  • Рыцарь ObjectARX
  • Skype: rivilis
Код - C# [Выбрать]
  1. using Autodesk.AutoCAD.Runtime;
  2. using Autodesk.AutoCAD.ApplicationServices;
  3. using Autodesk.AutoCAD.EditorInput;
  4. using Autodesk.AutoCAD.Internal;
  5.  
  6. // This line is not mandatory, but improves loading performances
  7. [assembly: ExtensionApplication(typeof(CommandUtils.MyPlugin))]
  8.  
  9. namespace CommandUtils
  10. {
  11.   public class MyPlugin : IExtensionApplication
  12.   {
  13.     void IExtensionApplication.Initialize()
  14.     {
  15.       Utils.AddCommand("RIVILIS", "TEST1", "ТЕСТ1", CommandFlags.Modal, CommandHandler1);
  16.       Utils.AddCommand("RIVILIS", "TEST2", "ТЕСТ2", CommandFlags.Modal, CommandHandler2);
  17.     }
  18.  
  19.     void IExtensionApplication.Terminate()
  20.     {
  21.       Utils.RemoveCommand("RIVILIS", "TEST1");
  22.       Utils.RemoveCommand("RIVILIS", "TEST2");
  23.     }
  24.     public void CommandHandler1()
  25.     {
  26.       Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  27.       ed.WriteMessage("\nЗапущена команда ТЕСТ1");
  28.     }
  29.     public void CommandHandler2()
  30.     {
  31.       Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
  32.       ed.WriteMessage("\nЗапущена команда ТЕСТ2");
  33.     }
  34.   }
  35. }


Не забывайте про правильное Форматирование кода на форуме
Создание и добавление Autodesk Screencast видео в сообщение на форуме
Если Вы задали вопрос и на форуме появился правильный ответ, то не забудьте про кнопку Решение

Оффлайн Дима_

  • ADN Club
  • ****
  • Сообщений: 473
  • Карма: 66
У меня dll'ки поделены на "функциональные" группы, все они загружаются через "сборку-загрузчик" при старте автокада, которая сверяет версию в "репризитории" БД и др. настройки "доступа" и уже в зависимости от полученных параметров загружает необходимое просто через Assembly.LoadFrom(...), сделано это правда больше для автоматического обновления пользователей.

Оффлайн Андрей Бушман

  • ADN Club
  • *****
  • Сообщений: 2000
  • Карма: 163
  • Пишу программки...
    • Блог
  • Skype: Compositum78
У меня такое ощущение, что автор темы применяя слова "сервисы", "контейнеры", "модули" и "подмодули", делает это интуитивно, вкладывая в них смысл, понятный только ему.

Цитировать
Первый вопрос заключается в том, что хотелось бы иметь возможность указать автокаду не регистрировать команды Module2,не смотря на то, что у него есть методы, помеченные атрибутом  [CommandMethod("")] , если он не подключен мною в методе Initialize(). На сегодняшний день я знаю только один способ сделать это. Пометить нужные мне классы команд атрибутом CommandClass.
Какая-то муть... Обозначенный атрибут как раз указывает на класс, в котором определены команды. Отсутствие атрибута на конечный результат не влияет - акад всё равно найдёт команды. Коим образом он указывает НЕ регистрировать команду?

Цитировать
Ведь если я хочу по какой то причине выключить модуль, мне надо лезть в код класса команд и убирать этот аттрибут. А потом, если я хочу включить модуль, то, несмотря на мои настройки инициализации мне надо слазить в этот класс и пометить его этим аттрибутом. 
Очередной фрагмент, который сложно уличить в ясности. Что подразумевается под "включить\выключить модуль"? Что вообще подразумевается под словом "модуль"? Сборка что ли?

Цитировать
Второй вопрос сложнее .
И снова без стакана не разобраться в сути вопроса... Ты не знаешь как работает паттерн Factory? Google забанили? :) См. здесь п.17.

Оффлайн Привалов Дмитрий

  • ADN Club
  • *****
  • Сообщений: 546
  • Карма: 119
Ты не знаешь как работает паттерн Factory?
Возможно и знает, но применяет не там где нужно.

То есть, разбить функции плагина на набор сервисов (или подмодулей), которые я регистрирую с помощью какого-нибудь IoC контейнера  в методе Initialize() сборки.

Непонятно зачем такая "гибкость" загрузки сервисов/подмодулей, регистрации команд.?
В теме смешаны вопросы архитектуры приложения и вопросы низкоуровневой реализации.
Стоит начать с архитектуры приложения.

Автору надо разобраться с вопросами:
один загрузчик в виде dll, который управляет загрузкой других модулей - dll?
Количество dll заранее не известно?
Как загрузчик должен узнавать о новых модулях? Каждый раз перекомпилировать загрузчик, изменяя список dll или сканировать папку и подгружать все dll, или информация о модулях хранится во внешнем файле?  Должен ли пользователь выбирать какие плагины загружать?

Действительно ли нужно загружать по выбору? Может проще загрузить все разом? (В NET обычно все подгружается и не выгружается до закрытия автокада.)

После общих вопросов уже можно приступать к реализации, коду и т.д.

Оффлайн Александр Пекшев aka Modis

  • ADN Club
  • *****
  • Сообщений: 1658
  • Карма: 366
  • Отец modplus.org
    • ModPlus
Ага! Мою идею реализовывают )))) Уже реализованную. Конечно не так "заумно" (я имею ввиду все эти страшные и незнакомые слова)
Лично я делал примерно так:
1. Есть dll, которая грузится в автокад и которая управляет загрузкой плагинов. Не забываем, что выгружать их нельзя. Достаточно Assembly.LoadFrom()
2. Есть программа (вне автокада), которая является неким сервисом по настройке - там пользователь включает/отключает плагины
3. Ну и, соответственно, есть некий файл конфигурации (достаточно обычного xml) с которым работают оба товарища из п.1,2 и в котором прописаны все настройки и предпочтения пользователя

И нечего мудрить - все должно быть просто, если нет иной необходимости ;)

Оффлайн Дима_

  • ADN Club
  • ****
  • Сообщений: 473
  • Карма: 66
Ну и, соответственно, есть некий файл конфигурации (достаточно обычного xml) с которым работают оба товарища из п.1,2
Вот от XML я в итоге полностью ушел (не считая экспорта для управляющих ЧПУ которые используют его) - в рамках предприятия ИХМО все удобней и практичней завязать на СУБД - минусов я не нашел (ну если только сеть упадет - но все равно все остальное без сети и БД так-же работать не будет).

Оффлайн Александр Пекшев aka Modis

  • ADN Club
  • *****
  • Сообщений: 1658
  • Карма: 366
  • Отец modplus.org
    • ModPlus
в рамках предприятия ИХМО все удобней и практичней завязать на СУБД
Ну я же говорю - нечего мудрить, если нет иной необходимости. В моем случае СУБД ну вот просто ни к чему) Достаточно обычного xml
Все зависит от функциональности - где-то достаточно xml, где-то лучше привязать базу данных...

Оффлайн StalsoАвтор темы

  • ADN OPEN
  • Сообщений: 30
  • Карма: 0
Какая-то муть... Обозначенный атрибут как раз указывает на класс, в котором определены команды. Отсутствие атрибута на конечный результат не влияет - акад всё равно найдёт команды. Коим образом он указывает НЕ регистрировать команду?

Гм. Так я вроде пояснил. Извините, если непонятно, ни разу не писатель. Речь идет об одной сборке. Я бы хотел иметь возможность самостоятельно управлять процессом регистрации команд ОДНОЙ сборки (Александр Ривилис показал, как это делать). Представьте, у меня в плагине есть класс команд, который выполняет определенные функции. В какой то момент времени разработки я хочу , чтобы эти команды не определялись автокадом. Например, если я хочу определить другой класс команд, который содержит такие же команды, но делает все немножко по другому. Для того, чтобы не загружались команды первого класса мне надо - либо помечать нужные мне классы команд атрибутом CommandClass (а из ненужных, соотвественно, его поубирать, тогда команды ненужных классов не будут загружены), либо выпилить первый класс из сборки, либо у первого класса закомментить все CommandMethod атрибуты. Мне кажется это неудобным. Хотелось бы иметь возможность где-то в одном месте плагина указывать, какие конкретно классы команд мне нужны.

И снова без стакана не разобраться в сути вопроса... Ты не знаешь как работает паттерн Factory? Google забанили?  См. здесь п.17.

А вот тут я вас уже не понял. Интересно, и как это я его должен применить, если я не имею возможности создавать классы команд? Я четко обрисовал требования. Хочу внедрять зависимости в классы команд. Как я могу написать фабрику и инжектировать в класс команд нужные зависимости, если автокад сам классы команд создает, причем один раз? Вот вам пример в MVC. Система принимает запрос, с помощью маршрутизации определяет, какой контроллер нужно выбрать (контроллер MVC вполне можно представить как аналог класса команд а автокаде). Далее вступает в дело переопределяемая  фабрика, которая управляет процессом создания экземпляра контроллера, в конструктор которого я могу инжектировать все, что ему надо, после выполнения запроса и возврата результата контроллер уничтожается. В автокаде я такого не знаю. Есть мысль сделать небольшой костыль. Навесить CommandStartReactor. В нем ловить нужные мне команды и пересоздавать с помощью фабрики класс команд, внедряя в него все, что мне нужно. Но я пока не знаю, прокатит ли это. Тут несколько узких мест которые мне нужно проверить.

  • Как по имени команды узнать какому классу она принадлежит?
  • Куда денется старый экземпляр класса команд?

Собственно для того, чтобы сделать то, что я хочу мне нужно:

  • Иметь возможность сопоставить где-нибудь в реакторе команду и класс, в котором она определена.
  • После сопоставления, иметь возможность создать САМОМУ экземпляр класса команд передавая в конструктор все  необходимые зависимости, которые я извлекаю из IoC контейнера.

С огромным уважением , Андрей, если Вы (или кто-либо еще) поможете с двумя последними вопросами, я реализую мою идею и пришлю Вам лично, чтобы вы ее оценили. Суть идеи в том, чтобы реализовать инжектирование зависимостей в классы команд (которые я обозвал модулями, что не совсем корректно, согласен) и некое подобие автокадовского OWIN (это второй этап), в котором вместо url выступают команды автокада.


Оффлайн Привалов Дмитрий

  • ADN Club
  • *****
  • Сообщений: 546
  • Карма: 119
Иметь возможность сопоставить где-нибудь в реакторе команду и класс, в котором она определена.
После сопоставления, иметь возможность создать САМОМУ экземпляр класса команд передавая в конструктор все  необходимые зависимости, которые я извлекаю из IoC контейнера.

Не слишком ли сложно? В автокаде обычно немного другие подходы, чем в ASP.Net.
Дело в том, что не стоит рассматривать команду автокада как класс или объект, который стоит генерировать автоматически. Автокад хранит список команд  "имя команды"-"метод". И расчитан этот механизм больше на расширение списка команд, которые знает автокад и может вызывать пользователь. К примеру создаешь кнопку и вешаешь вызов команды автокада. Для расширения функционала не обязательно создавать команды на лету.

Возможные варианты:
список из 10 команд и вызов их с параметрами.
Или как вариант вызов 10 команд, но функционал их зависит от настроек в другом месте и это тоже возможно.
Если создаешь свой интерфейс WPF/WinForms то можешь привязываться к своему функционалу без регистрации команд. Команды понадобяться только для вызова формы.
Можно подписаться на события автокада и опять же не прибегать к созданию команд.
Возможны другие сценарии без создания/переопределения команд.

Т.е. не стоит создавать/переопределять много команд автокада, и привязываться к ним, врятли это правильное решение.
Это может вызвать нестабильность или замедление работы, т.к. механизм регистрации команд в автокаде не факт что расчитан на такой "спам регистраций".

Оффлайн StalsoАвтор темы

  • ADN OPEN
  • Сообщений: 30
  • Карма: 0
Не слишком ли сложно? В автокаде обычно немного другие подходы, чем в ASP.Net.
Дело в том, что не стоит рассматривать команду автокада как класс или объект, который стоит генерировать автоматически. Автокад хранит список команд  "имя команды"-"метод". И расчитан этот механизм больше на расширение списка команд, которые знает автокад и может вызывать пользователь. К примеру создаешь кнопку и вешаешь вызов команды автокада. Для расширения функционала не обязательно создавать команды на лету.

Но что делать, если у меня в коде команды используются другие компоненты моего плагина. В стартовом посте я привел пример IHighlighter .  Я  хочу уйти от того, чтобы обращаться с статическому ServiceLocator для получения экземпляра IHighlighter

Оффлайн Привалов Дмитрий

  • ADN Club
  • *****
  • Сообщений: 546
  • Карма: 119
Но что делать, если у меня в коде команды используются другие компоненты моего плагина. В стартовом посте я привел пример IHighlighter .  Я  хочу уйти от того, чтобы обращаться с статическому ServiceLocator для получения экземпляра IHighlighter

..так мысли вслух
Ну ок есть статический репозиторий и каковы его функции?
Подгрузили в него список команд. ....дальше оповестили автокад?
И по ходу программы этот список должен меняться и автокад получать другой список команд или переопределенных команд.
А как такие команды должны вообще вызываться, интерфейс должен также меняться по ходу программы?
Из командной строки вызывать изменяемый список команд как-то сомнительно.
Нужен интерфейс, подстраивающийся под изменение команд?
Интерфейс автокада или собственный?
Если собственный, то не нужно регистрировать команды автокада.
Если интерфейс автокада то возможно, но зачем менять список команд? Можно делать неизменный список команд при загрузке и менять интерфейс отображая нужный в данный момент функционал.
Если нужно другое поведение команды, то можно использовать параметры передаваемые команде, либо связь с статическим классом настроек.

Может вообще отказаться от переопределения команд автокада и ввести к примеру промежуточный класс Action, Modul или какнибудь еще и в нем хранить экземпляр класса, который выполняет определенные действия.

..это все к чему, просто сомнения, что нужно именно оперировать списком команд автокада, меняя его по ходу программы.

Оффлайн StalsoАвтор темы

  • ADN OPEN
  • Сообщений: 30
  • Карма: 0
Решил вопрос посоветовавшись с ребятами из команды aspnet в гиттере. Оформлю это как статью и опубликую здесь, может кому будет полезно. Практически полностью содрал их подход. Восхищают меня такие ребята. Ни строчки кода в автокаде не написали, а поди ж ты, рабочие штуки советуют.)) Спасибо большое Александру Ривилису за подсказку с командами.

Оффлайн Александр Ривилис

  • Administrator
  • *****
  • Сообщений: 13882
  • Карма: 1787
  • Рыцарь ObjectARX
  • Skype: rivilis
Как по имени команды узнать какому классу она принадлежит?
Если это касается команд из твоей сборки, то сам и храни эту информацию. В ObjectARX эту информацию можно получить при помощи AcEdCommandStack::lookupCmd и AcEdCommandStack::lookupCmd2. В AutoCAD .NET API аналогов нет (ни документированных, ни недокументированных).
Куда денется старый экземпляр класса команд?
Никуда не денется, но если одноименная команда зарегистрирована позже, то будет работать именно её обработчик.
Можно воспользоваться из того же класса методами:
Код - C# [Выбрать]
  1. // Команда из ядра AutoCAD
  2. // Autodesk.AutoCAD.Internal.Utils
  3. [return: MarshalAs(UnmanagedType.U1)]
  4. public unsafe static bool IsCoreCommand(string name);
  5. // Команда уже зарегистрирована
  6. // Autodesk.AutoCAD.Internal.Utils
  7. [return: MarshalAs(UnmanagedType.U1)]
  8. public unsafe static bool IsCommandDefined(string cmdName);
  9. // Имя команды уже используется
  10. // Autodesk.AutoCAD.Internal.Utils
  11. public unsafe static CommandTypeFlags IsCommandNameInUse(string name);
Оформлю это как статью и опубликую здесь, может кому будет полезно.
Для этого у нас есть раздел: http://adn-cis.org/forum/index.php?board=19.0
А ссылку на тему со статьёй можешь положить сюда.
Не забывайте про правильное Форматирование кода на форуме
Создание и добавление Autodesk Screencast видео в сообщение на форуме
Если Вы задали вопрос и на форуме появился правильный ответ, то не забудьте про кнопку Решение