Создание приложения с полной поддержкой плагинов
Опубликовано 28 декабря 2009 14:43 | Coding4Fun |
В этой статье вы узнаете, как создать приложение с полной поддержкой плагинов (подключаемых модулей).
Автор: Ариан Т. Кулп (Arian T. Kulp)
Сайт проекта: https://utilrunner.codeplex.com/
Исходный код: загрузить
Попробуйте прямо сейчас: запустить приложение
Сложность: средняя
Необходимое время: 3 часа
Затраты: бесплатно!
ПО: Visual C# 2008 Express Edition или выше
Введение
С момента создания моей первой версии приложения Utility Runner прошло уже несколько месяцев. Utility Runner позволяет выполнять системные утилиты с минимальными издержками: вместо кучи индикаторов на панели задач, бесчисленных EXE-файлов (и вызванных ими неудобств, связанных с памятью и временем запуска) это приложение управляет множеством утилит из одного места.
Чтобы открыть это решение, вам понадобится, как минимум, Visual Studio 2008 Express Edition (Visual C# или Visual Basic). Если у вас еще нет этой среды разработки, вам стоит позаботиться о ее получении!
Работас MEF
Функциональность приложения мало изменилась по сравнению с предыдущей версией, если не считать некоторой ее переработки в связи с выпуском новой версии MEF. Начнем с того, что атрибут, с помощью которого помечается экспортируемая функция, больше не является запечатанным (sealed), поэтому теперь вы можете создать подкласс атрибута, охватывающий имя экспортируемой функции и любые другие включенные в нее метаданные. Такой новый атрибут пользователи смогут применять просто для более строгой типизации. В данном случае я создал класс WpfServiceMetadata. Подробности о нем написано дальше.
Управление надстройками
Большая проблема для меня заключалась в том, чтобы реализовать диспетчер для поддержки множества надстроек-утилит — как в Firefox. MEF позволяет легко помечать, какие классы являются надстройками и как они должны быть связаны с вашим кодом в целом, но мне хотелось большего. Поразмыслив, я счел, что мои цели таковы:
- возможность загрузки надстройки через UI;
- возможность отключения/включения надстроек;
- возможность удаления надстройки.
Достичь этих целей можно самыми разными способами. Мой способ — отнюдь не идеален, но работает вполне нормально.
Загрузканадстроек
Загрузка надстроек в основном обрабатывается самой инфраструктурой MEF, тем не менее, кое-какая ручная работа все же потребуется. Я решил, что вместо проверки папки на наличие нужных DLL, как в первой версии, лучше использовать простой формат упаковки и помещать надстройку и связанные с ней файлы в один контейнер.
В качестве контейнера я задействовал Zip-файл с расширением, переименованным в .util. Этот файл содержит, как минимум, DLL самой надстройки, но может включать связанные библиотеки, изображения, звуки и другие ресурсы. Одно время я создавал файл манифеста, предоставляющего информацию о надстройке, которая не требовала загрузки класса, но потом предпочел не усложнять простые вещи.
Перед загрузкой DLL с надстройками класс AddinManagerсканирует папку надстроек на наличие файлов с расширениями .util и распаковывает их в одноименные папки. Каждая из таких папок добавляется в список папок, которые просматриваются MEF. После этого исходный файл удаляется.
Добавление надстройки
В приложении, поддерживающем надстройки, нужен какой-то способ, с помощью которого пользователи могли бы добавлять новые надстройки. Самый простой способ — перетаскивание новых файлов .util в папку Addins, но на самом деле он не слишком понятен на интуитивном уровне. Мне хотелось, чтобы пользователь мог установить надстройку одним щелчком кнопки.
Если пользователь щелкает кнопку AddNewAddinFromFile, он фактически открывает диалоговое окно для поиска файла. Выбранный файл копируется в папку Addins, и приложение уведомляет пользователя о том, что новая надстройка будет загружена при следующем запуске приложения. Было бы здорово загружать надстройку немедленно, и MEF поддерживает это, но ради простоты я решил не применять динамическое управление надстройками.
Вместо прохождения всего диалога при загрузке надстройки было бы неплохо ввести поддержку двойного щелчка. Для этого нужно связать расширение .util с соответствующим исполняемым файлом. При запуске исполняемого файла с аргументом, где указывается файл .util, и при условии корректности формата этот файл .util будет автоматически копироваться в папку Addins. Если экземпляр исполняемого файла уже работает, на экране появляется существующий экземпляр, а новый — просто завершается.
Важно отметить, что у обычных пользователей нет прав на запись в папку ProgramFiles. По этой причине надстройки нужно хранить в папке локального профиля пользователя. Надстройки системного уровня можно помещать в папку ProgramData. Надстройки можно было бы хранить в папке перемещаемого профиля пользователя, но я пока не продумал этот вариант до конца. (Например, если пользователь регистрируется на разных компьютерах, на каком-то из них надстройка может просто не запуститься из-за неподходящей аппаратной конфигурации.)
Отключение надстройки
Для отключения надстройки достаточно переименовать папку надстройки. Однако это невозможно в период выполнения, так как все загруженные DLL блокируются в файловой системе. В качестве альтернативы я создал файл (нулевой длины) с тем же именем и добавил к нему расширение .disable. Проверка показала, что этот вариант вполне работоспособен.
Включение и удаление надстроек обрабатываются аналогично, при этом используются файлы нулевой длины с расширениями .enable и .delete соответственно.
Созданиенадстроек
Чтобы создать надстройку, вам нужно, во-первых, реализовать интерфейс IWpfService, а во-вторых, добавить атрибут WpfServiceMetadataс именем вашей надстройки. После этого не забудьте реализовать все методы интерфейса. Метод Startвызывается при инициализации (старайтесь не проводить много времени в конструкторе), а Stop— в конце, при очистке. Обновляя Status, обязательно генерируйте событие StatusChanged.
[WpfServiceMetadata("SampleAddin")]
public class SampleAddinImpl : IWpfService
{
}
Наконец, вам понадобится скопировать DLL своей надстройки в папку с именем этой надстройки и расширением .util. (Да, именно так: расширение присваивается имени папки.) Я делаю это в Visual Studio с помощью события, генерируемого после сборки проекта. В режиме отладки приложение ищет папку Addins в текущем каталоге. При нормальном запуске (например, после установки) просматривается локальный профиль пользователя.
Когда вы будете готовы к распространению надстройки, заархивируйте DLL и любые сопутствующие файлы в ZIP-файл, а затем переименуйте расширение .zip в .util. Этот файл можно загружать со страницы Addins в параметрах приложения или вручную перемещать его в папку MefUtilRuner \ Addins, создаваемую в папке вашего локального профиля (c :\ users \{ USERNAME }\ AppData):
TimedQueue
Информационные окна, подсказки в виде реплик, строки состояния — все это отличные способы отображать сообщения пользователю, но они позволяют показывать лишь одну строку единовременно. Если пользователь не успеет прочесть первое сообщение, вывод второго сообщения приведет к тому, что первое из них просто исчезнет. Существуют неплохие диспетчеры сообщений, облегчающие управление множеством оповещений, но я решил остановиться на встроенном провайдере подсказок в виде реплик и контролировать частоту отправки изменений. В целом это провайдер буферизуемых подсказок, который всего лишь показывает сообщения каждые x секунд независимо от того, сколько сообщений передает приложение. Для его использования добавьте элементы в набор. Всякий раз, когда становится доступным какой-либо элемент, генерируется событие. Если срок существования элемента превысит пороговое значение, пользователи набора никогда не увидят его.
// Повторяем, пока есть элементы (возможна готовность
// сразу нескольких элементов)
while (item != null)
{
// Если это старый элемент, событие не генерируется.
// Это возможно, если
// процесс входит в цикл и копирует большое количество
// элементов за очень короткий
// промежуток времени.
if (DateTime.Now.Subtract(item.TimeStamp).TotalMilliseconds
< _maxTTL) { RaiseEvent(item.Item); Thread.Sleep(_interval); } item = default(ItemWrapper<T>); lock (_lock) { if( _items.Count > 0 ) item = _items.Dequeue(); } }
Основное окно создает экземпляр класса TimedQueue. Всякий раз, когда надстройке нужно вывести сообщение, оно добавляется в набор. Когда срабатывает событие ItemAvailableEvent, оно направляется UI-потоку для отображения сообщения. Диспетчеризация предотвращает проблемы прямой передачи сообщений между фоновым потоком TimedQueueи UI-потоком.
void statuses_ItemAvailableEvent(object sender,
ItemAvailableEventArgs<StatusMessage> e)
{
Dispatcher.BeginInvoke((ThreadStart)delegate() {
StatusUpdatedHandler(e.Item); }
, DispatcherPriority.Background);
}
Хотя этот экземпляр используется для сообщений состояния, вы можете применять TimedQueueи для других целей, когда есть необходимость в удалении устаревших сообщений. Один из примеров — «регулировка» пользовательского ввода в игровой программе, где вряд ли нужно разрешать постоянный огонь из оружия или слишком частые прыжки.
Следующиешаги
Есть и другие вещи, которые было бы неплохо включить в это приложение. Например, позволять утилитам расширять контекстное меню для включения/отключения чего-либо или, по крайней мере, прямого перехода к их страницам настройки.
Кроме того, для развертывания утилит была бы полезна технология ClickOnce. Она удобна и понятна пользователю, но у нее есть своя цена: ClickOnce сильно ограничивает то, как приложения могут взаимодействовать с системой. Приложения всегда устанавливаются индивидуально для конкретного пользователя и помещаются в «секретные» папки. Разработчики не могут считывать или записывать на жесткий диск — только в защищенное хранилище (аналогично iPhone, полагаю). Не уверен насчет других ограничений, но и это может создать трудности.
Также было бы замечательно иметь возможность добавлять надстройки в период выполнения. Это сравнительно простая функция, но у меня пока не дошли руки до нее. При этом можно было бы ожидать и реализации включения/отключения/удаления в период выполнения. Увы, истинное отключение и удаление на самом деле невозможны. Конечно, я мог бы вызывать Stop для конкретной утилиты и удалять ее из UI (и при следующем запуске не вызвать Startдля нее), но на практике эта утилита по-прежнему выполнялась бы до перезапуска приложения. Принудительно удалить ее нельзя, если не использовать раздельные домены приложения, а возиться с ними мне совсем не хочется.
Наконец, мне очень хотелось бы создать хранилище наподобие «магазина приложений». В нем можно было бы публиковать разнообразные утилиты вроде гаджетов для боковой панели или Windows Live Writer Plugins, чтобы пользователи могли их просматривать, читать информацию и устанавливать одним щелчком.
Заключение
На данный момент в приложении хватает острых углов, но процесс шлифовки потихоньку движется. Его исходный код полностью доступен, и я готов выдать права на его модификацию любому, кто заинтересован в ускорении его развития. Просто черкните мне пару строк!
Обавторе
Arian Kulp — разработчик ПО, живет в Западном Орегоне. Создает примеры, демо-ролики, лабораторные занятия и пишет статьи, выступает на различных мероприятиях, посвященных вопросам программирования, а также с удовольствием проводит свободное время со своей семьей.