Использование Managed Extensibility Framework (MEF) для разработки модульных Silverlight приложений

суббота, 23 октября 2010, Александр Краковецкий

Библиотека MEF появилась относительно недавно, но быстро завоевала популярность у .Net разработчиков за простоту использования и эффективность. Она позволяет строить модульные приложения с минимальным уровнем связности частей (parts) приложения. Эта библиотека включает в себя не только Dependency Injection контейнер, но большой объём инфраструктуры: множество механизмов поиска элементов композиции в сборках, удалённых XAP файлах, механизм пометки элементов композиции с помощью .Net атрибутов и т.д.

Существует версия MEF для Silverlight, которая имеет отличия от настольной версии. Об особенностях использовании MEF для Silverlight приложений мы и поговорим в этой статье.

Отличия от настольной версии

В MEF for Siverlight добавлены несколько специфичных классов.

  • CompositionInitializer
  • DeploymentCatalog
  • CompositionHost
  • ExportFactory

CompositionInitializer

В Silverlight разработчиками MEF предлагается использовать класс CompositionInitializer (библиотека System.ComponentModel.CompositionInitialization.dll), который позволяет осуществить композицию для конкретного объекта, инициализировав все импорты этого объекта и остальные зависимые сущности. Эта функциональность особенно важна для Silverlight приложений, где децентрализация составных элементов приложения проявляется особенно ярко.

При первом вызове метода SatisfyImports() этого класса происходит создание глобального контейнера, который будет использоваться во всех дальнейших вызовах SatisfyImports(). SatisfyImports производит композицию всех объектов, которые встречаются в текущей сборке и всех зависимых сборках (т.е. в рамках всего XAP файла). Объекты, инстанцированные при композиции, будут находиться в контейнере до момента уничтожения последнего, т.е. до момента окончания работы программы.

Существует несколько особенностей использования этого класса:

  1. Классы, которые вызывают метод SatisfyImports() не могут иметь атрибут [Export];
  2. Объекты инстанцируются только один раз и хранятся в контейнере;
  3. По умолчанию композиции подвергается только текущий XAP файл, что легко исправить.

Пример:

public partial class Shell : UserControl
{
 public MainPage()
 {
  ComposeContainer()
 }

 private void ComposeContainer()
 {
  CompositionInitializer.SatisfyImports(this);
 }
}

DeploymentCatalog

Этот класс предназначен для динамической загрузки XAP файлов, что позволяет ещё больше децентрализовать разработку, уменьшить размер главного XAP файла, увеличив скорость загрузки приложения, и пр. XAP файлы загружаются в ассинхронном режиме, и разработчик может подписаться на уведомление об окончании загрузки файла и обработать ошибки, если они существуют.

Этот класс является составной частью идеи рекомпозиции (повторное составление композиции из существующих и добавленных частей), осуществляя последнюю, если есть части, которые допускают рекомпозицию.

Особенности использования DeploymentCatalog:

  1. Кэш браузера используется в случае, когда приложение находится в состоянии offline
  2. Обязательно должен использоваться класс CompositionHost (см. ниже)
  3. Если в разных XAP файлах присутствуют одинаковые сборки, то DeploymentCatalog будет пытаться добавить их все в каталог, что может привести к возникновению исключения (exception), если рекомпозиция не разрешена. Следует выставлять CopyLocal=False сборкам-дубликатам всего приложения или воспользоваться моим расширением для VS2010 XapsMinifier
  4. В каталог загружаются из XAP только те сборки, которые указаны в файле-манифесте
  5. В режиме Out of Browser загружаемые сборки не кэшируются в файловой системе

Пример:

private void CancelLoading()
{
 catalog.CancelAsync();
}

private void LoadXapFile(string xapPath)
{
 DeploymentCatalog catalog = new DeploymentCatalog(xapPath);
 catalog.DownloadCompleted += new EventHandler(DownloadCompleted);
 catalog.DownloadProgressChanged += new EventHandler(ProgressChanged);
 catalog.DownloadAsync();
 _aggregateCatalog.Catalogs.Add(catalog);
}

void DownloadCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
 if (e.Error != null)
  throw e.Error;
}

void catalog_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
 int progress = e.ProgressPercentage;
 long received = e.BytesReceived;
 long total = e.TotalBytesToReceive;
}

CompositionHost

Этот класс является связующим звеном между каталогами, которые содержат информацию об экспорте/импорте частей, и контейнером, который содержит композицию частей. Этот класс позволяет переопределить первоначальную конфигурацию приложения (загрузка частей только из текущего XAP файла). Он позволяет задать набор каталогов, содержимое которых будет отслеживаться и в случае изменения (добавления/удаления) будет вызвана рекомпозиция существующих частей.

Пример:

public partial class Shell : UserControl
{
 public MainPage()
 {
  ComposeContainer()
 }

 private void ComposeContainer()
 {
  _aggregateCatalog = new AggregateCatalog(new DeploymentCatalog());
  CompositionHost.Initialize(_aggregateCatalog);
  CompositionInitializer.SatisfyImports(this);
 }
}

ExportFactory

В некоторых ситуация требуется создать несколько экземпляров элементов композиции. Например, если приложение допускает создание пользователем нескольких экземпляров документов (элементов композиции), то обычными средствами этого не достичь. Следует воспользоваться возможностями ExportFactory.

Пример:

[Export]
public class DocumentViewModel {

 [Import]
 public ExportFactory DocumentFactory
 {
    get;
    set;
 }

 protected List Documents
 {
    get;
    set;
 }

 public void CreateDocument()
 {
    Documents.Add(DocumentFactory.CreateExport().Value);
 }
}

Создавая части композиции с использованием ExportFactory, разработчик берёт на себя полную ответственность за время жизни созданного экземпляра. Если экземпляры требуют освобождения памяти, то можно воспользоваться стандартным паттерном Dispose().

Пример:

[Export]
public class DocumentViewModel : IDisposable
{
 private bool isDisposed = false;
 [Import]
 public ExportFactory DocumentFactory
 {
    get;
    set;
 }

 private List> ExportLifeTimeContextList
 {
    get;
    set;
 }

 protected List Documents
 {
    get;
    set;
 }

 public void CreateDocument()
 {
    ExportLifetimeContext LifeTimeContext = DocumentFactory.CreateExport();
    ExportListLifeTimeContext.Add(LifeTimeContext);
    Documents.Add(LifeTimeContext.Value);
 }

 public void Dispose()
 {
    Dispose(true);
    GC.SuppressFinalize(this);
 }

 public void Dispose(bool disposing)
 {
    if (isDisposed)
     return;

    if (disposing)
    {
     foreach(IDisposable d in ExportLifeTimeContextList)
        d.Dispose();
    }
    
    isDisposed = true;
 }
}

Пример реализации механизма рекомпозиции

В качестве примера, который отражает всю простоту и мощь использования MEF в Silverlight приложениях, я выбрал реализацию поддержки визуальных тем (Themes) для контролов приложения.

Приложение должно соответствовать следующим требованиям:

  1. Поддерживать темы
  2. Иметь возможность загружать сторонние XAP файлы с темами
  3. Загрузка XAP файла должна приводить к обновлению списка доступных тем (рекомпозиция)
  4. Выбор темы должен приводить к изменению внешнего вида контролов

Поддержка тем

Для реализации поддержки тем я обратился опубликованному проекту, который предоставляет несколько готовых наборов тем. Из этих проектов я использую xaml файлы с темами для основных контролов.

Каждая тема располагается в отдельной сборке отдельного XAP файла. Сами темы хранятся как ресурсы и извлекаются по запросу.

В качестве механизма доступа к темам используется класс, реализующий интерфейс IThemeLoader. Этот класс умеет извлекать требуемые ресурсы и содержит имя темы.

Пример:

[InheritedExport]
public interface IThemeLoader
{
  string Name
  {
    get;
  }

  IEnumerable Resources
  {
    get;
  }

}


public class ThemeLoader : ThemeLoaderBase
{

  #region IThemeLoader Members

  public override string Name
  {
    get
    {
      return "Accent";
    }
  }

  public override IEnumerable Resources
  {
    get
    {
      yield return LoadResourceDictionary("/SLandMEFdevcamp.AccentTheme;component/Style.xaml");
    }
  }

  #endregion
  
  /*
  protected virtual ResourceDictionary LoadResourceDictionary(string uri)
  {
    return new ResourceDictionary
    {
      Source = new Uri(uri, UriKind.Relative)
    };
  }
  */
}

Атрибут InheritedExport указывает на то, что все реализации интерфейса, помеченного этим атрибутом, должны экспортироваться.

Загрузка сторонних XAP файлов

Для поддержки сторонних XAP файлов, я на основной форме приложения размещаю поле ввода адреса XAP файла и кнопку начала загрузки. Загрузка осуществляется с использованием DeploymentCabinet, который инициирует рекомпозицию.

Как только рекомпозиция произошла, обновится список реализаций IThemeLoader и на UI отобразится новый список доступных тем.

Пример:

private AggregateCatalog _aggregateCatalog = null;
private IEnumerable themesLoaders;

private void ComposeContainer()
{
  _aggregateCatalog = new AggregateCatalog(new DeploymentCatalog());
  CompositionHost.Initialize(_aggregateCatalog);
  CompositionInitializer.SatisfyImports(this);
}

[ImportMany(AllowRecomposition = true)]
public IEnumerable ThemesLoaders
{
  get
  {
    return themesLoaders;
  }
  set
  {
    themesLoaders = value;
    RaisePropertyChanged("ThemesLoaders");
  }
}

private IThemeLoader theme;
public IThemeLoader Theme
{
  get
  {
    return theme;
  }
  set
  {
    theme = value;
    LoadTheme(value);
    RaisePropertyChanged("Theme");
  }
}

private void LoadTheme(IThemeLoader themeLoader)
{
  if (themeLoader.Resources == null || !themeLoader.Resources.Any())
    return;

  App.Current.Resources.MergedDictionaries.Clear();

  foreach (var resourceDict in themeLoader.Resources)
    App.Current.Resources.MergedDictionaries.Add(resourceDict);
}

private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
  DeploymentCatalog catalog = new DeploymentCatalog(XapUrlTextBox.Text);
  catalog.DownloadAsync();

  _aggregateCatalog.Catalogs.Add(catalog);
}

В данном случае, XAP файлы с темами находятся в в той же папке, что и основной XAP файл, а потому можно указывать только имя XAP файла без полного url. Например, SLandMEFdevcamp.AccentTheme.xap, SLandMEFdevcamp.Win7Theme.xap.

Логика работы этого кода следующая:

  • При создании формы происходит инициализация контейнера (ComposeContainer())
  • Пользователь вводит в поле XapUrlTextBox адрес XAP файла и нажимает кнопку загрузки. Загрузка XAP файла приводит к добавлению в каталог новой реализации IThemeLoader и к рекомпозиции свойства ThemesLoaders, которое связано с элементом ListBox на форме
  • Если пользователь выделяет какой-либо элемент ListBox, генерируется изменение свойства Theme, что приводит к вызову метода LoadTheme()
  • Метод LoadTheme() удаляет все существующие ресурсы и добавляет ресурсы для той темы, имя которой выбрал пользователь

Результаты выполнения программы можно видеть здесь:

Заключение

MEF обладает большим числом достоинств, среди которых рекомпозиция, отсутствие строго определённого места, в котором должна производиться регистрация частей композиции и т.д.

MEF для Silverlight обладает дополнительными возможностями, которые не доступны даже в настольной версии, позволяя строить ещё более гибкие программы.

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

Исходный код может быть загружен здесь.


Автор статьи - Max Paulousky, блог - http://www.maxpaulousky.com/blog/

Компании из статьи


Microsoft Украина


Сайт:
http://www.microsoft.com/ukr/ua/

Microsoft Украина Украинское подразделение компании Microsoft.

Ищите нас в интернетах!

Комментарии

Свежие вакансии