Зачем вашему приложению нужна подключаемая
инфраструктура?
Люди, как правило, добавляют поддержку подключаемых модулей в свои приложения
по следующим причинам:
- Чтобы расширить функциональные возможности приложения без необходимости
перекомпиляции или повторного распространения его среди заказчиков.
- Чтобы добавить функциональные возможности без необходимости доступа к
оригинальному исходному коду.
- Бизнес-правила для приложения меняются часто или часто добавляются новые
правила.
В этой статье вы создадите очень простой текстовый редактор, состоящий всего
из одной формы. Все, что он сможет делать — выводить на экран текст в
единственном текстовом окне в центре формы. Как только будет готово это
приложение, вы создадите простой подключаемый модуль и добавите его в
приложение. Этот подключаемый модуль сможет читать текст, находящийся в
настоящее время в текстовом окне, проводить его синтаксический разбор в поисках
действительных адресов электронной почты и возвращать строку, содержащую только
эти адреса. Затем вы поместите этот текст в текстовое окно.
Как видите, в этом учебном примере есть несколько «неизвестных»:
- Как вы найдете подключаемый модуль из приложения?
- Откуда подключаемый модуль знает, какой текст находится в текстовом окне?
- Как вы активируете этот подключаемый модуль?
Ответы на все эти вопросы появятся, когда мы создадим решение.
Шаг 1. Создание простого текстового редактора
Я не буду надоедать вам с деталями. Вы все найдете в исходном коде: это всего
лишь простая форма, показывающая кусок текста. С этого момента я предполагаю,
что вы уже создали простое приложение.
Шаг 2. Создание подключаемого SDK
Теперь, когда у вас есть приложение, вы захотите, чтобы оно могло
обмениваться информацией с внешним подключаемым модулем. Как это сделать?
Решение в том, чтобы сориентировать работу приложения на опубликованный
интерфейс, набор общих членов и методов, которые будут реализовываться всеми
специальными подключаемыми модулями. Я назову этот интерфейс
IPlugin. С этих пор любой разработчик, который захочет создать
подключаемый модуль для вашего приложения, должен будет реализовать этот
интерфейс. Он будет располагаться в общей библиотеке, которую будут использовать
и ваше приложение, и любой специальный подключаемый модуль.
Чтобы описать этот интерфейс, вам нужны просто некоторые данные о вашем
простом подключаемом модуле: его имя и метод, который будет давать модулю
указания осуществлять универсальные действия на основании данных вашего
приложения.
public interface IPlugin
{
string Name{get;}
void PerformAction(IPluginContext context);
}
Код прост, но зачем передавать интерфейс IPluginContext в
PerformAction? Причина, по которой вы посылаете интерфейс, а не
просто строку — обеспечение большей гибкости в отношении того, какой объект вы
сможете посылать. В настоящее время, интерфейс очень прост:
public interface IPluginContext
{
string CurrentDocumentText{get;set;}
}
Теперь все, что вам надо сделать — реализовать этот интерфейс в одном или
более объектов и отправить их в любой подключаемый модуль для получения
результата. В будущем это позволит вам изменять строку не только текстового
окна, но любого объекта.
Шаг 3. Создание вашего специального подключаемого
модуля
Все, что вам сейчас надо сделать:
- Создать отдельный объект библиотеки классов.
- Создать класс, который реализовывает интерфейс IPlugin.
- Откомпилировать этот класс и поместить его в ту же папку, к которой
находится главное приложение.
public class EmailPlugin:IPlugin
{
public EmailPlugin()
{
}
// Единственная точка для входа в ваш подключаемый модуль
// Принимает объект IPluginContext,
// в котором находится текущее
// содержимое редактора.
// Затем он проводит синтаксический разбор текста, обнаруженного в
// редакторе, и оставляет в нем только обнаруженные электронные адреса.
public void PerformAction(IPluginContext context)
{
context.CurrentDocumentText=
ParseEmails(context.CurrentDocumentText);
}
// Имя подключаемого модуля, которое появится
// в меню «Подключаемые модули» («Plugins») редактора
public string Name
{
get
{
return "Email Parsing Plugin";
}
}
// Проводит синтаксический разбор данной строки в поисках любых адресов
// электронной почты, используя класс Regex,
// и возвращает строку, содержащую только адреса электронной почты
private string ParseEmails(string text)
{
const string emailPattern= @"\w+@\w+\.\w+((\.\w+)*)?";
MatchCollection emails =
Regex.Matches(text,emailPattern,
RegexOptions.IgnoreCase);
StringBuilder emailString = new StringBuilder();
foreach(Match email in emails)
{
emailString.Append(email.Value + Environment.NewLine);
}
return emailString.ToString();
}
}
Шаг 4. Заставьте ваше приложение знать о новом
подключаемом модуле
Когда вы скомпилировали ваш подключаемый модуль, как приложение узнает о его
существовании?
Решение простое:
- Создайте файл конфигурации приложения.
- Создайте секцию в файле конфигурации, в которой перечислены все
существующие подключаемые модули.
- Создайте синтаксический анализатор для этой секции файла конфигурации.
Чтобы завершить первый шаг, просто добавьте XML-файл в главное
приложение.
Комментарий: Назовите этот файл
App.Config. В этом случае при каждой сборке вашего приложения
Microsoft® Visual Studio .NET автоматически будет копировать этот файл в
выходную папку и переименовывать его в <yourApp>.Config,
избавляя вас от хлопот.
Теперь разработчик подключаемого модуля должен иметь возможность легко
добавить данные в файл конфигурации, чтобы опубликовать все созданные
подключаемые модули. Здесь показано, как должен выглядеть файл конфигурации:
<configuration>
<configSections>
<section name="plugins"
type="Royo.PluggableApp.PluginSectionHandler, PluggableApp"
/>
</configSections>
<plugins>
<plugin type="Royo.Plugins.Custom.EmailPlugin, CustomPlugin" />
</plugins>
</configuration>
Обратите внимание на тэг configSections. Он сообщает настройкам конфигурации
приложения, что у вас в этом файле конфигурации есть секция подключаемых
модулей, и что у вас есть синтаксический анализатор для этой секции. Он
находится в классе Royo.PluggableApp.PluginSectionHandler,
который располагается в сборке PluggableApp. Ниже я покажу вам
код этого класса.
Далее, у вас есть секция подключаемых модулей файла конфигурации, в которой
перечислены для каждого подключаемого модуля имя класса и имя сборки, в которых
он находится. Вы будете использовать эту информацию при создании экземпляров
подключаемого модуля позже.
Закончив файл конфигурации, вы завершаете один из этапов этого цикла.
Подключаемый модуль готов к использованию, и сообщил о своем существовании во
все необходимые каналы. Все, что вам осталось теперь сделать — научить ваше
приложение считывать эту информацию и создавать в соответствие с нею экземпляры
опубликованных подключаемых модулей.
Шаг 5. Синтаксический анализ файла конфигурации с
использованием IConfigurationSectionHandler
Чтобы провести синтаксический анализ подключаемых модулей, обнаруженных в
файле .config приложения, инфраструктура предлагает очень простой механизм,
позволяющий вам регистрировать определенный класса в качестве обработчика
определенной части вашего файла конфигурации. У вас должен быть обработчик любой
части файла, для которой инфраструктура не проводит автоматического
синтаксического анализа; в противном случае, возникнет
ConfigurationException.
Чтобы обеспечить класс, проводящий синтаксический разбор секции подключаемых
модулей, вам всего лишь надо реализовать интерфейс
System.Configuration.IConfigurationSectionHandler. Сам по себе
этот интерфейс очень прост:
public interface IConfigurationSectionHandler
{
public object Create(object parent, object configContext, System.Xml.XmlNode section);
}
Все, что вам надо сделать — переопределить метод Create в
своем специальном классе и провести синтаксический разбор XML-узла,
предоставленного вам. Этот XML-узел, в данном случае, будет XML-узлом
«Подключаемые модули» («Plugins»). После этого у вас есть вся информация,
необходимая для создания экземпляра подключаемых модулей для вашего
приложения.
Ваш специальный класс должен поставлять стандартный конструктор, поскольку
его экземпляр создается инфраструктурой автоматически во время выполнения, а
затем вызывается его метод Create. Вот код для класса
PluginSectionHandler:
public class PluginSectionHandler:IConfigurationSectionHandler
{
public PluginSectionHandler()
{
}
// Пройдите по всем дочерним узлам
// XMLNode, который был передан вам, и создайте экземпляры
// заданных в значениях атрибутов узлов типов
// мы используем здесь try/Catch, потому что некоторые из узлов
// могут содержать неверные ссылки на тип подключаемого модуля
public object Create(object parent,
object configContext,
System.Xml.XmlNode section)
{
PluginCollection plugins = new PluginCollection();
foreach(XmlNode node in section.ChildNodes)
{
//Здесь располагается код для создания экземпляров
// подключаемых модулей и их инициализации
.
.
.
}
return plugins;
}
}
Как видите, в упомянутом ранее файле конфигурации вы предоставляете данные,
необходимые инфраструктуре для обработки секции подключаемых модулей, используя
тэг configSection перед тэгами самих подключаемых модулей.
<configuration>
<configSections>
<section name="plugins"
type="Royo.PluggableApp.PluginSectionHandler, PluggableApp"
/>
</configSections>
.
.
.
Обратите внимание, как определять класс. Строка состоит из двух частей:
полного имени класса (включая пространство имен), запятой, имени сборки, в
которой размещается этот класс. Это все, что нужно инфраструктуре для создания
экземпляра класса, и не удивительно, что именно эта информация нужна и при
регистрации любого подключаемого модуля для вашего приложения.
Создание экземпляров подключаемых модулей и их
инициализация
Итак, как на самом деле вы будете создавать экземпляр подключаемого модуля,
приведенного в этой строке?
String ClassName = "Royo.Plugins.MyCustomPlugin, MyCustomPlugin"
IPlugin plugin = (IPlugin )Activator.CreateInstance(Type.GetType(ClassName));
Здесь происходит следующее: поскольку ваше приложение не делает прямых ссылок
на сборку специального подключаемого модуля, вы используете класс
System.Activator. Activator — это специальный класс, который
может создавать экземпляры объекта, заданного с любым количеством определенных
параметров. Он даже может создавать экземпляры объектов и возвращать их. Если вы
когда-нибудь писали код в ASP или Microsoft® Visual Basic®, вы должны помнить
функцию CreateObject(), которая использовалась для создания
экземпляров и возвращения объектов на основании CLSID класса.
Activator действует по той же схеме: использует различные
аргументы и возвращает экземпляр System.Object.
В этом обращении к Activator вы передаете в качестве
параметра Type, экземпляр которого хотите создать. Используйте метод
Type.GetType() для возвращения экземпляра Type, который
соответствует Type подключаемого модуля. Обратите внимание, что метод
Type.GetType() в качестве параметра принимает именно ту строку,
которая была помещена в тэг подключаемых модулей, которая описывает имя класса и
сборку, в которой он находится.
Создав экземпляр подключаемого модуля, приведите его к интерфейсу
IPlugin и поместите его в объект вашего подключаемого модуля.
Здесь должен присутствовать блок Try-Catch, поскольку вы не можете быть
уверенными, что описанный там подключаемый модуль существует на самом деле или
действительно поддерживает необходимый вам интерфейс
IPlugin.
Создав экземпляр подключаемого модуля, добавьте его в ArrayList подключаемых
модулей вашего приложения и переходите к следующему XML-узлу.
Вот код приложения:
public object Create(object parent,
object configContext,
System.Xml.XmlNode section)
{
//Происходит от CollectionBase
PluginCollection plugins = new PluginCollection();
foreach(XmlNode node in section.ChildNodes)
{
try
{
//Используйте метод 'CreateInstance' класса Activator
//при попытке создать экземпляр подключаемого модуля,
//передавая в него имя типа, определенного в значении атрибута
object plugObject =
Activator.CreateInstance(Type.GetType(node.Attributes["type"].Value));
//Приведите это к интерфейсу IPlugin и добавьте в коллекцию
IPlugin plugin = (IPlugin)plugObject;
plugins.Add(plugin);
}
catch(Exception e)
{
//Регистрируйте все возникающие исключения,
//но продолжайте перебор в поисках подключаемых модулей
}
}
return plugins;
}
Инициализация подключаемых модулей
Сделав все это, вы можете использовать подключаемые модули. Однако кое-что
еще мы пропустили. Помните, что IPlugin.PerformAction() требует
аргумент типа IPluginContext, в котором находятся все
необходимые для работы подключаемого модуля данные. вы реализуете простой класс,
реализовывающий этот интерфейс, который вы посылаете в метод
PerformAction() всякий раз при вызове подключаемого модуля. Вот
код класса:
public interface IPluginContext
{
string CurrentDocumentText{get;set;}
}
public class EditorContext:IPluginContext
{
private string m_CurrentText= string.Empty;
public EditorContext(string CurrentEditorText)
{
m_CurrentText = CurrentEditorText;
}
public string CurrentDocumentText
{
get{return m_CurrentText;}
set{m_CurrentText = value;}
}
}
Когда этот класс готов, вы можете просто осуществлять операции на текущим
текстом редактора:
private void ExecutePlugin(IPlugin plugin)
{
//создаем объект 'context' для передачи в подключаемый модуль
EditorContext context = new EditorContext(txtText.Text);
//Подключаемый модуль изменяет свойство 'Text' объекта 'context'
plugin.PerformAction(context);
txtText.Text= context.CurrentDocumentText;
}
}
Заключение
Как видите, обеспечить поддержку подключаемых модулей в своем приложении
чрезвычайно просто. Вы только:
- Создаете общую библиотеку интерфейсов.
- Создаете специальные подключаемые модули, реализовывающие специальные
интерфейсы.
- Создаете контекстные аргументы для передачи их в подключаемые модули.
- Создаете секцию в своем файле конфигурации для размещения имен
подключаемых модулей.
- Создаете экземпляры подключаемых модулей, используя
IConfigurationSectionHandler.
- Вызываете ваши подключаемые модули.
- Отправляетесь домой и с пользой проводите время подальше от своего
компьютера.
Никакая часть настоящей статьи не может быть воспроизведена или
передана в какой бы то ни было форме и какими бы то ни было средствами, будь то
электронные или механические, если на то нет письменного разрешения владельцев
авторских прав.
Материал, изложенный в данной статье, многократно
проверен. Но, поскольку вероятность технических ошибок все равно существует,
сообщество не может гарантировать абсолютную точность и правильность приводимых
сведений. В связи с этим сообщество не несет ответственности за возможные
ошибки, связанные с использованием статьи.