Создание подключаемой инфраструктуры

ОГЛАВЛЕНИЕ

Люди, как правило, добавляют поддержку подключаемых модулей в свои приложения по следующим причинам:
- Чтобы расширить функциональные возможности приложения без необходимости перекомпиляции или повторного распространения его среди заказчиков.
- Чтобы добавить функциональные возможности без необходимости доступа к оригинальному исходному коду.
- Бизнес-правила для приложения меняются часто или часто добавляются новые правила.

В этой статье вы создадите очень простой текстовый редактор, состоящий всего из одной формы. Все, что он сможет делать — выводить на экран текст в единственном текстовом окне в центре формы. Как только будет готово это приложение, вы создадите простой подключаемый модуль и добавите его в приложение. Этот подключаемый модуль сможет читать текст, находящийся в настоящее время в текстовом окне, проводить его синтаксический разбор в поисках действительных адресов электронной почты и возвращать строку, содержащую только эти адреса. Затем вы поместите этот текст в текстовое окно.
Как видите, в этом учебном примере есть несколько «неизвестных»:
- Как вы найдете подключаемый модуль из приложения?
- Откуда подключаемый модуль знает, какой текст находится в текстовом окне?
- Как вы активируете этот подключаемый модуль?

Ответы на все эти вопросы появятся, когда мы создадим решение.


 

Шаг 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, 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.
- Вызываете ваши подключаемые модули.
- Отправляетесь домой и с пользой проводите время подальше от своего компьютера.