0,1
рейтинг
22 октября 2010 в 11:35

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

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

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

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

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

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

Существует несколько особенностей использования этого класса:
  1. Классы, которые вызывают метод SatisfyImports() не могут иметь атрибут [Export];
  2. Объекты инстанцируются только один раз и хранятся в контейнере;
  3. По умолчанию композиции подвергается только текущий XAP файл, что легко исправить.
Пример:
public partial class Shell : UserControl<br>{<br> public MainPage()<br> {<br>  ComposeContainer()<br> }<br><br> private void ComposeContainer()<br> {<br>  CompositionInitializer.SatisfyImports(this);<br> }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
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()<br>{<br> catalog.CancelAsync();<br>}<br><br>private void LoadXapFile(string xapPath)<br>{<br> DeploymentCatalog catalog = new DeploymentCatalog(xapPath);<br> catalog.DownloadCompleted += new EventHandler<AsyncCompletedEventArgs>(DownloadCompleted);<br> catalog.DownloadProgressChanged += new EventHandler<DownloadProgressChangedEventArgs>(ProgressChanged);<br> catalog.DownloadAsync();<br> _aggregateCatalog.Catalogs.Add(catalog);<br>}<br><br>void DownloadCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)<br>{<br> if (e.Error != null)<br>  throw e.Error;<br>}<br><br>void catalog_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)<br>{<br> int progress = e.ProgressPercentage;<br> long received = e.BytesReceived;<br> long total = e.TotalBytesToReceive;<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
CompositionHost
Этот класс является связующим звеном между каталогами, которые содержат информацию об экспорте/импорте частей, и контейнером, который содержит композицию частей. Этот класс позволяет переопределить первоначальную конфигурацию приложения (загрузка частей только из текущего XAP файла). Он позволяет задать набор каталогов, содержимое которых будет отслеживаться и в случае изменения (добавления/удаления) будет вызвана рекомпозиция существующих частей.

Пример:
public partial class Shell : UserControl<br>{<br> public MainPage()<br> {<br>  ComposeContainer()<br> }<br><br> private void ComposeContainer()<br> {<br>  _aggregateCatalog = new AggregateCatalog(new DeploymentCatalog());<br>  CompositionHost.Initialize(_aggregateCatalog);<br>  CompositionInitializer.SatisfyImports(this);<br> }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
ExportFactory<T>
В некоторых ситуация требуется создать несколько экземпляров элементов композиции. Например, если приложение допускает создание пользователем нескольких экземпляров документов (элементов композиции), то обычными средствами этого не достичь. Следует воспользоваться возможностями ExportFactory<T>.

Пример:
[Export]<br>public class DocumentViewModel {<br><br> [Import] <br> public ExportFactory<IDocument> DocumentFactory<br> {<br>    get;<br>    set;<br> }<br><br> protected List<IDocument> Documents<br> {<br>    get;<br>    set;<br> }<br><br> public void CreateDocument() <br> {<br>    Documents.Add(DocumentFactory.CreateExport().Value);<br> }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
Создавая части композиции с использованием ExportFactory<T>, разработчик берёт на себя полную ответственность за время жизни созданного экземпляра. Если экземпляры требуют освобождения памяти, то можно воспользоваться стандартным паттерном Dispose().

Пример:
[Export]<br>public class DocumentViewModel : IDisposable <br>{<br> private bool isDisposed = false;<br> [Import] <br> public ExportFactory<IDocument> DocumentFactory<br> {<br>    get;<br>    set;<br> }<br><br> private List<ExportLifetimeContext<IDocument>> ExportLifeTimeContextList<br> {<br>    get;<br>    set;<br> }<br><br> protected List<IDocument> Documents<br> {<br>    get;<br>    set;<br> }<br><br> public void CreateDocument() <br> {<br>    ExportLifetimeContext<IDocument> LifeTimeContext = DocumentFactory.CreateExport();<br>    ExportListLifeTimeContext.Add(LifeTimeContext);<br>    Documents.Add(LifeTimeContext.Value);<br> }<br><br> public void Dispose()<br> {<br>    Dispose(true);<br>    GC.SuppressFinalize(this);<br> }<br><br> public void Dispose(bool disposing)<br> {<br>    if (isDisposed)<br>     return;<br><br>    if (disposing)<br>    {<br>     foreach(IDisposable d in ExportLifeTimeContextList)<br>        d.Dispose();<br>    }<br>    <br>    isDisposed = true;<br> }<br>}<br><br>* This source code was highlighted with Source Code Highlighter.

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

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

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

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

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

Пример:
[InheritedExport]<br>public interface IThemeLoader<br>{<br>  string Name<br>  {<br>    get;<br>  }<br><br>  IEnumerable<ResourceDictionary> Resources<br>  {<br>    get;<br>  }<br><br>}<br><br><br>public class ThemeLoader : ThemeLoaderBase<br>{<br><br>  #region IThemeLoader Members<br><br>  public override string Name<br>  {<br>    get<br>    {<br>      return "Accent";<br>    }<br>  }<br><br>  public override IEnumerable<ResourceDictionary> Resources<br>  {<br>    get<br>    {<br>      yield return LoadResourceDictionary("/SLandMEFdevcamp.AccentTheme;component/Style.xaml");<br>    }<br>  }<br><br>  #endregion<br>  <br>  /*<br>  protected virtual ResourceDictionary LoadResourceDictionary(string uri)<br>  {<br>    return new ResourceDictionary<br>    {<br>      Source = new Uri(uri, UriKind.Relative)<br>    };<br>  }<br>  */<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
Атрибут InheritedExport указывает на то, что все реализации интерфейса, помеченного этим атрибутом, должны экспортироваться.

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

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

Пример:
private AggregateCatalog _aggregateCatalog = null;<br>private IEnumerable<IThemeLoader> themesLoaders;<br><br>private void ComposeContainer()<br>{<br>  _aggregateCatalog = new AggregateCatalog(new DeploymentCatalog());<br>  CompositionHost.Initialize(_aggregateCatalog);<br>  CompositionInitializer.SatisfyImports(this);<br>}<br><br>[ImportMany(AllowRecomposition = true)]<br>public IEnumerable<IThemeLoader> ThemesLoaders<br>{<br>  get<br>  {<br>    return themesLoaders;<br>  }<br>  set<br>  {<br>    themesLoaders = value;<br>    RaisePropertyChanged("ThemesLoaders");<br>  }<br>}<br><br>private IThemeLoader theme;<br>public IThemeLoader Theme<br>{<br>  get<br>  {<br>    return theme;<br>  }<br>  set<br>  {<br>    theme = value;<br>    LoadTheme(value);<br>    RaisePropertyChanged("Theme");<br>  }<br>}<br><br>private void LoadTheme(IThemeLoader themeLoader)<br>{<br>  if (themeLoader.Resources == null || !themeLoader.Resources.Any())<br>    return;<br><br>  App.Current.Resources.MergedDictionaries.Clear();<br><br>  foreach (var resourceDict in themeLoader.Resources)<br>    App.Current.Resources.MergedDictionaries.Add(resourceDict);<br>}<br><br>private void Button_Click(object sender, System.Windows.RoutedEventArgs e)<br>{<br>  DeploymentCatalog catalog = new DeploymentCatalog(XapUrlTextBox.Text);<br>  catalog.DownloadAsync();<br><br>  _aggregateCatalog.Catalogs.Add(catalog);<br>}<br><br>* This source code was highlighted with Source Code Highlighter.
В данном случае, XAP файлы с темами находятся в в той же папке, что и основной XAP файл, а потому можно указывать только имя XAP файла без полного url. Например, SLandMEFdevcamp.AccentTheme.xap, SLandMEFdevcamp.Win7Theme.xap.

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

Заключение

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

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

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

Исходный код проекта

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

Upd: Класс ExportFactory<T> добавлен в настольную версию библиотеки в MEF 2 Preview 2.
Максим Павловский @paulousky
карма
16,0
рейтинг 0,1
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (29)

  • +1
    Отлично! Кстати с выходом MEF на Silverlight, думаю, что нет, вообще, никакой необходимости в использовании Prism. Дает он, реально, по мелочи. Все что там есть напишется достаточно быстро. Притом он жиреет, причем не в очень хороших направлениях. Добавили логгер, который вряд ли будет использоваться и т.п.
    • +1
      Я думаю что призм более зрелый в плане построения приложений, нежели MEF. В мефе надо писать больше, в призме уже готовые компоненты (регионы, мессенжер, и т.д.).
      • +1
        MEF предназначен для другого, в нем можно загружать во время исполнения части в виде плагинов
        • 0
          Prism, базирующийся и на Mef, и на Unity тоже это позволяет. Только unity не позволяет делать рекомпозицию
        • 0
          Я относительно статьи (сильвер + меф) высказал свое мнение :) Глобально да, у них разные цели.
      • +1
        DelegateCommand пишется за 5 минут. EventAggregator за 10. Ну и подстановка регионов за пару часов. Unity оставляем. Итого, все готово. Что еще вам даст Prism?
        Более того — можно просто выцепить только то, что вам нужно прям из исходного кода Prism. Еще не плохой вариант. Более того в Prism 4 возможность регистрации модулей будет возможна при помощи MEF.
        • +6
          Денис, ты начинаешь напоминать линуксоидов, все можно написать руками ;)
          • +1
            ха-ха :) ну я просто из соображений еще, что в SL важен размер XAP файла. Prism жиреет, а это не простительно. Хотя, конечно, библиотеки там (говорю про версию 2) достаточно независимы, потому может и можно взять только то, что тебе нужно.
            • +1
              Prism естественно будет развиваться и жиреть, надеюсь они все повыносят по отдельным библиотекам и можно будет подключаться только то, что надо
              • 0
                как раз наоборот, в prism 4 уменьшили число сборок с 3 до 2-х
                • +1
                  ага, вот и я про то. И там будет логгинг, который я больше чем уверен использовать я не буду.
            • 0
              Я б не сказал что призм сделал прям наше приложение толстым. 150 кб несжатых dll.
              • +1
                вместе с Unity призм 4 (все библиотеки) — это 385 Kb.
                Ладно, ладно, я сдаюсь. Я просто высказал свое мнение, что я скорее всего сделаю так в будущем.
                • 0
                  никто ж не воюет :) нормальная дискуссия. я тож думал по поводу мефа и призма и пока что решил пользоваться готовым в приложениях коммерческих. А дома играю с кубиками мефа.
                • 0
                  главное не забывать вычищать эти 385 кб из каждого XAP файла (кроме того, где находится entry point), а то они быстренько превратятся в мегабайты.
                  • +1
                    Cамо собой. Когда-то делал это руками. Но ты подарил нам замечательную тузлу :)
                    (Кстати я тебе скриншот с проблемкой отсылал в интерфейсе)
        • 0
          Так все пишется :) иногда это время стоит потратить на бизнес.
    • 0
      Кстати, я готовлю статью о миграции проектов с Prism 2 на Prism 4 mef edition. Есть подводные камни
      • +1
        тоже подумывал о такой статье, но даже руки не доходят посмотреть Prism 4
  • +1
    >> Библиотека MEF появилась относительно недавно

    скорее, «была выпущена». Про MEF я писал на хабре еще 2 с лишним года назад
    habrahabr.ru/blogs/net/26853/
    • 0
      всё относительно… )
  • –1
    Про PartCreationPolicy атрибут автор совсем не слышал?
    • 0
      к чему конкретно вы предъявляете притензии?
      • 0
        Ну если только на однобокость :)

        Про ExportFactory сказали, а про альтернативный вариант [PartCreationPolicy(CreationPolicy.NonShared)] умолчали :)
        • 0
          Госпади ну есть же дока по мефу :)
          Афтар дал свое видение, не будет же он переводить доку на русский и кидать на хабр :)

        • +1
          Тема раскрыта — можно создавать модульные приложения на mef.
          Была бы тема «mef во всех подробностях», тогда бы ваша претензия прокатила.
        • 0
          А, понятно
          этот атрибут есть и в настольной версии. В начале статьи я перечислил то, что отличает Silverlight версию от desktop.
          • 0
            Коллеги, давайте без обид. Мы и так на Хабре в меньшинстве.

            Статья называется «Использование», а не «Отличия от десктопной версии», поэтому предполагалось то, что жизненноважные подробности будут хотя бы упомянуты.
  • +1
    В проекте стоит изменить Startup Project на SLandMEFdevcamp.Web
    Иначе не работает из за пути в виде file:///…

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.