Существует большая потребность в составных приложениях (composite applications), но требования к отказоустойчивости варьируются. В некоторых сценариях может быть нормально, когда один сбойный плагин обрушивает все приложение. В других сценариях это неприемлемо. В этой статье я опишу архитектуру отказоустойчивого составного настольного приложения. Предлагаемая архитектура обеспечит высокий уровень изоляции за счет выполнения каждого плагина в своем Windows-процессе. Я создаю ее, учитывая следующие цели проектирования:
- жесткую изоляцию между хостом и плагинами;
- полную визуальную интеграцию подключаемых элементов управления с окном хоста;
- простую разработку новых плагинов;
- сравнительно простое преобразование существующих приложений в плагины;
- возможность для плагинов использовать сервисы, предоставляемые хостом, и наоборот;
- сравнительно простое добавление новых сервисов и интерфейсов.
В сопутствующем исходном коде содержатся два решения Visual Studio 2012: WpfHost.sln и Plugins.sln. Сначала скомпилируйте хост, а затем плагины. Основной исполняемый файл — WpfHost.exe. Сборки плагинов загружаются по запросу. Законченное приложение показано на рис. 1.
Рис. 1. Окно хоста бесшовно интегрируется с внешними плагинами
Обзор архитектуры
Хост отображает элемент управления «вкладка» (tab control) и кнопку «+» в верхнем левом углу, которая открывает список доступных плагинов. Список плагинов считывается из XML-файла plugins.xml, но возможны реализации альтернативных каталогов. Каждый плагин выполняется в своем процессе, и в хост-процесс никакие сборки плагинов не загружаются. Высокоуровневая схема этой архитектуры приведена на рис. 2.
Увеличить
Рис. 2. Высокоуровневая схема архитектуры приложения
Plug-In Process | Процесс плагина |
remoting | удаленное взаимодействие |
На внутреннем уровне хост плагинов является обычным WPF-приложением (Windows Presentation Foundation), которое следует парадигме Model-View-ViewModel (MVVM). Модель представлена классом PluginController, который хранит набор загруженных плагинов. Каждый загруженный плагин представлен экземпляром класса Plugin, который содержит один элемент управления — плагин и взаимодействует с процессом одного плагина.
Хост-система состоит из четырех сборок, организованных так, как показано на рис. 3.
Рис. 3. Сборки хост-системы
WpfHost.exe — это хост-приложение. PluginProcess.exe — процесс плагина. Один экземпляр этого процесса загружает один плагин. WpfHost.Interfaces.dll содержит общие интерфейсы, используемые хостом, процессом плагина и самими плагинами. PluginHosting.dll хранит типы, применяемые хостом и процессом плагина для хостинга плагинов.
Загрузка плагина включает вызовы, которые должны выполняться в UI-потоке, и вызовы, которые можно выполнять в любом потоке. Чтобы сделать приложение «отзывчивым», я блокирую UI-поток только при абсолютной необходимости. Следовательно, программный интерфейс для класса Plugin разбивается на два метода: Load и CreateView:
class Plugin
{
public FrameworkElement View { get; private set; }
public void Load(PluginInfo info); // Можно выполнять в любом потоке
public void CreateView(); // Должен выполняться в UI-потоке
}
Метод Plugin.Load запускает процесс плагина и создает инфраструктуру на стороне этого процесса. Он выполняется в рабочем потоке. Метод Plugin.CreateView связывает локальное представление с удаленным FrameworkElement. Вам придется выполнять это в UI-потоке, чтобы избежать исключений вроде InvalidOperationException.
Класс Plugin в конечном счете вызывает пользовательский класс плагина внутри процесса плагина. Единственное требование к этому пользовательскому классу — он должен реализовать интерфейс IPlugin из сборки WpfHost.Interfaces:
public interface IPlugin : IServiceProvider, IDisposable
{
FrameworkElement CreateControl();
}
FrameworkElement, возвращаемый от плагина, может быть произвольной сложности. Это может быть единственное текстовое поле или тщательно проработанный пользовательский элемент управления, реализующий какое-то специализированное бизнес-приложение (line-of-business, LOB).
Потребность в составных приложениях
За последние несколько лет ряд моих клиентов говорили об одном и том же бизнес-требовании: им нужны настольные приложения, способные загружать внешние плагины, тем самым комбинируя несколько LOB-приложений под одной «крышей». Причины этого требования могут быть разными. Несколько групп могут разрабатывать разные части приложения по разным временным графикам. Разным бизнес-пользователям могут требоваться разные наборы функциональности. Или, возможно, клиенты хотят обеспечить стабильность «базового» приложения, в то же время получая гибкость. Так или иначе, требование хостинга сторонних плагинов неоднократно предъявлялось разными организациями.
Существует несколько традиционных решений этой задачи: классический Composite Application Block (CAB), Managed Add-In Framework (MAF), Managed Extensibility Framework (MEF) и Prism. Другое решение было опубликовано моими бывшими коллегами, Геннадием Слободским (Gennady Slobodsky) и Леви Хаскеллом (Levi Haskell), в номере «MSDN Magazine» за август 2013 г. (см. статью «Architecture for Hosting Third-Party .NET Plug-Ins» по ссылке msdn.microsoft.com/magazine/dn342875). Все эти решения обладают большой ценностью, и многие полезные приложения были созданы с их использованием. Я тоже являюсь активным пользователем этих инфраструктур, но есть одна проблема, которая довольно давно преследует меня: стабильность.
Приложения рушатся. Это суровая реальность жизни. Нулевые ссылки, необработанные исключения, блокированные файлы и поврежденные базы данных никуда в ближайшее время не исчезнут. Хорошее хост-приложение должно выживать при крахе плагина и продолжать свою работу. Нельзя позволить сбойному плагину обрушить хост или другие плагины. Эта защита не обязательно должна быть пуленепробиваемой; я не пытаюсь предотвратить попытки атак злонамеренных хакеров. Однако такие простые ошибки, как необработанное исключение в рабочем потоке, не должно влиять на работу хоста.
Хорошее хост-приложение должно выживать при крахе плагина и продолжать свою работу.
Уровни изоляции
Приложения Microsoft .NET Framework могут обрабатывать сторонние плагины минимум тремя способами.
- Без изоляции Хост и все плагины выполняются в одном процессе с одним AppDomain.
- Средняя изоляция Каждый плагин загружается в свой AppDomain.
- Жесткая изоляция Каждый плагин загружается в свой процесс.
Первый вариант влечет за собой минимальную защиту и минимальный контроль. Все данные доступны глобально, нет защиты от сбоев, и нет никакого способа выгрузки проблемного кода. Самая типичная причина краха такого приложения — необработанное исключение в рабочем потоке, созданном плагином.
Вы можете попробовать защитить потоки хоста с помощью блоков try/catch, но, когда дело доходит до потоков, создаваемых плагинами, «все ставки отменяются». Начиная с .NET Framework 2.0, необработанное исключение в любом потоке приводит к завершению процесса, и предотвратить это нельзя. Для такой кажущейся грубости есть веская причина: необработанное исключение означает, что приложение, возможно, находится в нестабильном состоянии и разрешить ему продолжать работу слишком опасно.
Средняя изоляция обеспечивает больший контроль над безопасностью и конфигурацией плагина. Кроме того, вы можете выгружать плагины, по крайней мере, когда все идет нормально и никакие потоки не заняты выполнением неуправляемого кода. Однако хост-процесс по-прежнему не защищен от краха плагинов, как продемонстрировано в моей статье «AppDomains Won’t Protect Host from a Failing Plug-In» (bit.ly/1fO7spO). Спроектировать надежную стратегию обработки ошибок трудно, если вообще возможно, а выгрузка сбойного AppDomain не гарантируется.
AppDomain были изобретены для хостинга приложений ASP.NET как облегченная альтернатива процессам. Подробнее см. публикацию в блоге Криса Брумма (Chris Brumme) «AppDomains (application domains)» за 2003 г. по ссылке bit.ly/PoIX1r. ASP.NET в какой-то мере применяет политику невмешательства в области отказоустойчивости. Рухнувшее веб-приложение может легко остановить весь рабочий процесс с несколькими приложениями. В этом случае ASP.NET просто перезапускает рабочий процесс и заново выдает любые незавершенные веб-запросы. Это разумное проектировочное решение для серверного процесса, в котором нет окон для взаимодействия с пользователем, но в настольных приложениях такой вариант может работать не так хорошо.
Жесткая изоляция обеспечивает высший уровень защиты от сбоев. Поскольку каждый плагин выполняется в собственном процессе, плагины не могут обрушить хост, и при необходимости они могут быть завершены в любой момент. В то же время это решение требует довольно сложной архитектуры. Приложение должно иметь дело со взаимодействием между процессами и с синхронизацией. Кроме того, оно должно выполнять маршалинг WPF-элементов управления между границами процессов, что совсем не тривиально.
Как и с другими вещами в разработке ПО, выбор уровня изоляции представляет собой компромисс. Более жесткая изоляция дает вам больший контроль и гибкость, но за это вы расплачиваетесь более высокой сложностью приложения и меньшей производительностью.
В некоторых инфраструктурах отказоустойчивость игнорируется, и они работают на уровне «без изоляции». Хорошие примеры такого подхода — MEF и Prism. В случаях, где отказоустойчивость и тонкая настройка конфигурации плагинов не составляют проблемы, это простейшее решение, которое работает, а значит, имеет право на существование.
Многие архитектуры плагинов, включая предложенную Слободским и Хаскеллом, используют среднюю изоляцию. Они достигают изоляции через AppDomain. Эти AppDomain обеспечивают разработчикам хоста значительный уровень контроля над конфигурацией и безопасностью плагинов. Лично я за последние несколько лет создал ряд решений на основе AppDomain. Если приложение требует выгрузки кода, создания изолированной среды и контроля конфигурации, но отказоустойчивость не является проблемой, то AppDomain как раз то, что надо.
MAF выделяется среди инфраструктур надстроек, так как позволяет разработчикам хоста выбирать любой из трех уровней изоляции. Она может выполнять надстройку (add-in) в собственном процессе, используя класс AddInProcess. К сожалению, AddInProcess изначально не работает для визуальных компонентов. Вы можете расширить MAF так, чтобы обеспечить маршалинг визуальных компонентов между процессами, но это приведет к появлению еще одного уровня сложности в и без того сложной инфраструктуре. Создание MAF-надстроек — дело непростое, а с дополнительным уровнем над MAF оно и вовсе грозит перерасти в нечто неуправляемое.
Предлагаемая мной архитектура нацелена на то, чтобы заполнить пустоты, и предоставить надежное решение хостинга, способное загружать плагины в отдельные процессы и обеспечивать визуальную интеграцию между плагинами и хостом.
Жесткая изоляция визуальных компонентов
Когда запрашивается загрузка плагина, хост-процесс порождает новый дочерний процесс. Затем этот дочерний процесс загружает пользовательский класс плагина, который создает FrameworkElement, отображаемый в окне хоста (рис. 4).
Увеличить
Рис. 4. Маршалинг FrameworkElement между процессами плагина и хоста
Created by | Создается |
Windows presentation foundation host process | Процесс WPF-хоста |
Plug-in Process | Процесс плагина |
remoting | удаленное взаимодействие |
Remoting Proxy | Прокси удаленного взаимодействия |
Прямой маршалинг FrameworkElement между процессами невозможен. Он не наследует от MarshalByRefObject и не помечен как [Serializable], поэтому .NET-механизм удаленного взаимодействия (remoting) не будет выполнять его маршалинг. Кроме того, он не помечен атрибутом [ServiceContract], поэтому его маршалинг не будет выполняться и Windows Communication Foundation (WCF). Чтобы преодолеть эту проблему, я использую класс System.Addin.FrameworkElementAdapters из сборки System.Windows.Presentation, которая является частью MAF. В этом классе определены два метода:
- ViewToContractAdapter — преобразует FrameworkElement в интерфейс INativeHandleContract, маршалинг которого возможен через .NET-механизм удаленного взаимодействия. Этот метод вызывается из процесса плагина;
- ContractToViewAdapter — преобразует интерфейс INativeHandleContract обратно в FrameworkElement. Этот метод вызывается из процесса хоста.
К сожалению, простое сочетание этих двух методов изначально работает не очень хорошо. Очевидно, MAF рассчитана на маршалинг WPF-компонентов между AppDomain, а не между процессами. Метод ContractToViewAdapter завершается неудачей на клиентской стороне со следующей ошибкой:
System.Runtime.Remoting.RemotingException:
Permission denied: cannot call non-public or static methods remotely
Корневая причина в том, что метод ContractToViewAdapter вызывает конструктор класса MS.Internal.Controls.AddInHost, который пытается привести прокси удаленного взаимодействия (remoting proxy) INativeHandleContract к типу AddInHwndSourceWrapper. Если приведение выполняется успешно, тогда он вызывает внутренний метод RegisterKeyboardInputSite в прокси удаленного взаимодействия. Вызов внутренних методов в прокси, действующих между процессами, запрещен. Вот что происходит в конструкторе класса AddInHost:
// Из Reflector
_addInHwndSourceWrapper = contract as AddInHwndSourceWrapper;
if (_addInHwndSourceWrapper != null)
{
_addInHwndSourceWrapper.RegisterKeyboardInputSite(
new AddInHostSite(this)); // вызов внутреннего метода!
}
Чтобы исключить эту ошибку, я создал класс NativeContractInsulator. Этот класс находится на серверной стороне (в плагине). Он реализует интерфейс INativeHandleContract, пересылая все вызовы исходному INativeHandleContract, возвращаемому методом ViewToContractAdapter. Однако в отличие от исходной реализации его нельзя привести к AddInHwndSourceWrapper. Тем самым приведение на клиентской стороне (хосте) заканчивается неудачей, и запрещенного вызова внутреннего метода не происходит.
Более подробное описание архитектуры плагинов
Методы Plugin.Load и Plugin.CreateView создают все необходимые детали для интеграции плагинов.
На рис. 5 показан конечный граф объектов. Он довольно сложен, но каждая часть играет свою роль. Совместно они обеспечивают надежную работу системы «хост — плагины».
Увеличить
Рис. 5. Диаграмма объектов загруженного плагина
Windows Presentation Foundation Host Process | Процесс WPF-хоста |
wraps | обертывает |
: INativeHandleContract Remoting Proxy | Прокси удаленного взаимодействия : INativeHandleContract |
Plug-In Process | Процесс плагина |
creates | Создает |
User-Defined implementation of IPlugin | Пользовательская реализация IPlugin |
Класс Plugin представляет один экземпляр плагина в хосте. Он содержит свойство View, которое является визуальным представлением плагина в хост-процессе. Класс Plugin создает экземпляр PluginProcessProxy и получает от него IRemotePlugin. Последний включает элемент управления удаленно взаимодействующего плагина в виде INativeHandleContract. Затем класс Plugin принимает этот контракт и преобразует его в FrameworkElement, как показано ниже (часть кода опущена для краткости):
public interface IRemotePlugin : IServiceProvider, IDisposable
{
INativeHandleContract Contract { get; }
}
class Plugin
{
public void CreateView()
{
View = FrameworkElementAdapters.ContractToViewAdapter(
_remoteProcess.RemotePlugin.Contract);
}}
Класс PluginProcessProxy управляет жизненным циклом процесса плагина из хоста. Он отвечает за запуск процесса плагина, создание канала удаленного взаимодействия (remoting channel) и мониторинг работоспособности процесса плагина. Кроме того, он обращается к сервису PluginLoader и получает от него IRemotePlugin.
Класс PluginLoader выполняется в процессе плагина и реализует жизненный цикл этого процесса. Он устанавливает канал удаленного взаимодействия, запускает WPF-диспетчер сообщений, загружает пользовательский плагин, создает экземпляр RemotePlugin и передает его в PluginProcessProxy на стороне хоста.
Класс RemotePlugin обеспечивает маршалинг пользовательского плагина (элемента управления) через границы процессов. Он преобразует FrameworkElement пользовательского плагина в INativeHandleContract, а затем обертывает этот контракт в NativeHandleContractInsulator, чтобы обойти проблему с недопустимым вызовом метода, о которой я рассказывал ранее.
Наконец, пользовательский класс плагина реализует интерфейс IPlugin. Его основная задача — создать элемент управления плагина в процессе плагина. Как правило, это WPF UserControl, но может быть любым FrameworkElement.
Когда запрашивается загрузка плагина, класс PluginProcessProxy порождает новый дочерний процесс. Исполняемым файлом этого дочернего процесса является либо PluginProcess.exe, либо PluginProcess64.exe — в зависимости от разрядности плагина (32- или 64-разрядный). Каждый процесс плагина получает уникальный GUID в командной строке, а также базовый каталог plug-in:
PluginProcess.exe
PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18
c:\plug-in\assembly.dll
Процесс плагина подготавливает сервис типа IPluginLoader для удаленного взаимодействия и генерирует событие ready, в данном случае PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18.Ready. Затем хост может использовать методы IPluginLoader для загрузки плагина.
Альтернативным решением было бы сделать так, чтобы процесс плагина вызывал хост, как только он готов к работе. Это исключило бы необходимость в событии ready, но намного затруднило бы обработку ошибок. Если операция загрузки плагина исходит из процесса плагина, информация об ошибке тоже остается в этом процессе. Если что-то пойдет не так, хост может никогда не узнать об этом. Поэтому я предпочел архитектуру с событием ready.
Другой вопрос в проектировании заключался в том, стоит ли принимать плагины, не установленные в каталоге WPF-хоста. С одной стороны, в .NET Framework загрузка сборок, не найденных в каталоге приложения, вызывает определенные трудности. А с другой стороны, я осознаю, что у плагинов могут быть свои проблемы установки и не всегда можно развернуть плагин в каталоге WPF-хоста. Более того, некоторые сложные приложения ведут себя некорректно, когда запускаются не из своих базовых каталогов.
Из этих соображений WPF-хост позволяет загружать плагины откуда угодно в локальной файловой системе. Для этого процесс плагина выполняет практически все операции в дополнительном AppDomain, чей базовый каталог приложения указывается как базовый каталог плагина. Это создает проблему загрузки сборок WPF-хоста в этот AppDomain. Ее можно было бы решить минимум четырьмя способами:
- помещать сборки WPF-хоста в Global Assembly Cache (GAC);
- использовать перенаправления сборок в файле app.config процесса плагина;
- загружать сборки WPF-хоста, используя одну из переопределенных версий LoadFrom/CreateInstanceFrom;
- использовать неуправляемый API хостинга для запуска CLR в процессе плагина с нужной конфигурацией.
У каждого из этих решений есть свои плюсы и минусы. Размещение сборок WPF-хоста в GAC требует прав доступа администратора. Хотя GAC — изящное решение, потребность в правах администратора при установке может стать серьезной головной болью в корпоративной среде, поэтому я попытался избежать этого. Перенаправления сборок также привлекательны, но тогда конфигурационные файлы будут зависеть от местонахождения WPF-хоста. Это сделает невозможной установку по xcopy. Создание неуправляемого проекта хостинга казалось большим риском в последующем сопровождении.
В итоге я выбрал подход с LoadFrom. Большой недостаток этого подхода в том, что сборки WPF-хоста в конечном счете оказываются в контексте LoadFrom (см. статью Сюзен Кук [Suzanne Cook] в блоге «Choosing a Binding Context» по ссылке bit.ly/cZmVuz). Чтобы предотвратить любые проблемы привязки, мне нужно было переопределить событие AssemblyResolve в AppDomain плагина, чтобы коду плагина было легче находить сборки WPF-хоста.
Разработка плагинов
Плагин можно реализовать как библиотеку классов (DLL) или как исполняемый файл (EXE). В варианте с DLL требуются следующие этапы.
- Создайте новый проект библиотеки классов.
- Добавьте ссылки на WPF-сборки PresentationCore, PresentationFramework, System.Xaml и WindowsBase.
- Добавьте ссылку на сборку WpfHost.Interfaces. Убедитесь, что параметр «copy local» установлен в false.
- Создайте новый пользовательский WPF-элемент управления, такой как MainUserControl.
- Создайте класс с именем Plugin, производный от IKriv.WpfHost.Interfaces.PluginBase.
- Добавьте запись для своего плагина в файл plugins.xml хоста.
- Скомпилируйте плагин и запустите хост.
Минимальный класс плагина выглядит так:
public class Plugin : PluginBase
{
public override FrameworkElement CreateControl()
{
return new MainUserControl();
}
}
В качестве альтернативы плагин можно реализовать как исполняемый файл. В этом случае этапы будут следующими.
- Создайте WPF-приложение.
- Создайте пользовательский WPF-элемент управления, например MainUserControl.
- Добавьте MainUserControl в главное окно приложения.
- Добавьте ссылку на сборку WpfHost.Interfaces. Убедитесь, что параметр «copy local» установлен в false.
- Создайте класс с именем Plugin, производный от IKriv.WpfHost.Interfaces.PluginBase.
- Добавьте запись для своего плагина в файл plugins.xml хоста.
Ваш класс плагина будет выглядеть точно так же, как и в предыдущем примере, а в XAML главного окна не должно быть ничего, кроме ссылки на MainUserControl:
<Window x:Class="MyPlugin.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyProject"
Title="My Plugin" Height="600" Width="766" >
<Grid>
<local:MainUserControl />
</Grid>
</Window>
Плагин, реализованный таким образом, может выполняться как автономное приложение или внутри хоста. Это упрощает отладочный код плагина, не относящийся к интеграции с хостом. Схема классов для такого «двойственного» плагина показана на рис. 6.
Рис. 6. Схема классов для «двойственного» плагина
Test App | Тестовое приложение |
Host | Хост |
MainWindow | MainWindow |
Plug-In | Плагин |
MainUserControl | MainUserControl |
Поскольку плагин не является независимым приложением и запускается хостом, его отладка может оказаться делом непростым.
Этот метод также позволяет быстро преобразовывать существующие приложения в плагины. Единственное, что вам понадобится сделать, — преобразовать главное окно приложения в пользовательский элемент управления. Затем создать этот элемент в классе плагина, как было продемонстрировано ранее. Плагин Solar System (Солнечная система) в сопутствующем коде — пример такого преобразования. Весь процесс преобразования занимает менее часа.
Поскольку плагин не является независимым приложением и запускается хостом, его отладка может оказаться делом непростым. Вы можете начать отладку хоста, но отладчик Visual Studio пока не умеет автоматически подключаться к дочерним процессам. Вы можете либо вручную подключить отладчик к процессу плагина, как только он будет выполняться, либо сделать так, чтобы процесс плагина прерывался в отладчике при старте, изменив строку 4 в app.config для PluginProcess следующим образом:
<add key="BreakIntoDebugger" value="True" />
Другая альтернатива — создать плагин как автономное приложение (я уже рассказывал об этом). Тогда вы сможете отлаживать большую часть плагина как автономное приложение, лишь периодически проверяя, что интеграция с WPF-хостом работает должным образом.
Если процесс плагина прерывается в отладчике при старте, вам понадобится увеличить период ожидания события ready, изменив строку 4 в файле app.config для WpfHost так:
<add key="PluginProcess.ReadyTimeoutMs" value="500000" />
Список примеров плагинов, доступных в сопутствующем коде, и описание того, что они делают, приведены в табл. 1.
Табл. 1. Примеры плагинов, доступных в сопутствующем коде
Проект плагина | Что делает |
BitnessCheck | Демонстрирует возможность выполнения плагина как 32- или 64-разрядного |
SolarSystem | Показывает преобразование старого демонстрационного WPF-приложения в плагин |
TestExceptions | Демонстрирует обработку исключений для пользовательского и рабочего потоков |
UseLogServices | Демонстрирует использование сервисов хоста и плагина |
Сервисы хоста и плагина
На практике плагинам зачастую нужно использовать сервисы, предоставляемые хостом. Я продемонстрирую этот сценарий в плагине UseLogService, который имеется в сопутствующем коде. В классе плагина может быть конструктор по умолчанию или конструктор, принимающий один параметр типа IWpfHost. В последнем случае загрузчик плагина будет передавать экземпляр WPF-хоста плагину. Интерфейс IWpfHost определен так:
public interface IWpfHost : IServiceProvider
{
void ReportFatalError(string userMessage,
string fullExceptionText);
int HostProcessId { get; }
}
Я использую в своем плагине часть с IServerProvider. IServiceProvider — это стандартный интерфейс .NET Framework, определенный в mscorlib.dll:
public interface IServiceProvider
{
object GetService(Type serviceType);
}
Я задействую его в плагине, чтобы получать сервис ILog от хоста:
class Plugin : PluginBase
{
private readonly ILog _log;
private MainUserControl _control;
public Plugin(IWpfHost host)
{
_log = host.GetService<ILog>();
}
public override FrameworkElement CreateControl()
{
return new MainUserControl { Log = _log };
}
}
Затем элемент управления может использовать сервис хоста ILog для записи в файл журнала хоста.
Кроме того, хост может использовать сервисы, предоставляемые плагинами. Я определил один такой сервис — IUnsavedData, который очень полезен в реальной жизни. При реализации этого интерфейса в плагине можно определить список несохраненных рабочих элементов. Если плагин или все хост-приложение закрывается, хост запросит пользователя, хочет ли он отказаться от несохраненных данных (рис. 7).
Рис. 7. Использование сервиса IUnsavedData
Интерфейс IUnsavedData определен следующим образом:
public interface IUnsavedData
{
string[] GetNamesOfUnsavedItems();
}
Автору плагина не требуется явным образом реализовать интерфейс IServiceProvider. Достаточно реализовать в плагине интерфейс IUnsavedData. Метод PluginBase.GetService позаботится о передаче этого интерфейса хосту. Мой проект UseLogService в сопутствующем коде предоставляет пример реализации IUnsavedData с релевантным кодом, который показан ниже:
class Plugin : PluginBase, IUnsavedData
{
private MainUserControl _control;
public string[] GetNamesOfUnsavedItems()
{
if (_control == null) return null;
return _control.GetNamesOfUnsavedItems();
}
}
Протоколирование и обработка ошибок
Процессы WPF-хоста и плагинов создают журналы в каталоге %TMP%\WpfHost. WPF-хост пишет в WpfHost.log, а каждый процесс плагина — в PluginProcess.Guid.log («Guid» не является частью литерального имени, а раскрывается в реальное значение Guid). Сервис протоколирования создан мной самостоятельно. Я избегал пользоваться популярными сервисами протоколирования (например, log4net или NLog), чтобы сделать пример самодостаточным.
Процесс плагина также записывает результаты в свое консольное окно, которое вы можете отображать, изменив строку 3 в файле app.config для WpfHost на:
<add key="PluginProcess.ShowConsole" value="True" />
Я приложил максимум усилий для информирования хоста обо всех ошибках и для их корректной обработки. Хост отслеживает процессы плагинов и закрывает окно плагина, если его процесс уничтожается. Аналогично процесс плагина отслеживает свой хост и закрывается, если хост погибает. Все ошибки протоколируются, поэтому изучение файлов журналов сильно помогает при анализе проблем.
Важно помнить: все, что передается между хостом и плагинами, должно быть либо [Serializable], либо типа, производного от MarshalByRefObject. Иначе .NET-механизм удаленного взаимодействия не сможет осуществить маршалинг объекта между двумя сторонами. Типы и интерфейсы должны быть известны обеим сторонам, поэтому для маршалинга, как правило, безопасны только встроенные типы и типы из сборок WpfHost.Interfaces и PluginHosting.
В целом, вы должны рассматривать плагины как сторонний код, неподконтрольный авторам хоста.
Управление версиями
WpfHost.exe, PluginProcess.exe и PluginHosting.dll жестко связаны и должны выпускаться одновременно. К счастью, код плагина не зависит от какой-либо из этих трех сборок, и поэтому их можно модифицировать почти как угодно. Например, вы можете легко изменить механизм синхронизации или имя события ready, не влияя на плагины.
Версии компонента WpfHost.Interfaces.dll следует контролировать очень тщательно. На него нужно ссылаться, но включать в код плагина не следует (CopyLocal=false), чтобы двоичный файл этой сборки всегда поступал только от хоста. Я не присваивал этой сборке строгое имя, потому что намеренно не хотел, чтобы можно было параллельно выполнять разные ее версии. Во всей системе должна присутствовать только одна версия WpfHost.Interfaces.dll.
В целом, вы должны рассматривать плагины как сторонний код, неподконтрольный авторам хоста. Модификация или даже перекомпиляция всех плагинов сразу может быть затруднительна или вообще невозможна. Поэтому новые версии этой интерфейсной сборки должны быть совместимы на уровне двоичного кода с предыдущими версиями, а количество разрушающих изменений нужно сводить к абсолютному минимуму.
Добавление новых типов и интерфейсов в эту сборку, в целом, безопасно. Любые другие изменения, в том числе добавление новых методов к интерфейсам или новых значений в перечисления, потенциально способны разрушить совместимость на уровне двоичного кода, и поэтому их следует избегать.
Хотя сборки хостинга не имеют строгих имен, важно увеличивать номера версий после каждого изменения, даже самого малого, чтобы никакие две сборки с одинаковым номером версии не содержали разный код.
Хорошая отправная точка
Моя эталонная архитектура, представленная здесь, не является инфраструктурой производственного качества для интеграции плагинов и хоста, но весьма близка к тому и может послужить в качестве хорошей отправной точки для вашего приложения.
Эта архитектура берет на себя стереотипные, но трудные вещи, такие как жизненный цикл процесса плагина, маршалинг элементов управления плагинов между процессами, механизм обмена данными, распознавание сервисов между хостом и плагинами и многое другое. Большая часть проектировочных решений и обходных вариантов не являются произвольными. Они базируются на реальном опыте в создании составных приложений для WPF.
Скорее всего вы захотите модифицировать внешний вид хоста, заменить механизм протоколирования на стандартный, используемый в вашей организации, добавить новые сервисы и, возможно, изменить способ обнаружения плагинов. Вероятны и многие другие модификации и усовершенствования.
Даже если вы не создаете составные приложения для WPF, то, возможно, все равно с интересом изучите эту архитектуру в качестве демонстрации того, насколько мощной и гибкой может быть .NET Framework и как комбинировать привычные компоненты неожиданным и эффективным способом.