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

WPF и Silverlight | создано: 04.12.2010 | опубликовано: 04.12.2010 | обновлено: 13.01.2024 | просмотров: 7562

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 выбора редактора в обоих модулях. Что означает ни что иное, как наличие нескольких редакторов (в моем случаи их два) для редактирования объекта.

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

Получилось, что любой модуль может использовать любой редактор да еще и в модальном окне. Кажется все хотелки удовлетворены. Ну, а раз всё заработало и хотелки удовлетворены, осталось только дать попрощаться.