Создание Plugin архитектуры с помощью C#

пятница, 16 октября 2009, Jorgen Bumajnikov

Эта статья посвящена описанию создания гибкого приложения на основе плагинов, с помощью .NET C#.

Введение

Все мы имеем базовые навыки разработки приложений. И так, ты создал программу, распространил ее и она делает то что должна (мы надеемся). Такой механизм будет действовать до тех пор, пока мы пользуемся компьютерами. Хотя появился новый, набирающий популярность механизм, позволяющий разработчику в Германии расширять функциональность приложения, написаного в Японии и запускать на компьютере в США не смотря на тот факт, что разработчик из Германии никогда даже не встречался с тем, кто создал оригинальное приложение. Это модель разьемной (plugin) архитектуры.

Вообще, программа поддерживаемая разьемную архитектуру может динамически подгружать неизвестный код и использовать его как часть себя. Тоесть, если ты гуру в написании звукообрабатываемого кода, но ужасно создаеш пользовательский интерфейс, то можно написать обрабатываемый звук плагин для Winamp'а, например, и он сможет им пользоваться, даже если ты не имееш понятия, как этот Winamp работает. Используя расширяемую архитектуру, разработчик предоставляет больше возможностей пользователю.

ОТЛИЧНО, вы убедили меня! Что дальше ?

В этой статье я опишу все шаги создания приложения на базе расширяемой архитектуры используя C#. Мы создадим простейшее, управляющее плагинами, приложение, а вы будете расширять его своими собственными плагинами. Хотя написаное нами приложение, не принесет пользу на практике, но я надеюсь, что сделаное нами поможет хорошо разобратся в том, как загружать чужой код в режиме выполнения и как проэктировать приложение, используемое, не известно как внутренне организованые, обьекты.

Общие сведения

Пример кода написан используя Visual Studio .NET 2005 и .NET версии 2.0. Он не может быть загружен более старой студией, но вы можете посмотреть исходный код и импортировать его. C# Express Edition версии 2.0 доступен на сайте Microsoft's MSDN и должен компилировать и запускать представленый код.

Начало

Планируя приложение, части которого нужно разделить, ваше решение станет более составным чем в случае с одно-проектовым решением. Но ничего страшного, Visual Studio заточена под такой тип роботы.
Первая вещь, которую нужно сделать - создать новое решение (solution). Выберите пункт меню File > New > Project. Вам нужно создать C# Windows Application и дать ему имя, к примеру PluginDemo (как у меня). Это будет управляющее расширениями приложение. Пожже мы добавим больше, содержащих наши плагины, проектов, но на данный момент это все что нам нужно.
Главная форма создается автоматически и называется Form1. Это изначально не правильно и вообще не подходит для нашего потрясающего приложения. Я переименовал свою форму в frmHost. Если вы переименуете файл в Solution Explorer, то Visual Studio 2005 достаточно умная, что бы спросить, не хочете ли вы переименовать заодно и имя класса. Но если вы проделаете подобное в 2003 и попробуете откомпилировать/запустить, то скорее всего узнаете, что она не может найти Form1. Глупая Visual Studio, это потому что ты её преименовал! Продолжим и заменим все ссылки на Form1 на frmHost или, если вы на это стесняетесь решится можете оставить все как есть.
Увеличте форму и добавте список, поле для текстового ввода, кнопку и диалог для выбора каталога. Гляда в присоедененный файл проекта, можно обнаружить на много больше добавленых елементов - не пугайтесь, мы их добавим пожже, пока нам достаточно этого.

Определение ваших интерфейсов

Логично задаватся вопросом: как же использовать плагин, не зная как он был написан ? Ключевым моментом здесь есть то, что плагин должен реализовывать известный интерфейс, тоесть набор свойств и методов для взаимодействия с ним. Другими словами, вы говорите писателю расширений "ваше расширение должно иметь метод называемый PlaySound и он должен принимать строку - путь в файлу с расширением wav - как параметр. Этот метод должен открывать файл, проигрывать его и закрывать." Как он это реализует - зависит только от его фантазии, но вы дали ему спецификацию, которой он должен придерживатся. Язык C# уже имеет представление об интерфейсах. В отличии от С++, C# позволяет наследоватся только от одного класса. Интерфейсы позволяют обойти это ограничение. Для знакомых с ОО терминологией, скажу что интерфейс - это абстрактный клас, в котором описаны полностью виртуальные публичные методы, которые класс-наследник должен раелизовать. Если только-что сказаное мной не содержит для вас никакого смысла - ничего страшного, просто не обращайте внимания.
Можно описать интерфейс внутри управляющего приложения, но нам ведь нужно предоставить его другим разработчикам. Поэтому добавим новый проект с определениями. КЛикните File > Add > New Project и выберите Visual C# class library. Я назвал свой Interfaces и указал путь расположения внутри каталога PluginDemo. давайте переименуем Class1.cs во что-нибуть более прикольное. например в InterfaceDefinitions. Продолжая, удалим автоматически сгенерированый каркас Class1, но оставим определения пространства имен Interfaces.
А сейчас нужно убедится, что проект Interfaces видимый для PluginDemo. В Project Explorer, перейдите к каталогу PluginDemo и вызовите контекстное меню на References. Выберите "Add Reference", а потом вкладку Projects. И наконец - выберите Interfaces из списка и жмите OK. ТО что нам нужно - теперь у нас есть доступ к содержимому файла InterfaceDefinitions из PluginDemo. Теперь в любой момент переходим в начало файла frmHost.cs и добавляем следующую строку:

using Interfaces;

Этим вы говорите - "Все что есть в пространстве имен Interfaces теперь в общем пространстве имен для этого файла". Вау!

Давай уже пистаь код

Мда... Это заняло многовато времени. Если вы схожы со мной, то вам наверно уже хочется закончить дурачистя со студией и начать писать настоящий код. Что ж, ХОРОШО! Переходим к файлу InterfaceDefinitions.cs и создадим новый интерфейс в пространстве имен Interfaces. Вот этот код:

public interface IPlugin
{ string Name { get;}
string Version { get;}
string Author { get;} };

Класс! Как все просто! Вы наверно заметили, что имя интерфейса начинается с символа 'I'. Это просто конвенция - вы можете этого не делать, если не хотите. Я же это сделаю потому, что на много проще набрав просто I получить список всех ваших интерфейсов в подсказке от Intellisense. Круто, не правда ли ? В любом случае, вы наверно задумывались о том, что же все таки будет делать этот плагин. Ответ прост - ничего. Он будт протсо хранить информацию о себе. Давайте же создадим настоящий плагин:

public interface IMathPlugin: IPlugin
{ double Add(double a, double b);
double Subtract(double a, double b);
double Multiply(double a, double b);
double Divide(double a, double b); };

Если для вас чужд синтаксис IMathPlugin:IPlugin, то знайте - это называется наследованием. Другими словами интерфейс IMathPlugin наследует все методы из своего предка. Наследуясь от интерфейса, вы по умолчанию обязуетесь реализовать все описаные в нем методы. Если же интерфейс наследуется от другого интерфейса, то методы из интерфейса-предка фактически добавляются интерфейсу-наследнику. Это хороший вариант не копипастить одно и то же! Получив возможность такого удобного наследования, давайте определим другой интерфейс:

public interface IStoragePlugin:IPlugin
{ void Add(object toAdd);
void Remove(object toRemove);
void EnumStart();
bool EnumNext();
object GetCurr(); };

Это кусочек очень простого плагина для хранения обьектов. Можно добавлять элементы, удалять определенный елемент и перебирать все элементы. не очень впечатляюще, но это именно то что нам нужно: продемонстрировать что оба наши расширения Math и Storage так же содержат членов из базового интерфейса, IPlugin.

Определение самих себя

Теперь имея определения некоторых интерфейсов для расширений, нам нужно знать где же они располагаются в файле с плагином. Файл с расширением - просто .dll файл, который может содержать в себе множество классов. Вам нужно будет убедится, что используете тот, что вам нужен и какой именно. Потому как это может быть как Math плагин, так и Storage. Кто знает ?
Для начала, опишем пречисление. Перечисление или enum - по сути просто перечисление значений. Это проще показать:

public enum PluginType
{ Math,
Storage,
Unknown };

Просто вставте такое определение в пространство имен Interfaces (аналогично определениям ваших интерфейсов). Переменная типа PluginType может принимать только одно из этих трех значений. ВОт мы написали еще несколько строк кода, но этого все не достаточно, что бы идентефицировать плагины в dll файле. К счастью, .NET весь построен на мета данных. И это нам предоставляет возможность определить свой мета атрибут, который будет доступен во время выполнения. Если вы когда нибуть видели такой код в квадратных скобках, как :

[STAThread]

Значит вы видели атрибут. Это просто тэг, которым метится класс для ссылания. И так, чего же я жду ? Давайте быстренько определим наш собственный атрибут:

[AttributeUsage(AttributeTargets.Class)]
public sealed class PluginAttribute:Attribute
{ private PluginType _Type;
public PluginAttribute(PluginType T) { _Type = T; }
public PluginType Type { get { return _Type; } } };

AttributeTargets.Class - атрибут нашего атрибута (во как загнул) говорящий .NET, что наш атрибут годен использоватся только с определениями класов - не методов или полей. Он содержит единственный член, несущий информацию о типе плагинов, к которому отнисится клас. Теперь, загрузив dll файл, все что нам нужно сделать - искать этот атрибут.
Канешно возможно создать расширяемую архитектуру без использования атрибутов. Но мое мнение таково, что описаный подход, все таки с использованием атрибутов на много понятнее и проще в обращении. Заметте, что PluginAttribute - очень общее название, тоесть, если вы будете писать приложение называемое Widget, то, во избежания конфликта имен, разумнее будет назвать новый атрибут - WidgetPluginAttribute.

Мы закончили ?

Нет! Осталась еще одна вещь. Добавте пустой .cs файл в проект PluginDemo, назовите его Plugin.cs и добавте в него следующий код:

namespace PluginDemo
{ public class Plugin:IPlugin
{ public static Type GetTypeFromEnum(PluginType T)
{ switch (T) {
case PluginType.Math:
        return typeof(IMathPlugin);
case PluginType.Storage:
        return typeof(IStoragePlugin);
default:
        return typeof(IPlugin);
} } public static string GetTypenameFromEnum(PluginType T)
{ switch (T) {
case PluginType.Math:
        return "Math";
case PluginType.Storage:
        return "Storage";
default:
        return "Unknown";
} } public static char GetTypecharFromEnum(PluginType T)
{ switch (T) {
case PluginType.Math:
        return 'M';
case PluginType.Storage:
        return 'S';
default:
        return '?';
} } private IPlugin internalPlugin;
private string myPath;
private PluginType myType = PluginType.Unknown;

public string Path { get { return myPath; } }
public string Filename { get { return new FileInfo(myPath).Name; } } public PluginType Type
{ get
{
        if (internalPlugin == null) {
                return PluginType.Unknown;
        }
        return myType;
} } public string Name
{ get
{
        if (internalPlugin == null) {
                return "Not a recognized plugin";
        }
        return internalPlugin.Name;
} } public string Version
{ get
{
        if (internalPlugin == null) {
                return "";
        }
        return internalPlugin.Version;
} } public string Author
{ get
{
        if (internalPlugin == null) {
                return "";
        }
        return internalPlugin.Author;
} } public IPlugin PluginInterface { get { return internalPlugin; } } public override string ToString()
{ return string.Format("{0}: {1}", GetTypecharFromEnum(myType), myPath); } } }

Ууух! Многовато! Но к щастью, я уже все для вас набрал. Немного поясню и прейдем к третей части, где мы заставим этот код работать по настоящему.Во-первых, методы Get*FromEnum созданы впринцыпе лиш для формальности. Они принимают то, что над чем вы так долго трудились (делают определенные действия, в зависимости от переданого значения перечисления) и ложат в одно место.ряд других свойств - ничто иное, как просто аксессоры для полей. Единственная интересная вещь - поле internalPlugin типа IPlugin и предоставляющее доступ ко всем матодам интерфейса IPlugin. Это значит, что вы можете использовать такие мелочи как свойства Name, Version и Author, которые берут свои значения из плагина, инкапсулирующего этот клас.

Управляющее приложение

Отлично, мы уже сами создали маленькую структуру. Настало время создать пользовательский интерфейс и приступить к тестированию. Вернемся к файлу frmHost и разместим компоненты на форме более удобно эстетично. И теперь, зная что мы будем делать, давайте добавим еще несколько контролов: выпадающий список с семью метками. Эти метки будут содержать информацию о плагинах. Назовите три из них, например, Name, Version и Author. А остальные, например, "No Plugin Selected" (можно, канешно, сделать ее пустой, но есть вероятность потерять ее на форме). Еще я имею обыкновение именовать свои метки прикольными именами, а не просто labelN, что помогает мне когда я пишу код и мне не нужно уточнят с какой меткой я хочу работать.Для начала нам нужно выбрать каталог, для поиска плагинов. Добавте пространство имен System.IO в общее пространство имен (напишите "using System.IO;" в самом верху файла) и добавте следующий код в вашу форму:

private void btnChooseDir_Click(object sender, EventArgs e)
{ if (fbdPluginFolder.ShowDialog() != DialogResult.Cancel) {
        SetPluginPath(fbdPluginFolder.SelectedPath);
} }

private void SetPluginPath(string path)
{ if(Directory.Exists(path)){
        fbdPluginFolder.SelectedPath = txtPluginPath.Text = path;
        GetPlugins(path);
} }

private void GetPlugins(string Path)
{ if (!Directory.Exists(Path)) {
        return;
}

Plugin curr = lstPlugins.SelectedItem as Plugin;
lstPlugins.BeginUpdate();
lstPlugins.Items.Clear();

foreach (string f in Directory.GetFiles(Path)) {
        FileInfo fi = new FileInfo(f);

if (fi.Extension.Equals(".dll")) {
        lstPlugins.Items.Add(new Plugin(f));
}
}
lstPlugins.EndUpdate();

//Restore the previously selected plugin
if (curr != null) {         Plugin test;
        for (int i = 0; i < lstPlugins.Items.Count; ++i) { test = lstPlugins.Items[i] as Plugin;
if (test.Path == curr.Path) {
        lstPlugins.SelectedIndex = i;
} } } }

Хммм, еще один огромный кусок кода! Не спешите пока жать F5. Еслы просто скопировали мой код, то этого мало - нужно добавить btnChooseDir_Click к списку обработчиков события btnChooseDir вручную. Это можно сделать кликнув на кнопке в дизайнере и перейдя на вкладку Events в окне Properties (кликние яркую молнию почти в самом верху панели) и выберите событие Click. Нажмите клавишу "вниз" и выберите btnChooseDir_Click из списка. К слову, вы можете поменят ь имена обьектов, если вам не по вкусу мои правила именования.
Если честно, это все очень просто. Просто формируем список всех фалов, что находятся в выбранов каталоге. Имейте ввиду, что вызов lstPlugins.Items.Clear() ограничивает нас в сканировании лиш одного каталога одновременно. Для обхода этого ограничения - просто удалите эту строчку.

Загрузка расширений

Если вы делали все как я говорил (а если нет, то что вы досих пор тут делате ?), то должны отметить, что я сделал много всего, но самая важная часть: реальная загрузка плагинов. Что ж, не настало ли время это изменить ?
Добавте в ваш клас Plugin (не интерфейс IPlugin !) пространство имен System.Reflection (using System.Reflection) и следующий код:

public bool SetPlugin(string PluginFile)
{ Assembly asm;
PluginType pt = PluginType.Unknown;
Type PluginClass = null;

if (!File.Exists(PluginFile)) {
        return false;
}
asm = Assembly.LoadFile(PluginFile);

if (asm != null) { myPath = PluginFile;
foreach (Type type in asm.GetTypes()) { if (type.IsAbstract) continue;
object[] attrs = type.GetCustomAttributes(typeof(PluginAttribute), true);
if (attrs.Length > 0) { foreach (PluginAttribute pa in attrs) {
        pt = pa.Type;
}
PluginClass = type;
//To support multiple plugins in a single assembly, modify this } }
if (pt == PluginType.Unknown) {
        return false;
}

//Get the plugin
internalPlugin = Activator.CreateInstance(PluginClass) as IPlugin;
myType = pt;
return true; }
return false; }

Вдобавок нам нужно добавить вызов SetPlugin в конструктор (держу пари, что вы были в недоумении - где же инициализируется myPath ?):

public Plugin(string Path)
{ SetPlugin(Path); }

БУМ, это магия, люди! Это то что превращает наш проект из чего-то написаного средним школьником(цей) в то, что требует подробного руководства. Пространство имен System.Reflection позволяет вам увидеть наши мета данные (с этого момента - название). С помощью этого же механизма возможно получить доступ и к другим мета данным. По сути, мы перебираем все класы в .dll, смотрим их атрибуты и загружаем, имеющий установлений PluginAttribute. Вот для чего мы запоминаем тип плагина в PluginAttribute. Поэтому все на много проще, если мы захотим поддерживать множество расширений в одном файле (скажем, математический плагин и плагин для рисования в одной сборке), то нам будет нужно сделать некоторые довольно обширные изменения.

Написание плагина

Реальная проблема с расширяемой архитектурой - ее все еще нужно продвигать. Странно конечно, люди не начинають искать вас лично, что бы написать плагин к вашему приложению, если конечно оно не на столько крутое, что бы заставить их решится на это. Потраченые усилия оправдают себя только тогда, когда все заработает так, как надо... по сути, вам нужно написать свое расширение. Иногда ваши расширения - просто базовая реализация и этого бывает достаточно, пока кто-нибуть не перепишет их лучшим образом. А иногда в вашем расширении возможно будет реализовано все что только можно к ниму прикрутить и кто-нибуть будет использовать плагины для расширения существующего функционала (как в случае самых популярных, базированых на плагинах проигрывателях). Не обращая внимания на все это, вам придется сделать некоторую грязную работу самым.
И так, чего же мы ждем ? Давайте добавим еще один проект в наше решение. Создайте новую библиотеку класов и нареките ее CustomMathPlugin. Добавьте ссылку на проект Interfaces (как вы делали это для управляющег оприложения). И вот мы имеем самое важное - ссылку на определенные нами ранее интерфейсы. И на конец - сделайте импорт пространства имен Interfaces (using Interfaces;) для возможности их использования.

public class MyMathPlugin:IMathPlugin

Так же добавим наш атрибут Plugin, после чего выглядеть это будет так:

[Plugin(PluginType.Math)]
public class MyMathPlugin:IMathPlugin

Вы наверно удивились, почему мы написали Plugin а не PluginAttribute. Что ж, C# достаточно умный, что бы догадатся, что Attribute часть реализована и вам не нужно говорить этого явно. Довольно хитро, не правда ли ? Если вы очень придирчивы, то можете написать [PluginAttribute(PluginType.Math)] и это будет значить то же самое. Но зачем тратить две секунды своей жизни делая то, что не обязательно ?
Еслы вы используете Visual Studio 2005, то просто наведите курсор на надпись IMathPlugin и выберите "Implement this interface" и среда сама сзгенерирует вам заглушки для всех методов, определенных в интерфейсе. Если вы еще не перешли на хваленую Visual Studio 2005 и используете более раннюю ее версию, то придется написать определения самому. В любом случае ваш клас примерно должет выглядеть следующим образом:

[Plugin(PluginType.Math)]
public class MyMathPlugin:IMathPlugin
{ public double Add(double a, double b) {
        return a + b; }

public double Subtract(double a, double b) {
        return a - b; }

public double Multiply(double a, double b) {
        return a * b; }

public double Divide(double a, double b) {
        return a / b; }

public string Name {
        get { return "Basic Math Plugin"; }
}

public string Version {
        get { return "1.0.0"; }
}

public string Author {
        get { return "Nathan Baker"; }
} };

Уух! Понимаю, что обьем код впечатляет, но если чесно - в нем больше пробелов )). ПОздравляю, вы только что завершили большую часть! Осталось лиш создание тупых плагинов. Не стесняйтес - измените имя автора на свое или чье угодно, я не буду обижатся. Так же поиграйтесь с другими строками. Заметте, что в настоящем, реальном приложении, создание плагинов будет на много сложнее, чем написание потзовательского интерфейса. Вы наверно обрадовались, что мы пишем лиш пример ?

Похвалите себя

В подтверждение, что вы сделали все праивльно (с моин безупречным руководством или нет ?) должна появится возможность выбрать ваш математический плагин из выпадающего списка. Вы будете удивлены не увидев ничего! Значит где-то проблема, и вы получите дружное сообщение "unhandled exception". Я думаю что должен посвятить вас в причину этого. Не переживайте - это просто. По хорошему - вы должни сами это решить. Но я все еще здесь и помогу тебе:

private void lstPlugins_SelectedIndexChanged(object sender, EventArgs e)
{
        SetPluginInfo();
}

private void SetPluginInfo()
{
        Plugin P = lstPlugins.SelectedItem as Plugin;
        lblAuthorText.Text = P.Author;
        lblNameText.Text = P.Name;
        lblVersionText.Text = P.Version;
        lblPluginFilename.Text = P.Filename;
}

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

M: E:\MyPath\MyPlugin.dll

Откуда же берется M: ? Попробую обьяснить это попроще. Те кто знаком с .NET framework или Java скорее всего уже кивают и улыбаются. И должны были заметить что метод ToString() в класе Plugin переопределен. Хранилище, типа выпадающего списка (аналогично и другие компоненты) вызывают метод ToString() у всех обьектов помещенных в него. Умно, правда ?

На завершение

Ну что ж, теперь у вас есть свой собственный пример - как создать приложение на базе расширяемой архитектуры. Я не думаю, что какая то из используемых мной технологий реализована именно в .NET версии 2.0, тоесть, если вы используете 1.1 или, не дай Бог - 1.0, то у вас всеравно описаный код должен работать прекрасно. Для тех же, кто использует 2005 студию (или Express Edition) - не бойтесь, просто скопируйте куски приведенного в статье кода. Он был описан немного разукрашено, но довольно интерактивно. Так что вы можете проверить плагин Math в действии. Так же имейте ввиду, что можете использовать любой .NET язык, для написания новых расширений. Даже если вы относитесь к VB.NET типу разработчиков, не бойтесь и примите удар на себя! Я надеюсь вы получили удовольствие читая написаное мной и не стесняйтесь задавать вопросы, коментировать, помогать деньгами и т.д. nathan.interlude@gmail.com.

Код в жизнь!

З.Ы.
Оригинал Building a Plugin Architecture with C#


Ищите нас в интернетах!

Комментарии

Свежие вакансии