Использование Managed Extensibility Framework (MEF) для разработки модульных Silverlight приложений
Библиотека 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 файла).
Объекты, инстанцированные при композиции, будут находиться в контейнере
до момента уничтожения последнего, т.е. до момента окончания работы
программы.
Существует несколько особенностей использования этого класса:
- Классы, которые вызывают метод
SatisfyImports()
не могут иметь атрибут[Export]
; - Объекты инстанцируются только один раз и хранятся в контейнере;
- По умолчанию композиции подвергается только текущий XAP файл, что легко исправить.
Пример:
public partial class Shell : UserControl
{
public MainPage()
{
ComposeContainer()
}
private void ComposeContainer()
{
CompositionInitializer.SatisfyImports(this);
}
}
DeploymentCatalog
Этот
класс предназначен для динамической загрузки XAP файлов, что позволяет
ещё больше децентрализовать разработку, уменьшить размер главного XAP
файла, увеличив скорость загрузки приложения, и пр. XAP файлы
загружаются в ассинхронном режиме, и разработчик может подписаться на
уведомление об окончании загрузки файла и обработать ошибки, если они
существуют.
Этот класс является составной частью идеи рекомпозиции (повторное
составление композиции из существующих и добавленных частей),
осуществляя последнюю, если есть части, которые допускают рекомпозицию.
Особенности использования DeploymentCatalog
:
- Кэш браузера используется в случае, когда приложение находится в состоянии offline
- Обязательно должен использоваться класс
CompositionHost
(см. ниже) - Если в разных XAP файлах присутствуют одинаковые сборки, то
DeploymentCatalog
будет пытаться добавить их все в каталог, что может привести к возникновению исключения (exception), если рекомпозиция не разрешена. Следует выставлятьCopyLocal=False
сборкам-дубликатам всего приложения или воспользоваться моим расширением для VS2010 XapsMinifier - В каталог загружаются из XAP только те сборки, которые указаны в файле-манифесте
- В режиме Out of Browser загружаемые сборки не кэшируются в файловой системе
Пример:
private void CancelLoading()
{
catalog.CancelAsync();
}
private void LoadXapFile(string xapPath)
{
DeploymentCatalog catalog = new DeploymentCatalog(xapPath);
catalog.DownloadCompleted += new EventHandler
catalog.DownloadProgressChanged += new EventHandler
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
{
get;
set;
}
protected List
{
get;
set;
}
public void CreateDocument()
{
Documents.Add(DocumentFactory.CreateExport().Value);
}
}
Создавая
части композиции с использованием ExportFactoryDispose()
.
Пример:
[Export]
public class DocumentViewModel : IDisposable
{
private bool isDisposed = false;
[Import]
public ExportFactory
{
get;
set;
}
private List
{
get;
set;
}
protected List
{
get;
set;
}
public void CreateDocument()
{
ExportLifetimeContext
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) для контролов приложения.
Приложение должно соответствовать следующим требованиям:
- Поддерживать темы
- Иметь возможность загружать сторонние XAP файлы с темами
- Загрузка XAP файла должна приводить к обновлению списка доступных тем (рекомпозиция)
- Выбор темы должен приводить к изменению внешнего вида контролов
Поддержка тем
Для реализации поддержки тем я обратился опубликованному проекту, который предоставляет несколько готовых наборов тем. Из этих проектов я использую xaml файлы с темами для основных контролов.
Каждая тема располагается в отдельной сборке отдельного XAP файла. Сами темы хранятся как ресурсы и извлекаются по запросу.
В качестве механизма доступа к темам используется класс, реализующий интерфейс IThemeLoader
. Этот класс умеет извлекать требуемые ресурсы и содержит имя темы.
Пример:
[InheritedExport]
public interface IThemeLoader
{
string Name
{
get;
}
IEnumerable
{
get;
}
}
public class ThemeLoader : ThemeLoaderBase
{
#region IThemeLoader Members
public override string Name
{
get
{
return "Accent";
}
}
public override IEnumerable
{
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
private void ComposeContainer()
{
_aggregateCatalog = new AggregateCatalog(new DeploymentCatalog());
CompositionHost.Initialize(_aggregateCatalog);
CompositionInitializer.SatisfyImports(this);
}
[ImportMany(AllowRecomposition = true)]
public IEnumerable
{
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 Украина | Украинское подразделение компании Microsoft. |