
- •Добавление к com-серверу поддержки событий
- •Создание модели взаимодействия приложения хоста с плагинами
- •Описание com-интерфейсов, связанных с поддержкой плагинов
- •Создание com-интерфейсов, связанных с поддержкой плагинов
- •Создание com-классов, связанных с поддержкой плагинов
- •Создание класса, отвечающего за взаимодействие с плагинами
- •Код класса Plugin.Cs
- •Внесение косметических изменений в серверную часть
- •Строим своё меню с плагинами и идентификаторами
- •Вносим изменения в главную форму сервера
- •Создание первого плагина
- •Импорт типов с сервера
- •Создание класса для хранения внутреннего представления точек
- •Создание форм редактирования данных о точках
- •Реализация форм редактирования данных о точках
- •Реализация класса первого плагина
- •Создание третьего плагина
- •Делаем наш плагин com-видимым
- •Создание главной формы плагина
- •Реализация класса третьего плагина
- •Создание инсталлятора для плагина
- •Создание второго плагина
- •Создание главной формы плагина
- •Создание библиотеки типов
- •Реализация формы второго плагина
- •Реализация класса второго плагина
- •Добавление метода GetComClassName
- •Data Execution Prevention и его отключение
- •Тестирование совместной работы клиента и плагинов
- •Исходный код
Создание класса, отвечающего за взаимодействие с плагинами
Мы подобрались к самому интересному. Сейчас мы напишем класс, который будет осуществлять взаимодействие между нашим сервером и плагинами. Добавим файл класса в проект. Так как это будет не COM-класс, то добавим его в самую внешнюю директорию проекта.
Добавим пару необходимых нам неймспейсов.
using System.Runtime.InteropServices;
using System.Reflection;
Интероп нам понадобится для взаимодействия с COM и импорта процедур из обычных (не .NET-овых) виндовых библиотек. Рефлекшен нам нужен будет для работой с плагинами, написанными на технологии .NET и не содержащими в себе COM-сервера.
Сначала немного подготовительной работы. Я решил быть няшей и создать здесь набор исключений, ведь возвращать ошибки через их код уже который год не модно, а мы же с вами модные программисты, не так ли? Тогда добавим в неймспейс следующие классы:
#region Исключения, связанные с плагинами
class CannotLoadDllAsPluginException : Exception
{
string dllPath;
public CannotLoadDllAsPluginException(string dllPath)
{ this.dllPath = dllPath; }
public string DllPath
{ get { return dllPath; } }
public override string Message
{ get { return "Невозможно загрузить dll как плагин сервера СПО: " + dllPath; } }
}
class InternalPluginErrorException : Exception
{
int errorCode;
public InternalPluginErrorException(int errorCode)
{ this.errorCode = errorCode; }
public int ErrorCode
{ get { return errorCode; } }
}
class PluginNotConnectedYetException : Exception
{
public PluginNotConnectedYetException()
{ }
}
#endregion
Лепота. Переходим непосредственно к работе с плагинами. Пропишем, что класс у нас наследуется от IDisposable. В методе Dispose (что вызывается деконструктором, если кто не знал) будем насильно отключать плагин от приложения в том случае, если он подключен.
class Plugin : IDisposable
Добавим в класс строку, которая будет содержать путь к библиотеке.
/// <summary> Путь к плагину. </summary>
string path;
Далее, добавим несколько булевых переменных, задающих состояние подключенности и тип плагина.
#region Набор булевых признаков класса.
/// <summary> Показывает, написан ли плагин в управляемом коде, т.е. на семействе языков платформы .NET. </summary>
bool isManaged = false;
/// <summary> Показывает, содержит ли плагин COM-сервер. </summary>
bool isCOMServer = false;
/// <summary> Показывает, подключен ли плагин. </summary>
bool isConnected = false;
#endregion
Далее импортируем процедуры из библиотеки kernel32.dll, необходимые нам для работы с библиотеками, написанными на языках, не принадлежащих к семейству .NET, то есть в неуправляемом (unmanaged) коде. В принципе, сами эти импортируемые процедуры тоже написаны в неуправляемом коде, за подробностями импорта – сюда.
#region Импорт из kernel32.dll для взаимодействия с неуправляемыми библиотеками.
/// <summary>
/// Загружает библиотеку с заданным путём в память приложения и возвращает её хэндл.
/// </summary>
/// <param name="dllToLoad">Путь к библиотеке.</param>
/// <returns>Хэндл загруженной библиотеки.</returns>
[DllImport("kernel32.dll")]
private static extern IntPtr LoadLibrary(string dllToLoad);
/// <summary>
/// Получает указатель процедуры библиотеки по имени процедуры и хэндлу библиотеки.
/// </summary>
/// <param name="hModule">Хэндл библиотеки.</param>
/// <param name="procedureName">Имя процедуры.</param>
/// <returns>Возвращает указатель на процедуру.</returns>
[DllImport("kernel32.dll")]
private static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
/// <summary>
/// Закрывает библиотеку и выгружает её из памяти приложения.
/// </summary>
/// <param name="hModule">Хэндл библиотеки.</param>
/// <returns>Возвращает булевый признак успешного завершения операции</returns>
[DllImport("kernel32.dll")]
private static extern bool FreeLibrary(IntPtr hModule);
#endregion
Если вы новенький в шарпе то вы могли бы заметить директиву #region. Её мы используем, чтобы обозначать блоки кода, которые можно сворачивать и разворачивать в редакторе.
Затем добавим делегаты и методы, которые мы будем использовать для работы с плагинами в неуправляемом коде:
#region Делегаты для привзяки к неуправляемым процедурам.
private delegate string GetCOMClassNameDelegate();
private delegate int ConnectDelegate(object iZigzagControl, ref object iPluginConnectionInfo);
private delegate int DisconnectDelegate();
private delegate int PerformActionDelegate(int actionId);
/// <summary>
/// Вызов процедуры получения имени COM-класса плагина, содержащейся в unmanaged-библиотеке.
/// </summary>
private GetCOMClassNameDelegate GetCOMClassNameUnmanaged;
/// <summary>
/// Вызов процудуры подключение к плагину, содержащейся в unmanaged-библиотеке.
/// </summary>
private ConnectDelegate ConnectUnmanaged;
/// <summary>
/// Вызов процудуры отключения от плагина, содержащейся в unmanaged-библиотеке.
/// </summary>
private DisconnectDelegate DisconnectUnmanaged;
/// <summary>
/// Вызов процудуры выполнения действия плагина с заданным идентификатором, содержащейся в unmanaged-библиотеке.
/// </summary>
private PerformActionDelegate PerformActionUnmanaged;
#endregion
Каждый делегат задаёт набор параметров и возвращаемое значение, то есть сигнатуру функции. Если вы шарите в C++, то можно сказать, что делегат – это типобезопасный аналог указателя на функцию. Таким образом, объявление private GetCOMClassNameDelegate GetCOMClassNameUnmanaged фактически создаёт функцию с параметрами и возвращаемым значением, указанным в объявлении делегата. В нашем случае функция GetCOMClassNameUnmanaged не будет иметь параметров и будет возвращать строку. Следовательно, подобное объявление какбы создаёт переменную со значением функции. Я хочу узнать больше о делегатах и потому перехожу по этой ссылке.
Вызывать мы будем в основном GetCOMClassNameUnmanaged. Работу с unmanaged-плагинами без COM-серверов я совсем не тестировал.
Зададим ряд переменных для вызова методов из управляемых библиотек, не содержащих COM-сервера. Переменные имеют тип MethodInfo, объявленный в System.Reflection, содержащий в себе метаданные о методе. Из этого типа можно вызывать (invoke) методы с заданным набором аргументов. Сейчас это несколько устаревшая техника и все нормальные люди, использующие Framework 4 и выше, применяют для этого новый тип dynamic, но пока будем ортодоксальны.
#region Переменные для вызова методов из управляемых библиотек, не содержащих COM-сервера.
//Сделаны для поддержки Framework 3.5. В Framework 4 можно использовать тип dynamic и не обламываться.
private MethodInfo connectMethod, disconnectMethod, performActionMethod, getComClassNameMethod;
#endregion
Зададим список приватных полей для хранения информации о плагине.
#region Информация о плагине, полученная из его IPluginConnectionInfo
/// <summary> Информация о плагине, полученная при подключении. </summary>
IPluginConnectionInfo pluginConnectionInfo;
/// <summary> Имя плагина. </summary>
private string pluginName;
/// <summary> Описание плагина. </summary>
private string pluginDescription;
/// <summary> Список действий плагина. </summary>
private List<IPluginAction> actions = new List<IPluginAction>();
#endregion
И, наконец, ещё тройка переменных.
/// <summary> Содержит COM-объект с плагином. Используется как для плагинов в управляемом коде, так и для плагинов в коде неуправляемом. </summary>
private IPlugin COMPlugin;
/// <summary> Хэндл библиотеки с неуправляемым плагином. </summary>
private IntPtr unmanagedPluginDllHandle;
/// <summary> Экземлпяр класса ZigzagControl. </summary>
private IZigzagControl iZigzagControl;
Думаю, с ними всё ясно по описанию. Добавим публичные поля для доступа к данным о плагине извне.
#region Поля класса
/// <summary> Получает имя плагина. </summary>
public string Name
{
get
{ return pluginName; }
}
/// <summary> Получает описание плагина. </summary>
public string Description
{
get
{ return pluginDescription; }
}
/// <summary> Получает список действий, которые плагин может выполнить. </summary>
public List<IPluginAction> ActionsList
{
get
{ return new List<IPluginAction>(actions.ToArray()); }
}
/// <summary> Получает состояние подключенности плагина. </summary>
public bool IsConnected
{
get
{ return isConnected; }
}
#endregion
Отлично, есть всё, что нужно, чтобы переходить к реализации функционала. Закодим конструктор класса.
/// <summary>
/// Конструктор класса Plugin.
/// </summary>
/// <param name="dllPath">Путь к библиотеке, содержащей плагин.</param>
public Plugin(string dllPath)
Сначала сохраним путь и проверим наличие файла.
path = dllPath;
System.IO.FileInfo fi = new System.IO.FileInfo(dllPath);
if (!fi.Exists)
throw new System.IO.FileNotFoundException("dll-файл плагина не найден.", dllPath);
Попытаемся найти в библиотеке сборку. Если библиотека написана в управляемом коде, то метод найдёт сборку и перейдёт на return, иначе программа выбросит исключение. Дальнейшие комментарии – прямо по коду.
try
{
// Грузим либу.
Assembly pluginAssembly = Assembly.LoadFile(path);
isManaged = true;
// Будем считать, что управляемая библиотека должна содержать тип ZigzagPlugin
// Если библиотека содержит внутренний COM-сервер, то этот тип содержит метод string GetCOMClassName(), который возвращает имя COM-класса при его вызове.
// В противном случае он содержит все те методы, которые объявлены в IPlugin.
Type spoServerPluginType = pluginAssembly.GetType("ZigzagPlugin"); //Получим тип через reflection.
getComClassNameMethod = spoServerPluginType.GetMethod("GetCOMClassName", System.Type.EmptyTypes); //Пошмонаем его на наличие нужных нам методов.
// В GetMethod передаётся имя искомой функции и массив типов её аргументов
if (getComClassNameMethod != null)
{
// Если метод, возвращающий имя COM-класса найден, стало быть либа содержит COM-сервер и остальные три метода искать нет смысла -- получим их через IPlugin в тот момент, когда потребуются, а не сейчас.
isCOMServer = true;
return;
}
// В противном случае -- поищем остальные методы.
connectMethod = spoServerPluginType.GetMethod("Connect", new Type[] { typeof(object), typeof(object).MakeByRefType() });
disconnectMethod = spoServerPluginType.GetMethod("Disconnect", System.Type.EmptyTypes);
performActionMethod = spoServerPluginType.GetMethod("PerformAction", new Type[] { typeof(int) });
if ((connectMethod != null) && (disconnectMethod != null) &&
(performActionMethod != null))
return; // Все необходимые методы нашлись, работа конструктора завершена.
}
catch (Exception)
{ }
В случае, если библиотека написана в неуправляемом коде, либо как-то криво написана в управляемом (в том, например, смысле криво, что не хватает каких-то необходимых для работы плагина методов, если методы названы неприавильно, имеют другой набор параметров, etc), программа перейдёт дальше этого блока try-catch. Работа с unmanaged-библиотекой аналогичная.
isManaged = false;
try
{
//Здесь логика работы аналогичная, только уже не через reflection, а через interop.
//Грузим либу.
unmanagedPluginDllHandle = LoadLibrary(path);
//Ищем функции.
IntPtr getComClassNameMethodPtr = GetProcAddress(unmanagedPluginDllHandle, "GetCOMClassName");
if (getComClassNameMethodPtr != IntPtr.Zero)
{
// Если метод, возвращающий имя COM-класса найден, стало быть либа содержит COM-сервер и остальные
// три метода искать нет смысла -- получим их через IPlugin в тот момент, когда потребуются, а не сейчас.
isCOMServer = true;
GetCOMClassNameUnmanaged = (GetCOMClassNameDelegate)Marshal.GetDelegateForFunctionPointer(getComClassNameMethodPtr, typeof(GetCOMClassNameDelegate));
return;
}
// Это не COM, ищем другие методы.
IntPtr connectMethodPtr = GetProcAddress(unmanagedPluginDllHandle, "Connect");
IntPtr disconnectMethodPtr = GetProcAddress(unmanagedPluginDllHandle, "Disconnect");
IntPtr performActionPtr = GetProcAddress(unmanagedPluginDllHandle, "PerformAction");
if ((connectMethodPtr != IntPtr.Zero) && (disconnectMethodPtr != IntPtr.Zero) && (performActionPtr != IntPtr.Zero))
{
#region Работа этой секции кода не тестировалась
ConnectUnmanaged = (ConnectDelegate)Marshal.GetDelegateForFunctionPointer(connectMethodPtr, typeof(ConnectDelegate));
DisconnectUnmanaged = (DisconnectDelegate)Marshal.GetDelegateForFunctionPointer(disconnectMethodPtr, typeof(DisconnectDelegate));
PerformActionUnmanaged = (PerformActionDelegate)Marshal.GetDelegateForFunctionPointer(performActionPtr, typeof(PerformActionDelegate));
return; // Все необходимые методы нашлись, работа конструктора завершена.
#endregion
}
}
catch (Exception)
{ }
Если и здесь чего-то не получилось, то это может значить только то, что плагин нам подсунули палёный – выбросим исключение.
throw new CannotLoadDllAsPluginException(path);
Конструктор завершен. Добавим небольшой вспомогательный метод, который заполняет поля класса информацией из структуры IPluginConnectionInfo.
/// <summary>
/// Заполняет поля с информацией о плагине значениями из IPluginConnectionInfo
/// </summary>
/// <param name="info"></param>
private void FillInfo(IPluginConnectionInfo info)
{
pluginName = info.PluginName;
pluginDescription = info.PluginDescription;
IPluginAction[] pluginActions = info.AvailableActions;
actions = new List<IPluginAction>(pluginActions);
}
Теперь напишем метод для подключения плагина к приложению.
/// <summary>
/// Подключает плагин к приложению.
/// </summary>
/// <param name="iZigzagControl">Экземпляр класса ZigzagControl.</param>
/// <returns>Возвращает 0, если плагин подключился успешно, иначе возвращает код ошибки.</returns>
public int Connect(IZigzagControl iZigzagControl)
Сделаем сначала вот такую заготовку, чтобы нельзя было подключить один и тот же плагин дважды.
this.iZigzagControl = iZigzagControl;
if (!isConnected)
{
}
else
return 0;
Далее в этом методе всё будет писаться внутри этой конструкции if. Инициализируем некоторые переменные.
int result = 0;
object connectionInfo = null;
pluginConnectionInfo = null;
Теперь обработаем тот вариант, в котором наш плагин содержит внутренний COM-сервер. Пояснения в комментариях к коду.
if (isCOMServer)
{
// Преимуществом технологии COM является то, что можно единообразно обращаться как с managed, так и c unmanaged-кодом. Различия будут лишь в способе вызова метода получения имени COM-класса
// Сначала получим имя класса.
string typeName;
if (isManaged)
typeName = (string)getComClassNameMethod.Invoke(null, null);
else
typeName = GetCOMClassNameUnmanaged();
if ((typeName == null) || (typeName == ""))
return -404;
Type type = Type.GetTypeFromProgID(typeName); // Получим экземпляр класса по его имени.
object plugin = Activator.CreateInstance(type); // Создадим его. Будет использован конструктор по умолчанию.
COMPlugin = plugin as IPlugin; // Прикастим к нашему интерфейсу.
result = COMPlugin.Connect(iZigzagControl, ref connectionInfo); // Подключим.
pluginConnectionInfo = (IPluginConnectionInfo)connectionInfo; // Получим информацию о нём.
if ((pluginConnectionInfo == null) || (result != 0))
throw new InternalPluginErrorException(result);
FillInfo(pluginConnectionInfo); // Заполним поля нашего класса полученной информации.
isConnected = true;
return result; // Готово.
}
В случае, если плагин не содержит COM-сервера, обработаем варианты managed- и unmanaged-плагина.
if (isManaged)
{
object[] args = new object[] { iZigzagControl, connectionInfo }; // Создаем массив с аргументами.
result = (int)connectMethod.Invoke(null, args); // Вызываем исполнение метода.
pluginConnectionInfo = (IPluginConnectionInfo)args[1]; // Если метод выполнился без ошибок, то во втором аргументе будет инфа о плагине.
if ((pluginConnectionInfo == null) || (result != 0))
throw new InternalPluginErrorException(result);
FillInfo(pluginConnectionInfo); //Заполняем поля класса
isConnected = true;
return result;
}
else
{
#region Работа этой секции кода не тестировалась
result = ConnectUnmanaged(iZigzagControl, ref connectionInfo);
pluginConnectionInfo = (IPluginConnectionInfo)connectionInfo;
if ((pluginConnectionInfo == null) || (result != 0))
throw new InternalPluginErrorException(result);
FillInfo(pluginConnectionInfo);
isConnected = true;
return result;
#endregion
}
Метод подключения закончен. Переходим к методу отключения.
/// <summary>
/// Отключает плагин от приложения.
/// </summary>
/// <returns>Возвращает 0, если плагин подключился успешно, иначе возвращает код ошибки.</returns>
public int Disconnect()
Сначала обернём весь код большим try-catch и if-ом.
try
{
if (isConnected)
{
}
else
return 0;
}
catch (Exception)
{ return 1; }
Как-то коды ошибок не унифицированы и в разнобой, ну да ладно, если вам будет не лень – унифицируете. Я сейчас слишком сонный для работы с числами. А далее план такой же, как и в предыдущем методе. Сначала прорабатываем вариант с COM-плагином.
int result = 0;
if (isCOMServer)
{
result = COMPlugin.Disconnect();
if (!isManaged)
FreeLibrary(unmanagedPluginDllHandle);
if (result != 0)
throw new InternalPluginErrorException(result);
isConnected = false;
return result;
}
А потом, в случае, если это не COM, отрабатываем отдельно для managed и unmanaged.
if (isManaged)
{
result = (int)disconnectMethod.Invoke(null, null);
if (result != 0)
throw new InternalPluginErrorException(result);
isConnected = false;
return result;
}
else
{
#region Работа этой секции кода не тестировалась
result = DisconnectUnmanaged();
if (result != 0)
throw new InternalPluginErrorException(result);
FreeLibrary(unmanagedPluginDllHandle);
isConnected = false;
return result;
#endregion
}
Метод отключения тоже готов. Далее – метод вызова действия плагина.
/// <summary>
/// Выполняет действие, доступное из плагина.
/// </summary>
/// <param name="actionId">Идентификатор действия.</param>
/// <returns>Возвращает 0, если плагин подключился успешно, иначе возвращает код ошибки.</returns>
public int PerformAction(int actionId)
Тут всё аналогично и просто, потому просто приведу здесь текст метода:
if (isConnected)
{
int result = 0;
if (isCOMServer)
{
// В случае, если плагин имеет COM-сервер, просто запрашиваем выполнение действия и контроллируем результат.
result = COMPlugin.PerformAction(actionId);
if (result != 0)
throw new InternalPluginErrorException(result);
return result;
}
//Иначе, в зависимости от управляемости кода, запрашиваем выполнение его другими путями.
if (isManaged)
{
try
{
result = (int)performActionMethod.Invoke(null, new object[] { actionId });
if (result != 0)
throw new InternalPluginErrorException(result);
}
catch (TargetInvocationException)
{ result = -100; }
return result;
}
else
{
#region Работа этой секции кода не тестировалась
result = PerformActionUnmanaged(actionId);
if (result != 0)
throw new InternalPluginErrorException(result);
return result;
#endregion
}
}
else
throw new PluginNotConnectedYetException();
И осталось только реализовать IDisposable. Напоминаю, метод Dispose вызывается сборщиком мусора тогда, когда экземпляр готовится к уничтожению, когда на него уже не осталось ссылок. Здесь мы будем отключать плагин в том случае, если он подключен.
#region Члены IDisposable
/// <summary>
/// Деструктор класса, который будет автоматически освобождать объект после того, как не осталось ни одной ссылки на него.
/// </summary>
public void Dispose()
{
if (isConnected)
Disconnect();
}
#endregion
Класс поддержки плагинов готов, поздравляю вас.