10 November 2010

Managed Extensibility Framework (MEF) как полигон для экспериментов

Silverlight
Sandbox
Вопросы к рассмотрению

Вопрос 1: MEF и INotifyPropertyChanged: как уведомить экспортированный объект об изменениях?
Вопрос 2: Уведомление об изменении свойства, импортированного через MEF, как коллекцию (ImportMany).
Вопрос 3: Загрузка XAP-файлов по требованию через MEF.
Вопрос 4: Модальное окно в MVVM-паттерне.

Постановка задачи (Хотелки)

Хотелка номер 1: Я хочу сделать так, чтобы один модуль (Shell) мог «находить» модули при помощи MEF на этапе компиляции, а также уметь «подгружать» сторонние модули по требованию (например, при нажатии кнопки «Загрузить»).

Хотелка номер 2: Также, мне бы хотелось, чтобы при запуске редактирования моего объекта в окне одного редактора изменения сразу же передавались в другое окно.

Хотелка номер 3: Я хочу, чтобы в моем приложении можно было запустить несколько вариантов редактора моей сущности (моего объекта MyObject). Пусть пока их будет два, причем один редактор будет встроен в главный модуль приложения и доступен сразу при старте приложения, а второй пусть вообще будет в отдельном XAP-файле и будет доступен только при загрузки его в проект по требованию (по-заграничному звучит как OnDemand ).

Хотелка номер 4: Я хочу чтобы редакторы открывались в модальном окне.

Сборки используемые в реализации

Сразу оговорюсь, что этой статье и в частности в этом решении я использую свои собственные библотеки (сборки классов). Одна из них Calabonga.Silverlight.Framework.dll, которая, кстати, обновила свою версию до номера 1.0.5. В нее добалены интерфейсы для реализации модального окна (ModalDialog) с подменяемым контекстом. (Пример использования опять же есть в проекте.) Таким образом, модльное окно можно использовать в MVVM паттерне и подставлять в это окно любой другой View-класс.

Вторая сборка, которую я написал чтобы не муздыкаться постоянно с подгрузками модулей, получила название XapService. Это реализация DeploymentCatalogService. Сборка находится в проекте, который можно скачать и который является примером по использованию.

Реализация «хотелок»

Как Вы понимаете, решение готово в виде решения (solution), в котором несколько проектов. На этот раз потребовалось 6 проектов в одном солюшине. (SLN):

image

рис. 1. «Структура решения и проектов, которые в него входят»

MefTest — главный проект, а в контексте данной статьи, это и есть оболочка для модулей (Shell). Этот проект содержит один из модулей (давайте будем его называть «штатное расписание»), о котором говорилось в хотелке №1. А также один редакторов (см. хотелку №3). Вот так выглядит окно локально модуля:

image

рис. 2. «Вид внутреннего модуля»

MefTest.Web — это ASP.NET приложение, в котором хостится (запускается) мой эксперимент.

FormView — внешний модуль (давайте его называть, например, «бухгалетрия»). Это один из модулей, о котором упоминается в хотелке №1.

image

рис. 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(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(IModalDialog modalDialog, IModalView modalView, T dataContext, Action onClosed)
{
if (modalDialog == null)
throw new ArgumentNullException("modalDialog", "Не может быть null");
if (modalView == null)
throw new ArgumentNullException("modalView", "Не может быть null");

EventHandler onDialogClosedHandler = null;
EventHandler 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);
}
}


Так я получаю доступные на момент компиляции модули и выводу их на на главную форму. Посмотрите как это выглядит:

image

Хочу обрать Ваше внимание на то, что на данный момент загружен один "редактор", поэтому 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) немного отличается (можете в проекте посмотреть), но принцип определения доступности контрола идентичен. Теперь пришло время вызвать локальный редактор для этого модуля в модальном окне, нажимаем кнопку "Редактор" и ... вуаля!!!

image

Загрузим еще один модуль. Более того, при нажатии на кнопку "Загрузить внешний модуль" еще хочу загрузить не только сам модуль, но дополнительный редактор, так называемый "внешний" (см. хотелка №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 выбора редактора в обоих модулях. Что означает ни что иное, как наличие нескольких редакторов (в моем случаи их два) для редактирования объекта.

image

Попробую-ка выбрать для редактирования объекта внешний модуль и использовать при этом внешней редактор. Работает! А теперь наоборот?! Тоже работает! image

Скачать проект для Visual Studio 2010
Tags:mvvmmanaged extensibility frameworksilverlight 4
Hubs: Silverlight
-1
2k 4
Leave a comment