Managed Extensibility Framework (MEF) как полигон для экспериментов
WPF, MVVM, Silverlight | создано: 04.12.2010 | опубликовано: 04.12.2010 | обновлено: 13.01.2024 | просмотров: 7981
MEF - это аббревиатура от Managed Extensibility Framework, что дословно можно перевести как библиотека управляемых расширений. Знаете ли Вы что такое MEF? Использовали ли вы его в своих проектах? Понимаете ли вы, как этот самый MEF работает? С удовольствием поделюсь опытом разработки для Silverlight с использованием MEF.
Итак, о чём эта статья? Просто решил попробывать "поиграть" с MEF, а раз уж получилось кое-что, решил выложить в блог.
Вопросы к рассмотрению:
Вопрос 1: MEF и INotifyPropertyChanged: как уведомить экспортированный объект об изменениях?
Вопрос 2: Уведомление об изменении свойства, импортированного через MEF, как коллекцию (ImportMany).
Вопрос 3: Загрузка XAP-файлов по требованию через MEF.
Вопрос 4: Модальное окно в MVVM-паттерне.
Постановка задачи (Хотелки).
Хотелка номер 1: Я хочу сделать так, чтобы один модуль (Shell) мог "находить" модули при помощи MEF на этапе компиляции, а также уметь "подгружать" сторонние модули по требованию (например, при нажатии кнопки "Загрузить").
Хотелка номер 2: Также, мне бы хотелось, чтобы при запуске редактирования моего объекта в окне одного редактора изменения сразу же передавались в другое окно.
Хотелка номер 3: Я хочу, чтобы в моем приложении можно было запустить несколько вариантов редактора моей сущности (моего объекта MyObject). Пусть пока их будет два, причем один редактор будет встроен в главный модуль приложения и доступен сразу при старте приложения, а второй пусть вообще будет в отдельном XAP-файле и будет доступен только при загрузки его в проект по требованию (по-заграничному звучит как OnDemand ).
Хотелка номер 4: Я хочу чтобы редакторы открывались в модальном окне.
Не плохо у меня с хотелками, не правда ли? :)
Сборки используемые в реализации.
Сразу оговорюсь, что этой статье и в частности в этом решении я использую свои собственные библотеки (сборки классов). Одна из них Calabonga.Silverlight.Framework.dll. В нее добалены интерфейсы для реализации модального окна (ModalDialog) с подменяемым контекстом. (Пример использования опять же есть в проекте.) Таким образом, модльное окно можно использовать в MVVM паттерне и подставлять в это окно любой другой View-класс.
Вторая сборка, которую я написал чтобы не муздыкаться постоянно с подгрузками модулей, получила название XapService. Это реализация DeploymentCatalogService. Сборка находится в проекте, который можно скачать и который является примером по использованию.
Реализация "хотелок".
Как Вы понимаете, решение готово в виде решения (solution), в котором несколько проектов. На этот раз потребовалось 5 проектов в одном солюшине. (SLN):
рис. 1. "Структура решения и проектов, которые в него входят".
MefTest - главный проект, а в контексте данной статьи, это и есть оболочка для модулей (Shell). Этот проект содержит один из модулей (давайте будем его называть "штатное расписание"), о котором говорилось в хотелке №1. А также один редакторов (см. хотелку №3). Вот так выглядит окно локально модуля:
рис. 2. "Вид внутреннего модуля"
MefTest.Web - это ASP.NET приложение, в котором хостится (запускается) мой эксперимент.
FormView - внешний модуль (давайте его называть, например, "бухгалетрия"). Это один из модулей, о котором упоминается в хотелке №1.
рис. 3. "Вид внешнего модуля"
ContractLibrary - библиотека контрактов и интерфейсов. Именной в этой сборке лежит мой экспериментальный класс MyObject. А для того чтобы все модули знали о наличии моего класса эта сборка будет использоваться во всех модулях.
AdvancedEditor - одна из реализаций редактора, о котором говорилось в хотелке №3.
Эксперименты буду ставить с простым классом MyObject, который реализует INotifyPropertyChanged чтобы работала хотелка №2. Вот как он выглядит в коде:
public class MyObject : INotifyPropertyChanged { private string cityType; public string Name { get { return name; } set { name = value; OnPropertyChanged("Name"); } } private string name; public string CityType { get { return cityType; } set { cityType = value; OnPropertyChanged("CityType"); } } #region INotifyPropertyChanged Implementation public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #endregion }
В MainPage (главная оболочка Shell) реализуем загрузчик xap-файлов IDeploymentCatalogService который помогает загружать xap-файлы по требованию:
public partial class MainPage : UserControl, IPartImportsSatisfiedNotification { [Import] public IDeploymentCatalogService CatalogService { get; set; } public MainPage() { InitializeComponent(); CompositionInitializer.SatisfyImports(this); } [ImportMany(typeof(UserControl), AllowRecomposition=true)] public UserControl[] Views { get; set; } public void OnImportsSatisfied() { LayoutRoot.Children.Clear(); foreach (UserControl item in Views) { LayoutRoot.Children.Add(item); } } }
Окно модального диалога (ModalDialog).
Чтобы выводить на экран разные редакторы ничего умнее не придумал, как выводить эти модули в модальном окне (уж простите, не силён я в дизайне UI интерфейса). Суть структуры модального диалога состоит в том, что модальной является сам "каркас". Внутренний контекст этого каркаса может быть любым. Так вот, если модуль "штатное расписание" (FormLocal - локальный внутренний модуль в главном проекте, который мы назвали Shell) использует code behind, то вот модуль "бухгалтерия" (см. FormView) уже сделан по правилам MVVM (model - view - viewmodel) паттерна. Вдаваться в подробности по реализации модального окна, Вы сможете посмотреть всё в самом проекте, который можно скачать (ссылка в конце статьи) .
Вкратце про модальность окна диалога можно сказать следующее - она работает! Весь функционал "зашит" в мою библиотеку из которой наружу торчат Export's. (Если будет интересно, то я позже расскажу как работает ModalDialog в другой статье, потому как эта статья не о диалогах). На примере это выглядит так:
В классе FormExternalViewModel есть свойство ModalDialog, которое является интерфейсом для внешнего вида окна.
[Import] public IModalDialog ModalDialog { get; set; }
Чтобы не изобретать велосипед, решил взять ChildWindow контрол (назвав его ExtendedChildWindow) из библиотеки Silverlight контролов и наделить его нужными возможностями, в частности релизовать IModalDialog из своей библиотеки.
[Export(typeof(IModalDialog))] public class ExtendedChildWindow : ChildWindow, IModalDialog { public void ShowDialog() { this.Width = 450; this.Height = 300; this.Show(); } }
Есть также в классе FormExternalViewModel и свойство:
[ImportMany(AllowRecomposition = true)] public IModalView[] Editors { get; set; }
Интерфейс IМodalView - это как раз и есть реализация контекста для модального окна. То есть, любой визуальный контрол (View или еще какая-нибудь хрень) рализующий данный интерфейс может быть отображен в модальном окне. Так получилось, что у меня в этом проекте их несколько, а точнее два - внутренний редактор и внешний редактор. Ну, и на последок, про, так называемый, "открыватель" модальных окон:
[Import] public IModalDialogWorker ModalDialogWorker { get; set; }
Это свойство в классе FormExternalViewModel импортирует из библиотеки Calabonga.Silverlight.Framework.dll "запускатель" модального диалога, который вызывается из команды OpenDialogCommand:
this.ModalDialogWorker.ShowDialog<MyObject>(this.ModalDialog, view, o, dialog => { if (this.ModalDialog.DialogResult.HasValue && this.ModalDialog.DialogResult.Value) { } });
Вот таким образом в библиотеке "нарисован" класс ModalDialogWorker, в сборке Calabonga.Silverlight.Framework:
namespace Calabonga.Silverlight.Framework { [Export(typeof(IModalDialogWorker))] public class ModalDialogWorker : IModalDialogWorker { public void ShowDialog<T>(IModalDialog modalDialog, IModalView modalView, T dataContext, Action<T> onClosed) { if (modalDialog == null) throw new ArgumentNullException("modalDialog", "Не может быть null"); if (modalView == null) throw new ArgumentNullException("modalView", "Не может быть null"); EventHandler onDialogClosedHandler = null; EventHandler<ModalViewEventArgs> onViewClosedHandler = null; if (onClosed != null) { onDialogClosedHandler = (s, a) => { modalDialog.Closed -= onDialogClosedHandler; onClosed(dataContext); }; onViewClosedHandler = (s, a) => { modalDialog.Closed -= onDialogClosedHandler; modalView.Closed -= onViewClosedHandler; modalDialog.DialogResult = a.DialogResult; onClosed(dataContext); }; modalDialog.Closed += onDialogClosedHandler; modalView.Closed += onViewClosedHandler; } modalDialog.Content = modalView; modalView.DataContext = dataContext; modalDialog.ShowDialog(); } } }
Короче хватит про модальные окна и диалоги, потому как "песня" не об этом.
Модули и склейка приложения или MEF в действии.
Чтобы при старте приложения главный проект (shell) "нашел" все доступные модули (а их может быть неограниченное количество), буду использовать Managed Extensibility Framework (MEF). Для того чтобы MEF обозначил своё присутствие в проекте, необходимо добавить сборки во все проекты солюшена:
using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting;
Но помните, что в конечной итоге, эти библиотеки должны быть в единственном экземпляре. Для этого необходимо просто отключить копирование сборов в XAP-файл.Теперь далее, для того чтобы модули могли заявить о себе, каждый из них должен быть помечен атрибутом экспорта (ExportAttribute), например, так помечен FormExternal из FormView:
[Export(typeof(UserControl))] public partial class FormExternal : UserControl { public FormExternal() { InitializeComponent(); } }
Таким образом, зная о том, что есть экспорты, можно предположить, что потребуются импорты. (вот, блин, завернул...). В главной форме получим (импортируем) модули. Пусть это свойство называется View:
[ImportMany(typeof(UserControl), AllowRecomposition=true)] public UserControl[] Views { get; set; }
Ключивым моментом данного кода стоит отметить:
AllowRecomposition=true
Сей невзрачный параметр атрибута говорит о том, что данные могут обновиться в процессе работы и следовательно нужно сделать рекомпозицию главного MEF-каталога. В моем приложении так и будет, в смысле потребуется обновление. Сначала покажется только один модуль, а при нажатии на кнопку загрузится второй. Для того, чтобы запустить процесс наполнения импортов, в конструкторе главного окна выполним инициализацию:
public MainPage() { InitializeComponent(); CompositionInitializer.SatisfyImports(this); }
Для того чтобы отследить изменения главного MEF-каталога реализуем интерфейс IPartImportsSatisfiedNotification, в имплементации которого полученные модули добавим в проект:
public void OnImportsSatisfied() { LayoutRoot.Children.Clear(); foreach (UserControl item in Views) { LayoutRoot.Children.Add(item); } }
Так я получаю доступные на момент компиляции модули и выводу их на на главную форму. Посмотрите как это выглядит:
Хочу обрать Ваше внимание на то, что на данный момент загружен один "редактор", поэтому CheckBox выбора редактора отключен. Определяется возможность вызова в следующем коде:
private IModalView[] editors; [ImportMany(AllowRecomposition = true)] public IModalView[] Editors { get { return editors; } set { editors = value; checkEditor.IsEnabled = (this.Editors != null) && (this.Editors.Count() > 1); OnPropertyChanged("Editors"); } }
Код для Code-Bihind и код для ViewModel (MVVM) немного отличается (можете в проекте посмотреть), но принцип определения доступности контрола идентичен. Теперь пришло время вызвать локальный редактор для этого модуля в модальном окне, нажимаем кнопку "Редактор" и ... вуаля!!!
Загрузим еще один модуль. Более того, при нажатии на кнопку "Загрузить внешний модуль" еще хочу загрузить не только сам модуль, но дополнительный редактор, так называемый "внешний" (см. хотелка №3 и AdvancedEditor).
private void Button_Click(object sender, RoutedEventArgs e) { CatalogService.AddXap("FormView.xap"); CatalogService.AddXap("AdvancedEditor.xap"); (sender as Button).IsEnabled = false; }
Хочу отметить только одно, объект, который передается из модуля в модуль (да и из редактора в редактор) принимает моментально все изменения полей, что достигается реализацией INotifyPropertyChanged.
Примечание: задача реализация паттерна Memento в данной статье не стоит.
После успешной загрузки всех указанных модулей мы видим, что появился еще один модуль, а также включился CheckBox выбора редактора в обоих модулях. Что означает ни что иное, как наличие нескольких редакторов (в моем случаи их два) для редактирования объекта.
Попробую-ка выбрать для редактирования объекта внешний модуль и использовать при этом внешней редактор. Работает! А теперь наоборот?! Тоже работает!
Получилось, что любой модуль может использовать любой редактор да еще и в модальном окне. Кажется все хотелки удовлетворены. Ну, а раз всё заработало и хотелки удовлетворены, осталось только дать попрощаться.