Гайдар Магдануров
Создание расширяемых приложений
Использование Plug-In'ов
Расширяемое приложение
практически всегда имеет очевидные преимущества перед нерасширяемым. Прежде
всего - возможность добавить функциональность без перекомпиляции приложения, а
значит и без распространения полного дистрибутива всего программного пакета, а
для крупных продуктов создания дистрибутива и распространение между клиентами
весьма накладно, не только с точки зрения потерянного разработчиками времени, но
и потраченных средств на запись дистрибутивов на носители, сетевой трафик.
Но это не основное преимущество, которые дает возможность
расширения приложений - возможность создать "надстройку" привлекает
сторонних разработчиков к интеграции своих наработок в ваш продукт, а также
привлекает пользователей включится в работу по улучшению проектов. При этом,
достаточно часто, сторонние разработчики находят ошибки не выявленные
собственной Quality Assurances.
Также это дополнительный плюс с точки зрения
маркетинга. Современные крупные продукты просто обязаны быть расширяемыми, чтобы
не смотреться "бедными родственниками" на фоне более "продвинутых" конкурентов.
Поддержка расширения
приложения может быть весьма и весьма разнообразной, по разному воплощенной в
коде и использовать разные технологии. Это уже зависит от конкретной архитектуры
проекта, но, так или иначе существуют основные методы: поддержка
plug-in'ов (или подключаемых модулей)1,
двоичных компонент, автоматически включаемых в приложение, либо макросов,
фрагментов кода на макроязыках, автоматизирующих какие-либо из пользовательских
процессов. В этой статье будет рассказано о возможности поддержки
plug-in'ов.
При проектировании проекта
необходимо заранее предусмотреть все доступные для расширения элементы. Учтите,
реализацию проекта не стоит начинать пока не будет окончательно ясно как именно
можно будет расширять проект и какие возможности будут предоставляться
пользователю. Когда проект уже будет написан, изменить что-либо будет гораздо
сложнее, чем по ходу создания приложения, поэтому отнеситесь к этому процессу
особенно серьезно.
В данной статье будет рассмотрено несколько простых проектов,
но при этом постараюсь рассказать как можно более полно о предоставляемых
возможностях. Сразу должен предупредить, статья более теоретическая и, вполне
вероятно, будет не совсем понятна начинающим программистам, в то же время я
попытался сделать статью как можно более простой и понятной для всех, может быть
с некоторой потерей последовательности изложения. Если возникнет недопонимание или какие-то проблемы - я постараюсь
ответить на ваши вопросы. Мой e-mail неизменен:
gaidar@vbstreets.ru.
При запуске программа
проверяет список установленных модулей расширения, это может быть просто
содержимое определенной директории, список модулей в текстовом или двоичном
файле или набор записей в реестре. Определившись со списком модулей, программа
последовательно загружает их и вызывает некоторые стандартные методы,
присутствующие в каждом модуле, которые описывают функции, выполняемые каждым
конкретным модулем.
Допустим, приложение - текстовый редактор, позволяющий
редактировать текст и встроенную в текст графику. Таким образом могут быть два
типа расширений - графические и текстовые (для упрощения примера, мы не будем
рассматривать единый модуль поддерживающий работу и с текстом и с графикой, хотя
это достаточно легкая задача). При вызове стандартного метода, основная
программа получает некоторое значение, соответствующее типу модуля и, в
соответствии с этим значением, записывает модуль в одну или другую внутреннюю
таблицу модулей по принципу "мухи отдельно, котлеты отдельно". Таким списком,
например, может служить коллекция, либо просто массив элементов, содержащий
ссылки на объекты соответствующие модулям (об объектах см.
далее).
Если во время работы пользователя возникает событие в ответ
на которое должен быть предоставлен список модулей и функций, то приложение
просматривает свои внутренние таблицы и выдает соответствующий список
пользователю.
Теперь я расскажу об одном
из способов реализации, заготовка для этого примера прилагается к статье, но она
не содержит непосредственно рассматриваемого примера, реализацию которого я
оставляю читателю в качестве тренировочного задания (иначе же, читатель совсем
ленивым станет, а надо "учится, учится и учится"), используя код из статьи это
сделать совсем просто.
Наше приложение не будет выполнять никаких полезных действий
("His a real nowhere man, sitting in his nowhere land..."),
но проиллюстрирует собой технологию.
Программа должна при запуске проверять содержимое некоторой
директории, например ./mod/ и получать список
файлов с расширением dll, находящихся в ней. Это и
будет базовый список модулей, которые уже нужно рассортировать по категориям.
Но, прежде, надо определиться с тем, как, собственно, приложение будет общаться
с модулем расширения. Для этого распишем методы, которые должен иметь каждый
модуль, заметим, что для простоты модули должны быть объединены в единый
Namespace и каждый содержать класс с определенным именам,
например, Main.
Модуль
Root Namespace: PluginModule
Class: Main
Private WithEvents eObj As HostObject.HostObject - объект,
отвечающий за связь с главным приложением
Public Function Name(ByVal pObj As HostObject.HostObject) As String - функция,
сообщающая имя модуля
Public Function pType(ByVal pObj As HostObject.HostObject)
As Short - функция, сообщающая основному приложению,
какого типа этот модуль
Public Function Execute(ByVal pObj As HostObject.HostObject) -
Основной метод, запускаемый при вызове модуля.
Как вы могли заметить, в
классе присутствует некоторый объект eObj,
непонятного пока типа. Так вот, этот объект отвечает за функции предоставляемые
главным приложением (Host) модулям. Класс этого
объекта помещен в отдельную библиотеку, ссылка на которую есть и в основном
приложении и в каждом модуле (модулям всех типов мы передаем один и тот же
объект). Пример такого класса:
Imports System.Windows.Forms ' Импортируем пространство имен Windows.Forms чтобы свободно
' поддерживать ссылки на элементы управления формы
Public Class HostObject
' Ссылки непосредственно на элементы формы, которые будут полностью
' доступны для модуля
Public buttonElement As Button
Public textElement As TextBox
' "Скрытые" от модулей расширения объекты основного приложения
' Доступ к этим обхектам осуществляется только через этого методы класса
Private WithEvents formElement As Form
' Внутренние объекты, к свойствам и методам которых модули не имеют
' формально никакого доступа (на самом деле доступ все-таки есть, но
' только на уровне методов).
Private elementGroup As GroupBox
' События генерируемые классом
Public Event SomeEvent()
Public Event AnotherEvent()
' Функции предоставляемые модулям расширения
Public Property SomeProperty() As SomeType
Public Sub SomeSub()
Public Function SomeProperty() As SomeType
Public Sub New(ByVal elementOne As SomeType, ...)
' Сохранение ссылок на элементы "внутри" класса
End Sub
End Class
Таким образом класс
HostObject является единственным звеном через которое модуль
будет осуществлять обратную связь с основным приложением. При этом, через этот
объект могут быть доступны непосредственно объекты
приложения элементы форм (пользовательские элементы управления), какие-либо
"собственные" объекты приложения, или же эти объекты могут быть доступны
опосредованно (через методы класса-оболочки HostObject).
При этом HostObject содержит собственные
события, каким либо образом соотносящиеся с событиями основного приложения, что
позволяет модулям реагировать на любое изменение состояния. (Ознакомьтесь с
прилагаемым к статье проектом HostObject и
непосредственно с главным приложением PluginsHost и
посмотрите, как это реализовано там конкретно).
Теперь перейдем к самому
интересному. Как приложение будет распознавать модули и выполнять их собственные
методы. Для этого мы воспользуемся объектами пространства имен
System.Reflection, специально включенного для такого рода
задач в .NET Framework2.
Каждый модуль расширения будет представлен в
главном приложении своим объектом. Пример такого объекта, содержащий зачатки
универсальности, приведен ниже. В комментариях приведено основное описание кода.
Imports System.Reflection ' Импортируем необходимые нам пространства имен
Imports System.IO
Public Class Plugin
' Подключаемый модуль представлен сборкой
Private mAssembly As [Assembly]
Public Sub New(ByVal fileName As String)
' создаем новый объект в соответствии с переданным
' именем файла модуля расширения
' сюда неплохо бы добавить проверку ошибок, файл может оказаться и не
' сборкой, а просто случайно затесавшейся библиотекой
mAssembly = [Assembly].LoadFrom(fileName)
End Sub
' Основная функция, позволяющая выполнять методы модулей расширения и получать
' возвращаемые значения, так как значения могут быть различных типов, поэтому
' в качестве типа возвращаемого занчения использован Object
Public Function Execute(ByVal pObj As HostObject.HostObject, _
Optional ByVal functionName As String) As Object
' необходимые переменные
Dim mType As Type, mFlags As BindingFlags, mArg As _
New ArrayList, mObj As Object
' Определяем типа класса, интерфейса, значения или массива
' В данном случае класса Main из пространства имен PluginModule
mType = mAssembly.GetType("PluginModule.Main")
' Некоторый набор флагов, используемый далее
mFlags = BindingFlags.DeclaredOnly Or BindingFlags.Public
_ Or BindingFlags.NonPublic Or BindingFlags.Instance
' Набор параметров, передаваемый функции, в нашем случае
' всего один параметр - экземпляр объекта HostObject
mArg.Add(pObj)
' создаем объект
mObj = mType.InvokeMember(Nothing, mFlags Or _
BindingFlags.CreateInstance, Nothing, Nothing, Nothing)
' выполняем метод, передавая параметры, и получаем
' возвращаемое значение
Return mType.InvokeMember(functionName, mFlags Or _
BindingFlags.InvokeMethod, Nothing, mObj, mArg.ToArray)
End Function
End Class
Данный класс является
весьма грубым примером. В реальном приложении в котором производительность
является весьма критичным фактором, создавать объекты каждый раз в методе
Execute является недопустимой растратой ресурсов и процессорного
времени. Оптимальным путем является создание объекта только один раз при
создании экземпляра Plugin, но эту реализацию я снова
оставляю читателю.
Таким образом необходимо только получить имя файла-сборки,
которая относится к модулю расширения, для создания объекта и использования его
методов. Например, для приложения различающего два типа модулей, это может быть
реализовано так (при этом используются коллекции, базовый пример использования
коллекций находится в архиве исходного кода к статье с именем Collections
Tutorial):
Private modGraph As Collection
Private modText As Collection
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
' Создаем базовый объект и передаем ему параметры - объекты элементов
' управления и внутренние объекты приложения
pObj = New HostObject.HostObject(...)
' Создаем коллекции для разделения модулей по группам
modGraph = New Collection
modText = New Collection
End Sub
Private Sub frmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles MyBase.Load
' Получаем список модулей в директории ./mod/ и добавляем его в списки
Dim mDirInfo As DirectoryInfo, mFileInfo As FileInfo, mListItem As _
ListViewItem, plgModule As Plugin
mDirInfo = New DirectoryInfo(Application.StartupPath & "\mod\")
For Each mFileInfo In mDirInfo.GetFiles("*.dll")
plgModule = New Plugin(mFileInfo.FullName) ' создаем объект для
' каждого модуля
Select Case plgModule.Execute(pObj, "pType")
Case 1
modGraph.Add(plgModule, mFileInfo.Name)
Case 2
modText.Add(plgModule, mFileInfo.Name)
End Select
Next
End Sub
Таким образом после
загрузки основной формы, мы имеем две коллекции содержащие объекты отвечающие
модулям расширения с ключами соответствующими имени файла. Таким образом в
каждом случае, когда необходимо выполнять некоторые действия с графикой -
программа проходит по коллекции modGraph, а при
работе с текстом modText, с помощью конструкции
For ... Each.
Если вы надеетесь получить готовый код в
приложенном к статье архиве, то вы, наверное, несколько огорчитесь, не найдя его
там. Как я уже говорил, к статье приложен код с основным примером, а реализацию
более "продвинутого" приложения я оставляю вам3.
Теперь перейдем к изучению
примера, приложенного к статье4.
Откройте решение (solution)
PlugionsHost.sln из директории PluginsHost.
Как вы можете видеть, никаких проверок типов модулей (как,
собственно и самих типов) нет. В данном примере - модули из директории
./mod/ формируют список при перемещении по которому
заполняются поля Name и Description.
Для вызова метода Execute
необходимо нажать одноименную кнопку на форме. Пространством для
работы модулей являются контролы в рамке Test Area.
При перемещении по списку вызываются соответствующие методы для
получения информации:
Private Sub lstPlugins_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles lstPlugins.SelectedIndexChanged
Dim mSelItem As ListViewItem = lstPlugins.SelectedItem
Dim mObj As Plugin
mObj = CType(mSelItem.Tag, Plugin)
' Приведения типов возвращаемых резулитатов не производится, т.к.
' в примере использованы только типы String, как в модулях, так и
' в основной программе
txtName.Text = mObj.Execute(pObj, "Name")
txtDescription.Text = mObj.Execute(pObj, "Description")
End Sub
При щелчке по кнопке
Execute:
Private Sub btnCallMethods_Click(ByVal sender As System.Object, ByVal e As _
System.EventArgs) Handles btnCallMethods.Click
Dim mSelItem As ListViewItem = lstPlugins.SelectedItem
Dim mObj As Plugin
mObj = CType(mSelItem.Tag, Plugin)
' Возвращаемое значение сохраняется в текстовом поле txtText
txtText.Text = mObj.Execute(pObj, "Execute")
End Sub
Объект HostObject
содержит два события TextClick и TextChanged,
соответствующих изменению текста в тестовом текстовом поле и
щелчку мышью по текстовому полю из Test Area. Для
обработки этих событий в коде модуля ActivePlugin
предназначены соответствующие события:
Private WithEvents eObj As HostObject.HostObject
Public Event Private Sub eObj_TextChanged() Handles eObj.TextChanged
MsgBox("Text was changed!")
End Sub
Private Sub eObj_TextClick() Handles eObj.TextClick
If eObj.progressBar.Value <> 100 Then
eObj.progressBar.Value = eObj.progressBar.Value + 1
Else
eObj.progressBar.Value = 0
End If
End Sub
Обратите внимание, модуль
как бы "слушает" события текстового поля. И если будет добавлен еще один модуль,
то событие будет последовательно обработано сначала одним, потом другим модулем.
Также, попробуйте несколько раз вызвать метод Execute
модуля ActivePlugin, вы увидите,
что при этом события щелчка по текстовому полю и изменения текста будут
обрабатываться столько же раз, сколько вы вызовите этот метод. Попробуйте
разобраться почему, это поможет в дальнейшем избежать очень неприятных ошибок
при создании реального приложения.
Я полагаю, что код достаточно прозрачен для читателя. Поэтому
рекомендую пробежаться по нему и немного "поиграть" с приложением. Попытаться
написать свой модуль расширения на основе имеющихся и посмотреть на возникающие
"подводные камни".
В этой статье я попробовал как можно более
проще и нагляднее изложить методику создания приложения поддерживающего
расширение с помощью plug-in'ов. Надеюсь, что эта статья
дает те основы, которые необходимы для самостоятельных исследований этой
техники.
Основной целью статьи я ставил попытку подтолкнуть читателя к
собственной работе и размышлениям над проблемой, я хочу верить, что эта цель
достигнута. Как всегда - я готов помочь вам разобраться. Пишите!
1 Здесь и далее
использованы два равнозначных термина - подключаемые модули и plugin'ы,
для сокращения я буду использовать слово модуль.
2 Это уж очень
лихо сказано. Скажем так, для выполнения подобных и множества других задач. Если
эта тема вызовет достаточно живой интерес, то я расскажу подробнее о данном
пространстве имен в следующих статьях.
3 Не думайте, что
мне было сложно включить в архив реальное приложение или хотя бы реализовать все
описываемые в статье функции в примере. Нет. Но я очень надеюсь, что читатель не
будет слепо использовать мой код, а создаст свое приложение использовав свои
собственные знания и понимание предмета. Это поможет и лучше разобраться в
технологии, но и улучшить код, избежав, возможно, допущенных мною ошибок,
приводящих к потере производительности (а я это дело люблю ;)).
4 Извиняюсь за
комментарии на английском, но, если честно, не люблю в проектах писать
комментарии на английском, в дальнейшем сложнее делиться кодом с "забугорными"
коллегами.