<?xml version="1.0" encoding="utf-8"?> 
<rss version="2.0">

<channel>

<title>Just Developer Blog, заметки с тегом: Атрибуты в C#</title>
<link>http://bibobo.ru/tags/atributy-v-c/</link>
<description></description>
<generator>E2 (v3254; Aegea)</generator>

<item>
<title>Реализация механизма плагинов в программе</title>
<guid isPermaLink="true">http://bibobo.ru/all/realizaciya-mehanizma-plaginov-v-prilozhenii/</guid>
<link>http://bibobo.ru/all/realizaciya-mehanizma-plaginov-v-prilozhenii/</link>
<comments>http://bibobo.ru/all/realizaciya-mehanizma-plaginov-v-prilozhenii/</comments>
<description>&lt;p&gt;Рано или поздно практически любой разработчик популярной программы сталкивается с необходимостью реализации возможности написания плагинов для нее. Лучше, если данная возможность заложена изначально. Наиболее простым путем в реализации такого функционала является создание специального интерфейса (&lt;i&gt;например IPlugin&lt;/i&gt;). Данный интерфейс может содержать все необходимые методы и свойства для класса, работающего в инфраструктуре плагинов. Позже программа при загрузке сборки с плагином будет искать все классы, реализующие этот интерфейс, инстанциировать их как плагины и отображать их список в интерфейсе пользователя.&lt;/p&gt;
&lt;p&gt;В основе работы всей инфраструктуры плагинов лежит такая возможность Microsoft .NET Framework как Рефлексия (пространство имен &lt;i&gt;System.Reflection&lt;/i&gt;, более подробно &lt;a href="http://msdn.microsoft.com/ru-ru/library/System.Reflection(v=vs.110).aspx"&gt;можно почитать на MSDN&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;input id="button_plugins_machanism" type="button" onclick="toggle(cut_plugins_machanism, button_plugins_machanism)" value="Читать дальше" hiddenText="Свернуть" visibleText="Читать дальше"&gt;&lt;/p&gt;
&lt;div id="cut_plugins_machanism" style="display: none;" displayOld=""&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;У меня есть векторный редактор, который имеет три типа «кистей»:&lt;/p&gt;
&lt;p&gt;— Кисти рисования&lt;br /&gt;
— Кисти для редактирования уже нарисованных примитивов (обрезка линий, удлинение линий и т. п.)&lt;br /&gt;
— Кисти для реализации расчетного функционала (простой пример — посчитать длины определенных элементов на чертеже)&lt;br /&gt;
Будем использовать его как пример реализации инфраструктуры для работы плагинов.&lt;br /&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;В статье рассмотрим инфраструктуру плагинов, написанную для этого редактора, она будет состоять из:&lt;/p&gt;
&lt;p&gt;— Менеджера плагинов (реализует весь механизм загрузки и инстанциации плагинов)&lt;br /&gt;
— Набора специальных атрибутов (позволят добавить в метаданные дополнительную информацию о том или ином плагине)&lt;br /&gt;
— Несколько интерфейсов, позволяющих писать плагины к различному функционалу редактора&lt;br /&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Первым шагом необходимо реализовать интерфейс для каждого типа кисти (&lt;i&gt;исходные интерфейсы в редакторе довольно сложны, имеют много методов и свойств, в примере они намеренно упрощены&lt;/i&gt;)&lt;/p&gt;
&lt;p&gt;Обычно простейший интерфейс для плагина может выглядеть следующим образом:&lt;/p&gt;
&lt;pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;public interface IPlugin
{
	//Возвращает наименование плагина
	string Name { get; }

	//Инициализирует плагин
	void Init();

	//Метод, который собственно делает всю работу плагина
	void DoWork();
}&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;Из кода становится ясно, что после инстанциации плагинов достаточно получить имя каждого (&lt;i&gt;поле Name&lt;/i&gt;), вывести этот список в интерфейс. После чего когда пользователь выберет определенный плагин — вызвать его метод &lt;i&gt;Init()&lt;/i&gt;, который инициализирует его и затем вызвать метод &lt;i&gt;DoWork()&lt;/i&gt;, который сделает уже работу плагина.&lt;/p&gt;
&lt;p&gt;Используя такой интерфейс программе абсолютно по барабану что именно делает плагин, главное что она знает порядок вызова методов. Такой подход позволяет существенно расширить функционал исходной программы. Но вернемся к редактору.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Интерфейс, описывающий кисть рисования. Содержит основные методы, позволяющие клонировать кисть, отрисовать примитив кисти, инициализировать новый экземпляр кисти. Так же присутствует свойство-геттер, возвращающее строковый идентификатор кисти.&lt;/p&gt;
&lt;pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;public interface IDrawObject
{
	//Возвращает строковый ID кисти
	string Id { get; }
	
	//Клонирует кисть
	IDrawObject Clone();

	//Отрисовывает примитив кисти
	void Draw(ICanvas canvas, RectangleF unitrect);

	//Инициализирует новый экземпляр кисти
	void InitializeFromModel(UnitPoint point, DrawingLayer layer, ISnapPoint snap)
}&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Интерфейс, описывающий кисть редактирования. Содержит методы клонирования объекта и обработки нажатия мыши по экрану (позволяет реализовать логику работы кисти редактирования через последовательное выделение объектов, например выделили базовый объект, обрезали второй объект по базовому).&lt;/p&gt;
&lt;pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;public interface IEditTool
{
        //Возвращает строковый ID кисти
        string Id { get; }

        //Клонирует кисть
	IEditTool Clone();

        //Реализация логики кисти, обработка нажатия на экран
	public DrawObjectMouseDown OnMouseDown(ICanvas canvas, UnitPoint point, ISnapPoint snappoint)
}&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;
Интерфейс, описывающий расчетную кисть. Содержит методы, позволяющие клонировать кисть, а так же выполнить расчетный алгоритм, реализуемый кистью.
&lt;/p&gt;
&lt;pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;public interface ICalcTool
{
        //Возвращает строковый ID кисти
        string Id { get; }

        //Клонирует кисть
        ICalcTool Clone();

        //Запускает расчетный алгоритм кисти
        void PerformCalc(IModel model);
}&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;
Следующим этапом для каждого типа кисти необходимо реализовать атрибут, позволяющий добавить дополнительную информацию к кисти (ее имя, описание, имя файла иконки, хоткей для запуска и т. п.). Пример для кисти рисования:
&lt;/p&gt;
&lt;p&gt;&lt;input type="button" onclick="toggle(code1_realizaciya_mehanizma_plaginov_v_prilozhenii)" value="Нажмите, чтобы отобразить или спрятать код"&gt;&lt;/p&gt;
&lt;pre id="code1_realizaciya_mehanizma_plaginov_v_prilozhenii" style="display: none;"&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// Атрибут, описывающий графический объект
/// &amp;lt;/summary&amp;gt;
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class DrawToolAttribute : Attribute
{
        /// &amp;lt;summary&amp;gt;
        /// Имя кисти рисования
        /// &amp;lt;/summary&amp;gt;
        public string Name { get; private set; }
        /// &amp;lt;summary&amp;gt;
        /// Описание кисти рисования
        /// &amp;lt;/summary&amp;gt;
        public string Description { get; private set; }
        /// &amp;lt;summary&amp;gt;
        /// Изображение для кнопки кисти рисования
        /// &amp;lt;/summary&amp;gt;
        public string GlyphFileName { get; private set; }
        /// &amp;lt;summary&amp;gt;
        /// Горячая клавиша
        /// &amp;lt;/summary&amp;gt;
        public Keys Shortcut { get; private set; }

        public DrawToolAttribute()
        {
            Name = string.Empty;
            Description = string.Empty;
            GlyphFileName = string.Empty;
            Shortcut = Keys.None;
        }

        /// &amp;lt;summary&amp;gt;
        /// В конструкторе передаем все необходимые параметры для описания кисти
        /// &amp;lt;/summary&amp;gt;
        public DrawToolAttribute(string name, string description, string glyphFileName, Keys shortcut)
        {
            if (name == null) throw new ArgumentNullException(&amp;quot;name&amp;quot;);
            if (description == null) throw new ArgumentNullException(&amp;quot;description&amp;quot;);
            if (glyphFileName == null) throw new ArgumentNullException(&amp;quot;glyphFileName&amp;quot;);

            Name = name;
            Description = description;
            GlyphFileName = glyphFileName;
            Shortcut = shortcut;
        }
}

//Аналогичным способом реализованы атрибуты для кисти редактирования и кисти расчета:
public class EditToolAttribute : Attribute
{
...
}

public class CalcToolAttribute : Attribute
{
...
}&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Перед определением заголовка класса атрибута использован специализированный атрибут &lt;i&gt;AttributeUsage&lt;/i&gt;, который позволяет описать возможные применения создаваемого атрибута, в нашем случае атрибуты должны применяться только к классу. Так же устанавливается свойство &lt;i&gt;Inherited&lt;/i&gt; в значение &lt;i&gt;false&lt;/i&gt;, которое запрещает наследование атрибута производными классами (если имеется базовый класс, к которому применен наш атрибут, то при наследовании от этого класса наш атрибут наследоваться не будет).&lt;/p&gt;
&lt;p&gt;Так же стоит знать, что любой новый атрибут должен наследоваться от класса &lt;i&gt;Attribute&lt;/i&gt;, это является обязательным условием.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Ниже пример использования нашего атрибута при создании плагина с кистью рисования «Креста»&lt;/p&gt;
&lt;pre&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;[DrawTool(&amp;quot;Крест&amp;quot;, &amp;quot;Инструмент рисования креста&amp;quot;, &amp;quot;cross_tool.png&amp;quot;, (System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.R))]
class Cross : IDrawObject
{
...
}&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Финальным шагом нам необходимо реализовать менеджер плагинов, который будет загружать сборки с диска, искать в них плагины, инстанциировать их и складывать в в коллекции для последующего использования в редакторе. Класс является статическим, что бы упростить вызов и доступ к плагинам из любого места редактора.&lt;/p&gt;
&lt;p&gt;Основные шаги, которые должен выполнять менеджер:&lt;/p&gt;
&lt;p&gt;— Последовательно загружать сборки (с расширением dll) с диска&lt;br /&gt;
— Для каждой сборки получать все типы, находящиеся в этой сборке&lt;br /&gt;
— Для каждого типа получить список его кастомных атрибутов&lt;br /&gt;
— Проверить, реализует ли тип один из интерфейсов плагинов (&lt;i&gt;IDrawObject, IEditTool, ICalcTool&lt;/i&gt;)&lt;br /&gt;
— Проверить, имеет ли тип один из атрибутов, описывающих кисть&lt;br /&gt;
— Если описанные выше условия выполняются — то необходимо инстанциировать кисть и добавить ее в соответствующий список.&lt;br /&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;В работе всего этого хозяйства будут использоваться классы из пространства имен &lt;i&gt;System.Reflection&lt;/i&gt;, а именно следующие:&lt;/p&gt;
&lt;p&gt;— &lt;a href="http://msdn.microsoft.com/ru-ru/library/System.Reflection.Assembly(v=vs.110).aspx"&gt;Assembly&lt;/a&gt;, представляет саму сборку&lt;br /&gt;
— &lt;a href="http://msdn.microsoft.com/ru-ru/library/System.Activator(v=vs.110).aspx"&gt;Activator&lt;/a&gt;, предоставляет возможность инстанциировать экземпляр класса&lt;br /&gt;
— &lt;a href="http://msdn.microsoft.com/ru-ru/library/System.Reflection.MemberInfo(v=vs.110).aspx"&gt;MemberInfo&lt;/a&gt;, предоставляет сведения об атрибутах компонента и дает доступ к его свойствам&lt;br /&gt;
— Так же используем метод &lt;a href="http://msdn.microsoft.com/query/dev11.query?appId=Dev11IDEF1&amp;l=RU-RU&amp;k=k(System.Type.IsAssignableFrom);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv3.5);k(DevLang-csharp)&amp;rd=true"&gt;IsAssignableFrom&lt;/a&gt; класса &lt;i&gt;Type&lt;/i&gt;, который позволяет определить унаследован ли наш тип от другого типа&lt;br /&gt;
&lt;input type="button" onclick="toggle(code2_realizaciya_mehanizma_plaginov_v_prilozhenii)" value="Нажмите, чтобы отобразить или спрятать код"&gt;&lt;/p&gt;
&lt;pre id="code2_realizaciya_mehanizma_plaginov_v_prilozhenii" style="display: none;"&gt;&lt;pre class="e2-text-code"&gt;&lt;code&gt;static class PluginManager
{
        /// &amp;lt;summary&amp;gt;
        /// Загружать ли плагины при старте
        /// &amp;lt;/summary&amp;gt;
        public static bool IsLoadPlugins = false;

        /// &amp;lt;summary&amp;gt;
        /// Возвращает список объектов рисования
        /// &amp;lt;/summary&amp;gt;
        public static IEnumerable&amp;lt;IDrawObject&amp;gt; DrawObjectInstances { get; private set; }
        /// &amp;lt;summary&amp;gt;
        /// Возвращает список кистей редактирования
        /// &amp;lt;/summary&amp;gt;
        public static IEnumerable&amp;lt;IEditTool&amp;gt; EditToolInstances { get; private set; }
        /// &amp;lt;summary&amp;gt;
        /// Возвращает список расчетных инструментов
        /// &amp;lt;/summary&amp;gt;
        public static IEnumerable&amp;lt;ICalcTool&amp;gt; CalcToolInstances { get; private set; }

        /// &amp;lt;summary&amp;gt;
        /// Флаг об успешной загрузке сборок плагинов
        /// &amp;lt;/summary&amp;gt;
        public static bool IsPluginsLoaded
        {
            get
            {
                if (_pluginCache == null || _pluginCache.Count == 0) return false;
                return true;
            }
        }

        /// &amp;lt;summary&amp;gt;
        /// Загружает все модули в словарь и возвращает его
        /// &amp;lt;/summary&amp;gt;
        public static bool LoadAllModules(string path)
        {
            InitCollections();

            if (!IsLoadPlugins)
            {
                return false;
            }

            if (string.IsNullOrEmpty(path)) return false;

            foreach (string fileName in Directory.GetFiles(path/*, &amp;quot;*.dll&amp;quot;*/))
            {
                Assembly asm = null;
                string asmPath = Path.Combine(path, fileName);
                bool isAsm = false;
                bool isPlugin = false;

                try
                {
                        asm = Assembly.LoadFile(asmPath);

                        foreach (Type type in asm.GetTypes())
                        {
                            foreach (Attribute attribute in type.GetCustomAttributes(typeof (Attribute), true))
                            {
                                //кисти рисования
                                if (
                                    //если тип поддерживает соответствующий атрибут
                                    attribute is DrawToolAttribute
                                    &amp;amp;&amp;amp;
                                    //и отвечает соответствующему интерфейсу
                                    typeof (IDrawObject).IsAssignableFrom(type)
                                    )
                                {
                                    //инстанциируем экземпляр кисти из плагина
                                    DrawObjectInstances.Add(Activator.CreateInstance(type) as IDrawObject);
                                    break;
                                }

                                //кисти редактирования
                                if (
                                    //если тип поддерживает соответствующий атрибут
                                    attribute is EditToolAttribute
                                    &amp;amp;&amp;amp;
                                    //и отвечает соответствующему интерфейсу
                                     typeof (IEditTool).IsAssignableFrom(type)
                                    )
                                {
                                    //инстанциируем экземпляр кисти из плагина
                                    EditToolInstances.Add(Activator.CreateInstance(type) as IDrawObject);
                                    break;
                                }

                                //расчетные кисти
                                if (
                                    //если тип поддерживает соответствующий атрибут
                                    attribute is CalcToolAttribute
                                    &amp;amp;&amp;amp;
                                    //и отвечает соответствующему интерфейсу
                                     typeof (ICalcTool).IsAssignableFrom(type)
                                    )
                                {
                                    //инстанциируем экземпляр кисти из плагина
                                    CalcToolInstances.Add(Activator.CreateInstance(type) as IDrawObject);
                                    break;
                                }
                            }
                        }
                }
                catch(ApplicationException ex)
                {
                    MessageBox.Show(&amp;quot;Ошибка при загрузке плагина из сборки &amp;quot; + fileName, &amp;quot;Ошибка&amp;quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }

            return true;
        }

        /// &amp;lt;summary&amp;gt;
        /// Инициализирует пустые коллекции элементов плагинов (необходимо для случая, когда редактор загружается с выключенной возможностью использования плагинов, просто инициализирует пустые коллекции кистей)
        /// &amp;lt;/summary&amp;gt;
        private static void InitCollections()
        {
            DrawObjectInstances = new List&amp;lt;IDrawObject&amp;gt;();
            EditToolInstances = new List&amp;lt;IEditTool&amp;gt;();
            CalcToolInstances = new List&amp;lt;ICalcTool&amp;gt;();
        }
}&lt;/code&gt;&lt;/pre&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Вот собственно и все, что следовало написать по этой теме. Остается только вызвать метод &lt;i&gt;LoadAllModules()&lt;/i&gt; для загрузки плагинов и забрать свой плагин из соответствующей коллекции.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;В качестве завершения, из статьи мы получили следующую информацию:&lt;/p&gt;
&lt;p&gt;— Узнали, как построить простую (и не совсем простую) инфраструктуру плагинов для своей программы&lt;br /&gt;
— Рассмотрели реализацию простейшего интерфейса для плагина&lt;br /&gt;
— Узнали как реализовать собственный атрибут&lt;br /&gt;
— Разобрались с тем, как загружать сборки с диска, искать в них типы и получить список атрибутов этих типов&lt;br /&gt;
— Узнали как инстанциировать типы при помощи класса &lt;i&gt;Activator&lt;/i&gt;&lt;br /&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;В качестве домашней работы можно модифицировать код загрузки плагинов так, что бы он мог отделить управляемую сборку от неуправляемой, обработать такую ситуацию корректно и не упасть при этом :)&lt;/p&gt;
&lt;/div&gt;</description>
<pubDate>Sun, 04 Jan 2015 23:31:19 +0300</pubDate>
</item>


</channel>
</rss>