Применение рефлексии для создания плагинов
ОГЛАВЛЕНИЕ
AppDomain.CurrentDomain.Load(assemblyName);
Основной класс для динамического получения информации о классах, интерфейсах, их полях, методах и перечислениях - Type. Для получения объекта Type можно воспользоваться несколькими разными методами:- статический метод Type.GetType, который по имени типа возвращает объект Type
- методы GetInterface, GetInterfaces, FindInterfaces, GetElementType и GetTypeArray класса Type
- методы GetType, GetTypes и GetExportedTypes класса Assembly
- методы GetType, GetTypes и FindTypes класса Module
- оператор typeof
Создание экземпляров типов
По объекту Type можно не только определять параметры типа, но и создавать его экземпляры и вызывать их методы. Для этого также существует несколько методов:- методы CreateInstance, CreateInstanceAndUnrap, CrateInstanceFrom и CrateInstanceFromAndUnrap класса AppDomain. После вызова методов, названия которых не оканчиваются на AndUnrap, для доступа к реальным данным нужно вызывать дополнительную функцию Unrap, т.к. эти методы возвращают wrapper (объект класса ObjectHandle) для нового экземляра типа
- методы CreateInstance и CreateInstanceFrom класса Activator. Это специальный класс для создания экземпляров типов и получения ссылок на удаленные объекты. Методу CreateInstance передаются объект Type или название инстанцируемого типа, массив объектов, соответствующих параметрам конструктора типа и объекты CultureInfo. Методу CreateInstanceFrom дополнительно передается имя сборки, содержащий тип. Методы, не принимающие в качестве параметра объект Type, также возвращают wrapper's ObjectHandle
- метод CreateInstance класса Assembly, создающий тип по его имени
- метод Invoke класса ContructorInfo
- метод InvokeMember класса Type
Использование интерфейсов
При создании плагинов обычно используются интерфейсы, определяющие методы и свойства, которые должны реализовываться плагином. Для получения интерфейсов, которые есть у типа, используются методы GetInterface,GetInterfaces и FindInterfaces класса Type. Метод GetInterface по имени интерфейса позвращает объект Type для этого интерфейса или null если такого интерфейса у типа нет. Метод GetInterfaces возвращает массив объектов Type с информацией об интерфейсах. Метод FindInterfaces возвращает массив интерфейсов, выбранных с помощью фильтра - делегата, вызываемого для каждого интерфейса.Если класс реализует несколько интерфейсов, у которых есть методы с одинаковыми названиями, то нужно использовать метод GetInterfaceMap класса Type. Он возвращает объект InterfaceMapping для определения соотношения методов интерфейсов и методов класса, которые их реализуют.
Вызов методов
Обычно методы вызываются с помощью метода InvokeMember класса Type. Процесс вызова метода состоит из двух этапов - привязки, при котором находится нужный метод, и непосредственно вызова. Для вызова нужно указать- имя метода (в качестве метода может быть обычный метод, конструктор, свойство или поле)
- битовую маску из значений BindingFlags для поиска метода. В маске можно указать тип доступа метода, тип метода (поле, свойство, ...), тип данных и пр.
- объект Binder для связывания членов и аргументо
- объект, у которого вызывается метод
- массив аргументов метода
- массив объектов ParameterModifier
- объект CultureInfo
Разработка плагинов
Для демонстрации применения рефлексии при создании плагинов было разработано небольшое тестовое приложение, состоящее из 4 проектов.- MainApp - основное приложение, к которому будут подключаться плагины. Приложение загружает из графических файлов изображения и выводит их на форме
- Interface - определяет интерфейсы IPlugin для плагинов и IMainApp для приложений, к которым будут подключаться плагины
- RandomPlugin и ReversePlugin - плагины для добавления шума к изображениям и отражения изображения по вертикали
public interface IMainApp
{
Bitmap Image { get; set; }
}
public interface IPlugin
{
string Name { get; }
string Version { get; }
string Author { get; }
void Transform(IMainApp app);
}
Если бы наше приложение использовало какие то типы (классы, интерфейсы, перечисления, ...), которые бы использовались или передавались плагинам, то их тоже нужно было бы поместить в сборку Interface.
Основное приложение
Приложение MainApp, к которому мы будем подключать плагины, это простое windows-forms приложение для отображения графический файлов. Оно реализует интерфейс IMainApp - класс формы определен как public class Form1 : System.Windows.Forms.Form, Interface.IMainApp. На форме находится PictureBox для вывода изображения. Для реализации интерфейса IMainApp определяем свойство Image для доступа к изображению.public Bitmap Image
{
get { return (Bitmap)pictureBox.Image; }
set { pictureBox.Image = value; }
}
В конструкторе формы вызывается метод FindPlugins, который находит плагины в папке с приложением и загружает их сборки. Для поиска и загрузки применяется рефлексия. Существует и другой подход - создать для приложения конфигурационный файл, в котором прописаны пути ко всем плагинам. При этом мы не сможем устанавливать плагины путем простого копирования сборок, что не есть хорошо.
void FindPlugins()
{
// папка с плагинами
string folder = System.AppDomain.CurrentDomain.BaseDirectory;
// dll-файлы в этой папке
string[] files = Directory.GetFiles(folder, "*.dll");
foreach (string file in files)
try
{
Assembly assembly = Assembly.LoadFile(file);
foreach (Type type in assembly.GetTypes())
{
Type iface = type.GetInterface("Interface.IPlugin");
if (iface != null)
{
Interface.IPlugin plugin = (Interface.IPlugin)Activator.CreateInstance(type);
plugins.Add(plugin.Name, plugin);
}
}
}
catch (Exception ex)
{
MessageBox.Show("Ошибка загрузки плагина\n" + ex.Message);
}
}
Потенциальной проблемой для нашего кода может стать то, что из домена приложения нельзя выгрузить сборку. Если в папке с приложением окажется много сборок, которые будут загружаться в процессе поиска плагинов, то это приведет к ненужному расходу памяти. В таком случае можно создать новый домен, вызвав статическую функцию AppDomain.CreateDomain, загрузить все сборки в созданный домен и получить названия только тех сборок, которые содержат плагины, выгрузить домен функцией AppDomain(Unload) и загрузить сборки с плагинами в домен.
После того, как все плагины найдены, создаем для них в функции CreatePluginsMenu пункты меню. Названия пунктов меню берутся из ключей в хеш-таблице. Для обработки событий от меню для вызова плагинов создается обработчик OnPluginClick. В обработчике определяется названия пункта меню, который выбрал пользователь, и по нему, как по ключу в хеш-таблице, получаем интерфейс IPlugin соответствующего плагина. У плагина вызывается метод Transform, в качестве параметра this (т.к. класс формы наследуется от интерфейса IMainApp).
void CreatePluginsMenu()
{
// создаем обработчик для команд меню для плагинов
EventHandler handler = new EventHandler(OnPluginClick);
foreach (string name in plugins.Keys)
{
MenuItem item = new MenuItem(name, handler);
menuItemPlugins.MenuItems.Add(item);
}
}
private void OnPluginClick(object sender, EventArgs args)
{
Interface.IPlugin plugin = (Interface.IPlugin)plugins[((MenuItem)sender).Text];
plugin.Transform(this);
}
Создание плагина
Для создания плагинов создаем новый проект ClassLibrary, добавляем ссылку на сборку Interface и реализуем свойства и методы интерфейса IPlugin. Для примера были разработаны 2 плагина: ReverseTransform для отражения изображения по вертикали и RandomTransform для внесения случайного шума в изображение. Приведем их код и результаты применения к изображению.
public class ReverseTransform : Interface.IPlugin
{
public string Name
{
get { return "Переворот изображения"; }
}
public string Version
{
get { return "1.0"; }
}
public string Author
{
get { return "Кондратьев Денис"; }
}
public void Transform(Interface.IMainApp app)
{
Bitmap bitmap = app.Image;
for (int i = 0; i < bitmap.Width; ++i)
for (int j = 0; j < bitmap.Height / 2; ++j)
{
Color color = bitmap.GetPixel(i, j);
bitmap.SetPixel(i, j, bitmap.GetPixel(i, bitmap.Height - j - 1));
bitmap.SetPixel(i, bitmap.Height - j - 1, color);
}
app.Image = bitmap;
}
}
{
public string Name
{
get { return "Случайная трансформация"; }
}
public string Version
{
get { return "1.0"; }
}
public string Author
{
get { return "Кондратьев Денис"; }
}
public void Transform(Interface.IMainApp app)
{
Bitmap bitmap = app.Image;
Random rand = new Random(DateTime.Now.Millisecond);
int pixels = (int)(0.1 * bitmap.Width * bitmap.Height);
for (int i = 0; i < pixels; ++i)
bitmap.SetPixel(rand.Next(bitmap.Width - 1), rand.Next(bitmap.Height), Color.FromArgb(rand.Next(255), rand.Next(255), rand.Next(255)));
app.Image = bitmap;
}
}
Поворот изображения плагином ReverseTransform | Внесение случайного шума плагином RandomTransfor |
Свойства для получения номера версии плагина и авторства в нашей программе не используются, но с их помощью можно выводить информацию об установленных плагинах, как в Adobe Photoshop. После компиляции нужно поместить плагины в папку с приложением MainApp.
Кондратьев Денис