Приложение с плагинами
От: Аноним  
Дата: 21.07.06 07:14
Оценка:
Нужно сделать следующее: есть приложение, где форма оформленна для MDI. Есть директория, кде находятся сборки. Приложения должно просматривать всю директорию и загружать находящиеся там сборки. Сборки должны добавлять на главную форму свои пункты меню, и кнопки на панель инструментов.И дальше уже при выборе нужных пунктов меню, должны вызывать функции и показываться формы находящиеся в сборках.

Как это реализовать?
Скажите, в какую сторону копать?

Зарание большое спасибо!
Re: Приложение с плагинами
От: Andrew Merkulov Россия www.ibprovider.com
Дата: 21.07.06 08:05
Оценка: +1
Здравствуйте, Аноним, Вы писали:

А>Нужно сделать следующее: есть приложение, где форма оформленна для MDI. Есть директория, кде находятся сборки. Приложения должно просматривать всю директорию и загружать находящиеся там сборки. Сборки должны добавлять на главную форму свои пункты меню, и кнопки на панель инструментов.И дальше уже при выборе нужных пунктов меню, должны вызывать функции и показываться формы находящиеся в сборках.


А>Как это реализовать?

А>Скажите, в какую сторону копать?

Подключаем сборки через Reflection
1. Загружаем сборку
/// <summary>
        /// Кеш загруженных сборок
        /// </summary>
        /// <param name="AssemblyFileName"></param>
        /// <returns></returns>
        private static Assembly GetAssembly(string AssemblyFileName)
        {
            if(AssemblyDictionary==null) AssemblyDictionary=new ListDictionary(); 
            
            if(AssemblyDictionary.Contains(AssemblyFileName))
                return AssemblyDictionary[AssemblyFileName] as Assembly;
            else
            {
                //проверка наличия файла
                if (!File.Exists(AppDomain.CurrentDomain.BaseDirectory + @"Plugins\" + AssemblyFileName))
                {
                    Debuger.LogDebug( "Не удалось найти плагин " + AppDomain.CurrentDomain.BaseDirectory + @"Plugins\" + AssemblyFileName,null,AM.Messaging.MessageType.Error);
                    return null;
                }
                //загружаем сборку плагина в этот домен
                Assembly ass = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + @"Plugins\" + AssemblyFileName); 
                 
                if(ass!=null)
                    AssemblyDictionary.Add(AssemblyFileName,ass);
                return ass;
            }
        }


2. Создаем экземпляр заданного класса в сборке
/// <summary>
        /// Загружает из заданной сборки заданный тип
        /// </summary>
        /// <param name="RequiredType">Имя файла сборки без указания пути</param>
        /// <param name="ConstructorArguments">Имя файла сборки без указания пути</param>
        /// <param name="AssemblyFileName">Имя файла сборки без указания пути</param>        
        public static object LoadInstanceOf(Type RequiredType, object[] ConstructorArguments, string AssemblyFileName)
        {            
            Assembly ass = GetAssembly(AssemblyFileName); if(ass==null) return null;
            
            try
            {
                //ищем входной объект плагина
                foreach (Type type in ass.GetTypes())
                {
                    if(type.BaseType == RequiredType)
                    {                        
                        //Создаем экземпляр класса
                        return Activator.CreateInstance(type,ConstructorArguments);                          
                    }
                }
                return null;
    
            }
            catch (Exception ex) //перехватываем исключения в плагинах
            {
                .......                
            }
            return null;
        }

        #endregion


Теперь по общим вопросам.
1. Я разделяю в плагине сборки конфигурацию (PluginConfiguration) и формы (PluginForm). Когда главная форма загружает необходимый плагин она загружает конфигурацию которая предоставляет информацию основной форме к примеру

PluginConfigurationInstance.GetMenuItems(MenuItemCategory.File);
PluginConfigurationInstance.GetToolBars();

и соответственно главная MDI форма выстраивает свои меню и тулбары в соответствии с данными полученными из PluginConfiguration.

2. Каждому пункту меню и кнопке тулбара в плагине соответствует некая PluginCommand (см паттерн Command). Она управляет доступностью элементов меню и кнопок в определенный момент а так же выполняет действие внутри плагина при нажатии пункта меню(кнопки тулбара)

В итоге как мне кажеться получается достаточно слабое связывание.

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

Это если вкратце


А>Зарание большое спасибо!


Немного обо мне, и моем круге
Мои профиль в LiveLib &mdash; книги
Re: Приложение с плагинами
От: ie Россия http://ziez.blogspot.com/
Дата: 21.07.06 08:24
Оценка:
Здравствуйте, <Аноним>, Вы писали:

А>Как это реализовать?

А>Скажите, в какую сторону копать?

Доходчиво описано тут: http://msdn.microsoft.com/msdnmag/issues/03/10/Plug-ins/
Советом Andrew Merkulov тоже пренебрегать не стоит.
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Превратим окружающую нас среду в воскресенье.
Re[2]: Приложение с плагинами
От: FlyDN  
Дата: 22.07.06 14:12
Оценка:
Здравствуйте, Andrew Merkulov, Вы писали:

У меня похожая задача единственное отличие это то что эти плгины нужно выгружать,
то есть нужно их грузить в отдельный домен
но главная форма в одном домене а дочернее окно в другом работать не будут.
Что делать?
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[3]: Приложение с плагинами
От: Константин Л.  
Дата: 22.07.06 14:31
Оценка:
Здравствуйте, FlyDN, Вы писали:

FDN>Здравствуйте, Andrew Merkulov, Вы писали:


FDN>У меня похожая задача единственное отличие это то что эти плгины нужно выгружать,

FDN>то есть нужно их грузить в отдельный домен
FDN>но главная форма в одном домене а дочернее окно в другом работать не будут.
FDN>Что делать?

а зачем выгружать? Просто убивать инстансы не пойдет?
Re[2]: Приложение с плагинами
От: Константин Л.  
Дата: 22.07.06 14:33
Оценка:
Здравствуйте, Andrew Merkulov, Вы писали:

имхо, вес хорошо. Вот только ход с PluginConfiguration не позволяет делать не persistent кнопки и т.п. Я юы сделал как в Office, те позволял бы плагину самому добавлять кнопки когда нужно.
Re[4]: Приложение с плагинами
От: FlyDN  
Дата: 22.07.06 14:40
Оценка:
Здравствуйте, Константин Л., Вы писали:

КЛ>а зачем выгружать? Просто убивать инстансы не пойдет?


К сожалению нет.
Эти модули будут компилироваться и загружаться во время работы приложения,
изменяться и опять компилироваться и загружаться.
Если их не выгружать то рано или поздно их будет загружено очень много.
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[5]: Приложение с плагинами
От: Константин Л.  
Дата: 22.07.06 14:48
Оценка:
Здравствуйте, FlyDN, Вы писали:

FDN>Здравствуйте, Константин Л., Вы писали:


КЛ>>а зачем выгружать? Просто убивать инстансы не пойдет?


FDN>К сожалению нет.

FDN>Эти модули будут компилироваться и загружаться во время работы приложения,
FDN>изменяться и опять компилироваться и загружаться.
FDN>Если их не выгружать то рано или поздно их будет загружено очень много.

а разбить плагин на 2 сборки, одна из которых будет показывать UI и не выгружаться?
Re[6]: Приложение с плагинами
От: FlyDN  
Дата: 22.07.06 15:08
Оценка:
Здравствуйте, Константин Л., Вы писали:

КЛ>а разбить плагин на 2 сборки, одна из которых будет показывать UI и не выгружаться?


UI и будет зависеть от кода модуля и будет меняться.
Например как студия компилит код формы и отображает ее.
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[2]: Приложение с плагинами
От: Alexey Axyonov Украина  
Дата: 23.07.06 11:33
Оценка:
Здравствуйте, Andrew Merkulov, Вы писали:

AM>2. Создаем экземпляр заданного класса в сборке

AM>
AM>/// <summary>
AM>        /// Загружает из заданной сборки заданный тип
AM>        /// </summary>
AM>        /// <param name="RequiredType">Имя файла сборки без указания пути</param>
AM>        /// <param name="ConstructorArguments">Имя файла сборки без указания пути</param>
AM>        /// <param name="AssemblyFileName">Имя файла сборки без указания пути</param>        
AM>        public static object LoadInstanceOf(Type RequiredType, object[] ConstructorArguments, string AssemblyFileName)
AM>        {            
AM>            Assembly ass = GetAssembly(AssemblyFileName); if(ass==null) return null;
            
AM>            try
AM>            {
AM>                //ищем входной объект плагина
AM>                foreach (Type type in ass.GetTypes())
AM>                {
AM>                    if(type.BaseType == RequiredType)
AM>                    {                        
AM>                        //Создаем экземпляр класса
AM>                        return Activator.CreateInstance(type,ConstructorArguments);                          
AM>                    }
AM>                }
AM>                return null;
    
AM>            }
AM>            catch (Exception ex) //перехватываем исключения в плагинах
AM>            {
AM>                .......                
AM>            }
AM>            return null;
AM>        }

AM>        #endregion 
AM>


Сдается мне что вместо выделенного должно быть как-так так:

public static object LoadInstanceOf(string RequiredTypeName, object[] ConstructorArguments, string AssemblyFileName)
{            
    Assembly ass = GetAssembly(AssemblyFileName); if(ass==null) return null;
    
    try
    {
        Type RequiredType = ass.GetType(RequiredTypeName, true);
        //Создаем экземпляр класса
        return Activator.CreateInstance(RequiredType,ConstructorArguments);                          
    }
    catch (Exception ex) //перехватываем исключения в плагинах
    {
        .......                
    }
    return null;
}


Поскольку если сборка с типом еще не загружена, то экземпляр Type RequiredType описывающий тип из этой сборки взять будет просто неоткуда.
... << RSDN@Home 1.2.0 alpha rev. 655>>
Re[3]: Приложение с плагинами
От: Andrew Merkulov Россия www.ibprovider.com
Дата: 24.07.06 06:32
Оценка:
Здравствуйте, Alexey Axyonov, Вы писали:

AA>Здравствуйте, Andrew Merkulov, Вы писали:


AM>>2. Создаем экземпляр заданного класса в сборке

AM>>
AM>>/// <summary>
AM>>        /// Загружает из заданной сборки заданный тип
AM>>        /// </summary>
AM>>        /// <param name="RequiredType">Имя файла сборки без указания пути</param>
AM>>        /// <param name="ConstructorArguments">Имя файла сборки без указания пути</param>
AM>>        /// <param name="AssemblyFileName">Имя файла сборки без указания пути</param>        
AM>>        public static object LoadInstanceOf(Type RequiredType, object[] ConstructorArguments, string AssemblyFileName)
AM>>        {            
AM>>            Assembly ass = GetAssembly(AssemblyFileName); if(ass==null) return null;
            
AM>>            try
AM>>            {
AM>>                //ищем входной объект плагина
AM>>                foreach (Type type in ass.GetTypes())
AM>>                {
AM>>                    if(type.BaseType == RequiredType)
AM>>                    {                        
AM>>                        //Создаем экземпляр класса
AM>>                        return Activator.CreateInstance(type,ConstructorArguments);                          
AM>>                    }
AM>>                }
AM>>                return null;
    
AM>>            }
AM>>            catch (Exception ex) //перехватываем исключения в плагинах
AM>>            {
AM>>                .......                
AM>>            }
AM>>            return null;
AM>>        }

AM>>        #endregion 
AM>>


AA>Сдается мне что вместо выделенного должно быть как-так так:


AA>
AA>public static object LoadInstanceOf(string RequiredTypeName, object[] ConstructorArguments, string AssemblyFileName)
AA>{            
AA>    Assembly ass = GetAssembly(AssemblyFileName); if(ass==null) return null;
    
AA>    try
AA>    {
AA>        Type RequiredType = ass.GetType(RequiredTypeName, true);
AA>        //Создаем экземпляр класса
AA>        return Activator.CreateInstance(RequiredType,ConstructorArguments);                          
AA>    }
AA>    catch (Exception ex) //перехватываем исключения в плагинах
AA>    {
AA>        .......                
AA>    }
AA>    return null;
AA>}
AA>


AA>Поскольку если сборка с типом еще не загружена, то экземпляр Type RequiredType описывающий тип из этой сборки взять будет просто неоткуда.


Если бы тип был описан внутри сборки плагина то да. Но если вы посмотрите внимательно в коде:
if(type.BaseType  == RequiredType)

Типы прописанные внутри плагина сравниваются с неким базовым типом, который прописан не в плагине. RequiredType — это и есть базовая конфигурация, а все конкретные конфигурации наследдуются от неё и живут в сборках-плагинах.

условная программа "Главное MDI окно" знает только о базовой конфигурации, ничего не зная о типах в плагине.


Немного обо мне, и моем круге
Мои профиль в LiveLib &mdash; книги
Re[3]: Приложение с плагинами
От: Andrew Merkulov Россия www.ibprovider.com
Дата: 24.07.06 06:40
Оценка:
Здравствуйте, Константин Л., Вы писали:

КЛ>Здравствуйте, Andrew Merkulov, Вы писали:


КЛ>имхо, вес хорошо. Вот только ход с PluginConfiguration не позволяет делать не persistent кнопки и т.п. Я юы сделал как в Office, те позволял бы плагину самому добавлять кнопки когда нужно.


Поясните пожалуйста, что такое "persistent кнопки"? Наверное имеется в виду те, которые появляются при создании дочернего окна к примеру?
С кнопками скорее всего так и есть для Net 1.1. Т.к. Winforms не позволяют Merge-ить тулбары как к примеру менюшки дочерних и главной формы.
Думается мне что это проблема решена в Net 2?

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


Немного обо мне, и моем круге
Мои профиль в LiveLib &mdash; книги
Re[3]: Приложение с плагинами
От: Andrew Merkulov Россия www.ibprovider.com
Дата: 24.07.06 06:43
Оценка:
Здравствуйте, FlyDN, Вы писали:

FDN>Здравствуйте, Andrew Merkulov, Вы писали:


FDN>У меня похожая задача единственное отличие это то что эти плгины нужно выгружать,

FDN>то есть нужно их грузить в отдельный домен
FDN>но главная форма в одном домене а дочернее окно в другом работать не будут.
FDN>Что делать?

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

А в чем замес ? почему не работают в различных доменах?


Немного обо мне, и моем круге
Мои профиль в LiveLib &mdash; книги
Re[4]: Приложение с плагинами
От: Константин Л.  
Дата: 24.07.06 09:24
Оценка:
Здравствуйте, Andrew Merkulov, Вы писали:

AM>Здравствуйте, Константин Л., Вы писали:


КЛ>>Здравствуйте, Andrew Merkulov, Вы писали:


КЛ>>имхо, вес хорошо. Вот только ход с PluginConfiguration не позволяет делать не persistent кнопки и т.п. Я юы сделал как в Office, те позволял бы плагину самому добавлять кнопки когда нужно.


AM>Поясните пожалуйста, что такое "persistent кнопки"? Наверное имеется в виду те, которые появляются при создании дочернего окна к примеру?

AM>С кнопками скорее всего так и есть для Net 1.1. Т.к. Winforms не позволяют Merge-ить тулбары как к примеру менюшки дочерних и главной формы.
AM>Думается мне что это проблема решена в Net 2?

честно, не знаю. У меня практически нет опыта написания WinForms аппликэйшенов. Я имел ввиду, что плагин сам решает, когда и куда что добавить. В твоем же случае это делается самим приложением и при загрузке. Те плагин не может а рантайме добавлять и удалять кнопки.

AM>Решение: можно ввести дополнительный интерфейс и при создании дочернего окна добавлять к главному тулбары, запрашивая их у PluginConfiguration, которая будет брать их у конкретного создаваемого окна, являясь в свою очередь фасадом для главной формы.
Re[5]: Приложение с плагинами
От: Andrew Merkulov Россия www.ibprovider.com
Дата: 24.07.06 10:55
Оценка:
Здравствуйте, Константин Л., Вы писали:

КЛ>честно, не знаю. У меня практически нет опыта написания WinForms аппликэйшенов. Я имел ввиду, что плагин сам решает, когда и куда что добавить. В твоем же случае это делается самим приложением и при загрузке. Те плагин не может а рантайме добавлять и удалять кнопки.


В моем случае это и не нужно. Если понадобится когда-нибудь я не вижу проблемы в том чтобы встроить данную функциональность в существующую реализацию. Можно например воспользоватся эвентами . К примеру
заменить методы
PluginConfiguration.GetMenuItems(...)
PluginConfiguration.GetToolbarItems(...)


на события
PluginConfiguration.ToolButtonAdd(Button)
PluginConfiguration.MenuItemAdd(MenuItem)


Я не хочу чтобы плагин видел MainForm.MainMenu и MainForm.Toolbar (или различные вариации). Вариант со слабым связыванием мне нравиться больше. Для офиса такое поведение не подходит т.к. документы и их VBA программы вовсю используют объектную модель офиса, а у меня плагины разнородны и не пользуют единый Workflow. "Главное MDI окно" (Консоль приложений) является лишь оболочкой для запуска плагинов, предоставляя им некий общий функционал: подключение к БД, права пользователя, механизм обмена сообщениями, хранение настроек и т.п.

В офисе все выглядит так
ThisDocument.CommandBars.Add ("name")
ThisDocument.CommandBars(0).Controls.Add( ... )


Что в принципе красиво и в будущем возможно я пересмотрю их взаимодействие в сторону расширения паттерна Command, добавлю ему к существующим свойства Enabled,Cheked, Name, свойство Icon и буду созданные в плагине команды добавлять по аналогии с офисом:
Application.CommandManager.Add("Category",Position, Command, IsTemp)

создавая в меню и тулбарах соответствующие кнопки или целые Bar-ы


Немного обо мне, и моем круге
Мои профиль в LiveLib &mdash; книги
Re[2]: Приложение с плагинами
От: Alexander_Demchenko Россия  
Дата: 24.07.06 13:47
Оценка:
Я загрузку делаю так. Классы, которые экспортируются плагином должны реализовывать некоторый интерфейс IPlugin. Чтобы узнать, что данный класс нужно загрузить, он должен быть помечен некоторым ненаследуемым аттрибутом PluginAttribute. Казалось бы, достаточно и того, что класс реализует IPlugin, но, мне кажется, с атрибутом удобнее, это позволит внутри сборки с плагином реализовавывать, например, иерархию, когда уже базовый класс реализует IPlugin, а экспортировать нужно только производный. В общем, код загрузки у меня выглядит так:


        public void LoadPlugins(string pluginsPath)
        {
            const string searchPattern = "*.dll";
            try
            {
                string[] files = Directory.GetFiles(pluginsPath, searchPattern, SearchOption.AllDirectories);
                List<Assembly> assemblies = new List<Assembly>(10);
                Assembly assembly;
                foreach (string file in files)
                {
                    try
                    {
                        assembly = Assembly.LoadFrom(file);
                        assemblies.Add(assembly);
                    }
                    catch (BadImageFormatException exception)
                    {
                        ///TODO: Запись в лог, сообщение администратору и т.п.
                        ///о том, что файл не является валидной сборкой
                    }
                    catch (FileLoadException exception)
                    {
                        ///TODO: Запись в лог, сообщение администратору и т.п.
                        ///об ошибке загрузки файла
                    }
                    catch (SecurityException exception)
                    {
                        ///TODO: Запись в лог, сообщение администратору и т.п.                    
                    }
                }
                foreach (Assembly a in assemblies)
                {
                    Type[] types = a.GetExportedTypes();
                    foreach (Type type in types)
                    {
                        if (typeof(IPlugin).IsAssignableFrom(type) && !type.IsAbstract &&
                                type.IsDefined(typeof(PluginAttribute), false))
                        {
                            try
                            {
                                _plugins.Add(Activator.CreateInstance(type) as IPlugin);
                            }
                            catch (MissingMethodException exception)
                            {
                                ///TODO: у плагина нет конструктора без параметров.
                                ///Запись в лог, сообщение администратору и т.п.
                            }
                        }
                    }
                }
            }
            catch (DirectoryNotFoundException exception)
            {
                ///TODO: Запись в лог, сообщение администратору и т.п.
                ///о том, что директория не найдена                
            }
        }


Может кому не нравится такой подход? Тогда жду критики.
Re[6]: Приложение с плагинами
От: Константин Л.  
Дата: 24.07.06 14:50
Оценка:
Здравствуйте, Andrew Merkulov, Вы писали:

Отлично
Re[3]: Приложение с плагинами
От: IB Австрия http://rsdn.ru
Дата: 24.07.06 17:28
Оценка:
Здравствуйте, Alexander_Demchenko, Вы писали:

A_D>Может кому не нравится такой подход? Тогда жду критики.

Ты ближе всех подошел к самому кошерному решению..
Большие дядьки пользуются для таких вещей паттерном IServiceProvider, AVK как раз рассказывал об этом на прошлой встрече RSDN User Group.
... [RSDN@Home 1.2.0 alpha rev. 619]
Мы уже победили, просто это еще не так заметно...
Re[4]: Приложение с плагинами
От: FlyDN  
Дата: 26.07.06 05:37
Оценка:
Здравствуйте, Andrew Merkulov, Вы писали:

AM>А в чем замес ? почему не работают в различных доменах?


При попытке добавить форме контрол из другого домена получаем RemotingException
"Remoting cannot find field 'parent' on type 'System.Windows.Forms.Control'."
... << RSDN@Home 1.1.4 beta 7 rev. 447>>
Re[5]: Приложение с плагинами
От: Andrew Merkulov Россия www.ibprovider.com
Дата: 26.07.06 07:35
Оценка:
Здравствуйте, FlyDN, Вы писали:

FDN>Здравствуйте, Andrew Merkulov, Вы писали:


AM>>А в чем замес ? почему не работают в различных доменах?


FDN>При попытке добавить форме контрол из другого домена получаем RemotingException

FDN>"Remoting cannot find field 'parent' on type 'System.Windows.Forms.Control'."

Да действительно не работает . Если найдете решение опубликуйте его. Интересно как обойти эти грабли. С одной стороны нужно выгружать домен а с другой стороны организовать взаимодействие между формами различных доменов


Немного обо мне, и моем круге
Мои профиль в LiveLib &mdash; книги
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.