Однажды встала острая нужда просто и безболезненно расширять возможности своего приложения (для простоты будем называть его просто — MyApp), созданного на платформе .NET. Причем расширять возможности может любой пользователь, который в состоянии написать простую библиотеку. И тогда, собственно, и пришла идея создать поддержку плагинов.

После некоторого раздумья была придумана простая (и как мне кажется довольно эффективная) система расширения возможностей. Такая система довольно типична, поэтому вкратце об этапах ее работы:

1. Создаем в папке приложения еще одну папку, например, plugins
2. Копируем наш плагин (о разработке которого будет рассказано ниже) в эту папку
3. Приложение при старте сканирует эту папку на наличие модулей расширения
4. Из модулей извлекаются все метаданные (автор, версия описание и т.д.)
5. Модуль добавляется в список доступных дополнений
6. При необходимости запуска определенного модуля необходимо просто дважды нажать его в списке (в моем проекте все модули перечисляются в элементе ListView).

И вот начинается самое вкусное — начало разработки…

Естественно, необходимо использовать возможности пространства имен System.Reflection для обследования наших дополнений. После некоторых раздумий была принята следующая схема плагина:

1. В новый плагин (например в проект Class Library) добавляем новый класс и называем его Plugin
2. В классе создаются информационные поля (автор, версия, описание)
3. В моем случае плагин должен расширять основные возможности приложения по обработке изображений. По этой причине было принято решение в каждом плагине создать один метод (ProcessImage), который бы принимал объект типа System.Drawing.Image и возвращал бы объект такого же типа, но только уже обработанный логикой плагина.

В нашем случае примерный код основного метода ProcessImage выглядит так:

public static Image ProcessImage(Image srcImage)
{
return Invert((Bitmap)srcImage);
}

public static Bitmap Invert(Bitmap b)
{
BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
int stride = bmData.Stride;
System.IntPtr Scan0 = bmData.Scan0;
unsafe
{
byte* p = (byte*)(void*)Scan0;

int nOffset = stride - b.Width * 3;
int nWidth = b.Width * 3;

for (int y = 0; y < b.Height; ++y)
{
for (int x = 0; x < nWidth; ++x)
{
p[0] = (byte)(255 - p[0]);
++p;
}
p += nOffset;
}
}
b.UnlockBits(bmData);
return b;
}


Данный код очень прост — он выполняет инвертирование цветов изображения.

После написания плагина необходимо вызвать этот код из приложения-хоста.

Как было сказано выше будем использовать пространство имен System.Reflection.
Для начала проверим, что наш плагин — собственно и есть плагин:

public static bool ValidatePlugin(string FileName)
{
try
{
Assembly Asm = Assembly.LoadFile(FileName);
// Проверим наличие основного метода. Если нет, то выдадим Exception
Type t = Asm.GetType("Plugin", true);
if (t.IsClass)
{
foreach (MethodInfo info in t.GetMethods())
{
if (info.Name == "ProcessImage")
{
// Все ОК, поехали...
return true;
}
}
}
}
catch (Exception ex)
{
return false;
}
return true;
}


Итак, в нашем плагине есть основной класс Plugin и в нем есть необходимый нам метод ProcessImage. Заодно мы проверили, что плагин имеет заголовок CLR и является модулем .NET. В противном случае было бы выброшено исключение BadImageFormatException и автоматически плагин не прошел бы проверку.

После проверки плагина начинаем его вызов: создадим метод, который будет принимать имя файла модуля (например, MyPlugin.dll) и входной параметр типа System.Drawing.Image, который является исходным изображением:

public static Image ExecutePlugin(string FileName, Image InputImage)
{
Assembly Asm = Assembly.LoadFile(FileName);

foreach (Type t in Asm.GetTypes())
{
if (t.Name.ToLower() == "plugin")
{
// Получим основной метод
MethodInfo Info = t.GetMethod("ProcessImage");
// Вызовем наш метод с передачей входного изображения
Image ReturnImage = (Image)Info.Invoke(null, new Object[] { InputImage });
return ReturnImage;
}
}
return null;
}


Теперь необходимо получить информацию о плагине. Информация получается из внутренних полей плагина, а не через класс FileVersionInfo.
Мы имеем следующие поля:

public static string PluginName = "Sample.Plugin.Negative";
public static string PluginDisplayName = "Sample Negative Plugin";
public static string PluginDescription = "A sample image processing library that creates a negative copy of image";
public static string PluginCompany = "My Company";
public static string PluginUrl = "http://mycompany.com/sampleplugin/";
public static string PluginVersion = "1.1.2300";


(Может быть это конечно и не идеальный способ хранения информации, но он является наиболее простым).
Теперь получим значение этих полей:

public void PluginInfo(string FileName)
{
_filename = FileName;
Assembly Asm = Assembly.LoadFile(FileName);
foreach (Type t in Asm.GetTypes())
{
if (t.Name.ToLower() == "plugin" && t.IsClass)
{
foreach (FieldInfo Info in t.GetFields())
{
switch (Info.Name.ToLower())
{
case "pluginname":
_name = Info.GetValue(t).ToString();
break;
case "plugindisplayname":
_displayname = Info.GetValue(t).ToString();
break;
case "plugindescription":
_desc = Info.GetValue(t).ToString();
break;
case "plugincompany":
_company = Info.GetValue(t).ToString();
break;
case "pluginurl":
_url = Info.GetValue(t).ToString();
break;
case "pluginversion":
_version = Info.GetValue(t).ToString();
break;
}
}
}
}
}


Мы получили значение необходимых нам полей и извлекли всю информацию.

На этом и заканчивается основной этап разработки дополнений. Так как данный метод очень прост, то он предоставляет жестко заданный метод дополнений, без гибкости разработки самих модулей. На данный момент производится разработка новой версии логики для работы с дополнениями, которая позволяет плагину вызывать внутренние методы приложения-хоста и использовать некоторый его функционал для комбинирования со своим.

(Извиняюсь за возможное отсутствие форматирования кода, т.к. не пойму как сделать его нормальным...)