Дата публикации статьи: 25.04.2004 10:52

Гайдар Магдануров
Создание расширяемых приложений
Использование Plug-In'ов

  • Введение

  • Механизм работы

  • Реализация модулей и связующих объектов
  • Объекты модулей в основном приложении

  • Некоторые примеры использования

  • Заключение

  • Исходный код к статье

  •     Расширяемое приложение практически всегда имеет очевидные преимущества перед нерасширяемым. Прежде всего - возможность добавить функциональность без перекомпиляции приложения, а значит и без распространения полного дистрибутива всего программного пакета, а для крупных продуктов создания дистрибутива и распространение между клиентами весьма накладно, не только с точки зрения потерянного разработчиками времени, но и потраченных средств на запись дистрибутивов на носители, сетевой трафик.
        Но это не основное преимущество, которые дает возможность расширения приложений -  возможность создать "надстройку" привлекает сторонних разработчиков к интеграции своих наработок в ваш продукт, а также привлекает пользователей включится в работу по улучшению проектов. При этом, достаточно часто, сторонние разработчики находят ошибки не выявленные собственной Quality Assurances.
        Также это дополнительный плюс с точки зрения маркетинга. Современные крупные продукты просто обязаны быть расширяемыми, чтобы не смотреться "бедными родственниками" на фоне более "продвинутых" конкурентов.

        Поддержка расширения приложения может быть весьма и весьма разнообразной, по разному воплощенной в коде и использовать разные технологии. Это уже зависит от конкретной архитектуры проекта, но, так или иначе существуют основные методы: поддержка plug-in'ов (или подключаемых модулей)1, двоичных компонент, автоматически включаемых в приложение, либо макросов, фрагментов кода на макроязыках, автоматизирующих какие-либо из пользовательских процессов. В этой статье будет рассказано о возможности поддержки plug-in'ов.

     
    Введение

        При проектировании проекта необходимо заранее предусмотреть все доступные для расширения элементы. Учтите, реализацию проекта не стоит начинать пока не будет окончательно ясно как именно можно будет расширять проект и какие возможности будут предоставляться пользователю. Когда проект уже будет написан, изменить что-либо будет гораздо сложнее, чем по ходу создания приложения, поэтому отнеситесь к этому процессу особенно серьезно.
        В данной статье будет рассмотрено несколько простых проектов, но при этом постараюсь рассказать как можно более полно о предоставляемых возможностях. Сразу должен предупредить, статья более теоретическая и, вполне вероятно, будет не совсем понятна начинающим программистам, в то же время я попытался сделать статью как можно более простой и понятной для всех, может быть с некоторой потерей последовательности изложения. Если возникнет недопонимание или какие-то проблемы - я постараюсь ответить на ваши вопросы. Мой e-mail неизменен: gaidar@vbstreets.ru.

    Механизм работы plugin'ов

        При запуске программа проверяет список установленных модулей расширения, это может быть просто содержимое определенной директории, список модулей в текстовом или двоичном файле или набор записей в реестре. Определившись со списком модулей, программа последовательно загружает их и вызывает некоторые стандартные методы, присутствующие в каждом модуле, которые описывают функции, выполняемые каждым конкретным модулем.
        Допустим, приложение - текстовый редактор, позволяющий редактировать текст и встроенную в текст графику. Таким образом могут быть два типа расширений - графические и текстовые (для упрощения примера, мы не будем рассматривать единый модуль поддерживающий работу и с текстом и с графикой, хотя это достаточно легкая задача). При вызове стандартного метода, основная программа получает некоторое значение, соответствующее типу модуля и, в соответствии с этим значением, записывает модуль в одну или другую внутреннюю таблицу модулей по принципу "мухи отдельно, котлеты отдельно". Таким списком, например, может служить коллекция, либо просто массив элементов, содержащий ссылки на объекты соответствующие модулям (об объектах см. далее).
        Если во время работы пользователя возникает событие в ответ на которое должен быть предоставлен список модулей и функций, то приложение просматривает свои внутренние таблицы и выдает соответствующий список пользователю.

    Реализация модулей и связующих объектов   

        Теперь я расскажу об одном из способов реализации, заготовка для этого примера прилагается к статье, но она не содержит непосредственно рассматриваемого примера, реализацию которого я оставляю читателю в качестве тренировочного задания (иначе же, читатель совсем ленивым станет, а надо "учится, учится и учится"), используя код из статьи это сделать совсем просто.
        Наше приложение не будет выполнять никаких полезных действий ("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 Извиняюсь за комментарии на английском, но, если честно, не люблю в проектах писать комментарии на английском, в дальнейшем сложнее делиться кодом с "забугорными" коллегами.