Для начала хочу определить, что в данной статье понимается под модульным приложением. Так вот, модульным приложением будем считать такое приложение, которое состоит из т.н. шелла и набора подключаемых модулей. Между ними нет прямой зависимости, только через контракты. Это позволяет независимо вносить изменения в каждый из компонентов, менять их состав и т.д. Думаю, всем и без меня прекрасно известны преимущества модульной архитектуры.
Пожалуй, самым известным фреймворком для создания WPF приложений с такой архитектурой является Prism. В данной статье я не буду проводить сравнительный анализ, т.к. не имею опыта использования Prism. После прочтения туториала, Prism со всеми его регионами, мефом и прочими артефактами, показался мне сильно усложнённым. Если читатель, знающий Prism, обоснованно укажет мне на мою неправоту и преимущества данного фреймворка — буду признателен.
В этой статье будет рассмотрена разработка простейшего модульного приложения с применением указанных инструментов.
Caliburn.Micro — это фреймворк, сильно упрощающий описание View и ViewModel. По сути, он сам создаёт байндинги на основании соглашений об именах, тем самым избавляя разработчика от написания их вручную и делая код меньше и чище. Вот пара примеров с их сайта:
Здесь в XAML мы не указываем ни ItemSource, ни SelectedItem.
Никаких Command и CommandParameter.
Соглашения, при острой необходимости, можно переопределить.
Конечно же, Caliburn.Micro ещё много чего умеет. Что-то мы рассмотрим далее, об остальном можно прочесть в документации.
Castle.Windsor — это один из самых известных и самых функциональных DI-контейнеров для .net (предполагается, что читателю известно о DI и IoC). Да, в Caliburn.Micro, как и во многих других фреймворках, имеется собственный DI-контейнер — SimpleContainer, и для дальнейшего примера его возможностей вполне хватило бы. Но для более сложных задач он может не подойти, поэтому я покажу, как использовать произвольный контейнер на примере Castle.Windsor.
В качестве примера предлагаю рассмотреть процесс создания простого модульного приложения. Главная его часть — шелл — будет представлять из себя окно, в левой части которого будет ListBox-меню. При выборе пункта меню в правой части будет отображаться соответствующая ему форма. Меню будет заполняться модулями при их загрузке либо в процессе работы. Модули могут загружаться как при старте шелла, так и в процессе работы (например, какой-то модуль может подгрузить другие модули при необходимости).
Все контракты будут находится в сборке Contracts, на которую должны ссылаться шелл и модули. Исходя из поставленной задачи, напишем контракт нашего шелла.
Думаю, тут всё понятно. Шелл позволяет модулям управлять меню, а так же загружать модули в процессе работы. Элемент меню содержит отображаемое имя и ViewModel, тип которой может быть абсолютно любой. При выборе пункта меню в правой части окна будет отображаться View, соответствующая данной ViewModel. Как определить, какая же View соответствующая? Об этом позаботится Caliburn.Micro. Такой подход называется ViewModel-first, потому что в коде мы оперируем вью-моделями, а создание вью отходит на второй план и отдаётся на откуп фреймворку. Подробности — далее.
Контракт модуля выглядит совсем просто.
Метод Init() вызывает сторона, инициировавшая загрузку модуля.
Важно отметить, что если в проекте сборки подписаны, а в крупных проектах обычно так и есть, то необходимо быть уверенным, что шелл и модули используют сборки с контрактами одной версии.
Создадим проект типа WPF Application. Далее нам нужно подключить к проекту Caliburn.Micro и Castle.WIndsor. Проще всего сделать сделать это через NuGet.
Но можно и скачать сборки, либо собрать самим. Теперь создадим в проекте две папки: Views и ViewModels. В папке ViewModels создадим класс ShellViewModel; унаследуем его от PropertyChangedBase из Caliburn.Micro, чтобы не реализовывать INotifyPropertyChanged. Это будет вью-модель главного окна шелла.
Само главное окно MainWindow скопируем во View и переименуем в ShellView. Надл не забыть переименовать не только файл, но и класс вместе неймспейсом. Т.е. вместо класса Shell.MainWindows должен быть Shell.Views.ShellView. Это важно. Иначе Caliburn.Micro не сможет определить, что именно это вью соответствует созданной ранее вью-модели. Как было сказано ранее, Caliburn.Micro опирается на соглашения об именах. В данном случае из имени класса вью-модели убирается слово «Model» и получается имя класса соответствующего вью ( Shell.ViewModels.ShellViewModel — Shell.Views.ShellView ). В роли View может выступать Windows, UserControl, Page. В модулях мы будем использовать UserControl.
XAMl-разметка главного окна будет выглядеть так:
Для этого сначала создадим класс Bootstraper с минимальным содержимым:
Он должен наследоваться от BootstrapperBase. Метод OnStartup вызывается при запуске программы. DisplayRootViewFor() по-умолчанию создаёт экземпляр класса вью-модели дефолтным конструктором, ищет соответствующее вью по алгоритму, описанному выше, и отображает его.
Чтобы это заработало, надо отредактировать точку входа в приложение — App.xaml.
Мы убрали StartupUri (отдано на откуп бутстрапперу) и добавили в ресурсы наш бутстраппер. Такая вложенность — не просто так, иначе проект не соберётся.
Теперь при запуске приложения будет создаваться бутстраппер, вызываться OnStartup и отображаться главное окно приложения, привязанное ко вью-модели.
Обратите внимание на создание вью-модели. Она создаётся конструктором по-умолчанию. А если такого у неё нет? Если она имеет зависимости от других сущностей, или другие сущности зависят от неё? Я подвожу к тому, что пришло время пустить в дело DI-контейнер Castle.Windsor.
Создадим класс ShellInstaller.
В нём мы будем регистрировать все наши компоненты в коде с помощью fluent-синтаксиса. Есть возможность делать это через xml, см. документацию на сайте. Пока у нас есть один компонент — вью-модель главного окна. Регистрируем его как синглтон (можно явно не указывать, т.к. это LifeStyle по-умолчанию). Также зарегистрируем сам контейнер, чтобы иметь возможно обращаться к нему. Забегая вперёд — нам это понадобится при загрузке модулей.
Далее вносим изменения в наш бутсраппер:
Создаём контейнер. В переопределённом методе Configure применяем наш инсталлер. Переопределяем метод GetInstance. Его базовая реализация использует конструктор по-умолчанию для создания объекта. Мы же будем пытаться получить объект из контейнера.
Первым делом нам надо научиться загружать модули. А для этого давайте определимся, что же из себя представляет модуль?
Модуль (в нашем случае) — это сборка, содержащая набор классов, реализующих требуемый функционал. Один из этих классов должен реализовывать контракт IModule. Кроме того, так же как и шелл, модуль должен иметь инсталлер, регистрирующий компоненты (классы) модуля в DI-контейнере.
Теперь приступим к реализации загрузчика. Загрузка будет вызываться при старте шелла, а также может быть вызвана в процессе работы, поэтому создадим отдельный класс.
Через конструктор ижектится контейнер шелла (помните, мы его специально для этого регистрировали ?). В методе LoadModule получаем инсталлер из сборки модуля. Создаём отдельный контейнер для компонент загружаемого модуля. Регистрируем его как дочерний по отношению к контейнеру шелла. Применяем инсталлер модуля. Пытаемся вернуть экземпляр IModule. Сообщаем Caliburn.Micro о сборке, чтобы он применил соглашения о наименованиях для компонентов в ней.
И не забываем зарегистрировать наш загрузчик модулей в ShellInstaller.
Немного о «дочернем контейнере». Суть в том, что все его компоненты «видят» компоненты из родительского контейнера, помимо своего, но не наоборот. Компоненты разных дочерних контейнеров так же ничего не знают друг о друге. Получаем изоляцию шелла от модулей и модулей друг от друга, но не модулей от шелла — его они видят.
Далее реализуем контракт IShell, через который модули будут обращаться к шеллу.
Регистрируем.
Теперь нам нужно сделать так, чтобы модули загружались при запуске шелла. А откуда они возьмутся? В нашем примере шелл будет искать сборки с модулями рядом с Shell.exe.
Данный функционал следует реализовать в методе OnStartup:
Всё, шелл готов!
Наш тестовый модуль при загрузке будет добавлять в меню шелла два пункта. Первый пункт отобразит в правой части совсем простую форму с надписью. Второй — форму с кнопкой, с помощью которой можно будет догрузить модуль, выбрав его сборку в открывшемся диалоге выбора файла. Следуя соглашению о именах, создадим 2 папки Views и ViewModels. Затем наполним их.
Первые вью и вью-модель — тривиальны:
Вторая вью тоже не отличается сложностью.
Во второй вью-модели реализуем загрузку выбранного модуля.
Реализуем контракт IModule. В методе Init добавляем пункты в меню шелла.
И последний штрих — инсталлер.
Готово!
Исходники — на гит-хабе.
В данной статье мы рассмотрели создание простейшего модульного WPF-приложения с помощью фреймворков Castle.Windwsor и Caliburn.Micro. Конечно, многие аспекты освещены не были, некоторые детали опущены и т.д., иначе получилась бы книга, а не статься. А более подробную информацию можно найти на официальных ресурсах, да и не только.
На все возникшие вопросы с удовольствием постараюсь ответить.
Спасибо за внимание!
Пожалуй, самым известным фреймворком для создания WPF приложений с такой архитектурой является Prism. В данной статье я не буду проводить сравнительный анализ, т.к. не имею опыта использования Prism. После прочтения туториала, Prism со всеми его регионами, мефом и прочими артефактами, показался мне сильно усложнённым. Если читатель, знающий Prism, обоснованно укажет мне на мою неправоту и преимущества данного фреймворка — буду признателен.
В этой статье будет рассмотрена разработка простейшего модульного приложения с применением указанных инструментов.
Caliburn.Micro
Caliburn.Micro — это фреймворк, сильно упрощающий описание View и ViewModel. По сути, он сам создаёт байндинги на основании соглашений об именах, тем самым избавляя разработчика от написания их вручную и делая код меньше и чище. Вот пара примеров с их сайта:
<ListBox x:Name="Products" />
public BindableCollection<ProductViewModel> Products
{
get; private set;
}
public ProductViewModel SelectedProduct
{
get { return _selectedProduct; }
set
{
_selectedProduct = value;
NotifyOfPropertyChange(() => SelectedProduct);
}
}
Здесь в XAML мы не указываем ни ItemSource, ни SelectedItem.
<StackPanel>
<TextBox x:Name="Username" />
<PasswordBox x:Name="Password" />
<Button x:Name="Login" Content="Log in" />
</StackPanel>
public bool CanLogin(string username, string password)
{
return !String.IsNullOrEmpty(username) && !String.IsNullOrEmpty(password);
}
public string Login(string username, string password)
{
...
}
Никаких Command и CommandParameter.
Соглашения, при острой необходимости, можно переопределить.
Конечно же, Caliburn.Micro ещё много чего умеет. Что-то мы рассмотрим далее, об остальном можно прочесть в документации.
Castle.Windsor
Castle.Windsor — это один из самых известных и самых функциональных DI-контейнеров для .net (предполагается, что читателю известно о DI и IoC). Да, в Caliburn.Micro, как и во многих других фреймворках, имеется собственный DI-контейнер — SimpleContainer, и для дальнейшего примера его возможностей вполне хватило бы. Но для более сложных задач он может не подойти, поэтому я покажу, как использовать произвольный контейнер на примере Castle.Windsor.
Задача
В качестве примера предлагаю рассмотреть процесс создания простого модульного приложения. Главная его часть — шелл — будет представлять из себя окно, в левой части которого будет ListBox-меню. При выборе пункта меню в правой части будет отображаться соответствующая ему форма. Меню будет заполняться модулями при их загрузке либо в процессе работы. Модули могут загружаться как при старте шелла, так и в процессе работы (например, какой-то модуль может подгрузить другие модули при необходимости).
Контракты
Все контракты будут находится в сборке Contracts, на которую должны ссылаться шелл и модули. Исходя из поставленной задачи, напишем контракт нашего шелла.
public interface IShell
{
IList<ShellMenuItem> MenuItems { get; }
IModule LoadModule(Assembly assembly);
}
public class ShellMenuItem
{
public string Caption { get; set; }
public object ScreenViewModel { get; set; }
}
Думаю, тут всё понятно. Шелл позволяет модулям управлять меню, а так же загружать модули в процессе работы. Элемент меню содержит отображаемое имя и ViewModel, тип которой может быть абсолютно любой. При выборе пункта меню в правой части окна будет отображаться View, соответствующая данной ViewModel. Как определить, какая же View соответствующая? Об этом позаботится Caliburn.Micro. Такой подход называется ViewModel-first, потому что в коде мы оперируем вью-моделями, а создание вью отходит на второй план и отдаётся на откуп фреймворку. Подробности — далее.
Контракт модуля выглядит совсем просто.
public interface IModule
{
void Init();
}
Метод Init() вызывает сторона, инициировавшая загрузку модуля.
Важно отметить, что если в проекте сборки подписаны, а в крупных проектах обычно так и есть, то необходимо быть уверенным, что шелл и модули используют сборки с контрактами одной версии.
Начинаем реализовывать Shell
Создадим проект типа WPF Application. Далее нам нужно подключить к проекту Caliburn.Micro и Castle.WIndsor. Проще всего сделать сделать это через NuGet.
PM> Install-Package Caliburn.Micro -Version 2.0.2
PM> Install-Package Castle.Windsor
Но можно и скачать сборки, либо собрать самим. Теперь создадим в проекте две папки: Views и ViewModels. В папке ViewModels создадим класс ShellViewModel; унаследуем его от PropertyChangedBase из Caliburn.Micro, чтобы не реализовывать INotifyPropertyChanged. Это будет вью-модель главного окна шелла.
class ShellViewModel: PropertyChangedBase
{
public ShellViewModel()
{
MenuItems = new ObservableCollection<ShellMenuItem>();
}
public ObservableCollection<ShellMenuItem> MenuItems { get; private set; }
private ShellMenuItem _selectedMenuItem;
public ShellMenuItem SelectedMenuItem
{
get { return _selectedMenuItem; }
set
{
if(_selectedMenuItem==value)
return;
_selectedMenuItem = value;
NotifyOfPropertyChange(() => SelectedMenuItem);
NotifyOfPropertyChange(() => CurrentView);
}
}
public object CurrentView
{
get { return _selectedMenuItem == null ? null : _selectedMenuItem.ScreenViewModel; }
}
}
Само главное окно MainWindow скопируем во View и переименуем в ShellView. Надл не забыть переименовать не только файл, но и класс вместе неймспейсом. Т.е. вместо класса Shell.MainWindows должен быть Shell.Views.ShellView. Это важно. Иначе Caliburn.Micro не сможет определить, что именно это вью соответствует созданной ранее вью-модели. Как было сказано ранее, Caliburn.Micro опирается на соглашения об именах. В данном случае из имени класса вью-модели убирается слово «Model» и получается имя класса соответствующего вью ( Shell.ViewModels.ShellViewModel — Shell.Views.ShellView ). В роли View может выступать Windows, UserControl, Page. В модулях мы будем использовать UserControl.
XAMl-разметка главного окна будет выглядеть так:
<Window x:Class="Shell.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ListBox x:Name="MenuItems" DisplayMemberPath="Caption" Grid.Column="0"/>
<ContentControl x:Name="CurrentView" Grid.Column="1"/>
</Grid>
</Window>
Запускаем Caliburn.Micro
Для этого сначала создадим класс Bootstraper с минимальным содержимым:
public class ShellBootstrapper : BootstrapperBase
{
public ShellBootstrapper()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<ShellViewModel>();
}
}
Он должен наследоваться от BootstrapperBase. Метод OnStartup вызывается при запуске программы. DisplayRootViewFor() по-умолчанию создаёт экземпляр класса вью-модели дефолтным конструктором, ищет соответствующее вью по алгоритму, описанному выше, и отображает его.
Чтобы это заработало, надо отредактировать точку входа в приложение — App.xaml.
<Application x:Class="Shell.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shell="clr-namespace:Shell">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<shell:ShellBootstrapper x:Key="bootstrapper" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Мы убрали StartupUri (отдано на откуп бутстрапперу) и добавили в ресурсы наш бутстраппер. Такая вложенность — не просто так, иначе проект не соберётся.
Теперь при запуске приложения будет создаваться бутстраппер, вызываться OnStartup и отображаться главное окно приложения, привязанное ко вью-модели.
Обратите внимание на создание вью-модели. Она создаётся конструктором по-умолчанию. А если такого у неё нет? Если она имеет зависимости от других сущностей, или другие сущности зависят от неё? Я подвожу к тому, что пришло время пустить в дело DI-контейнер Castle.Windsor.
Запускаем Castle.Windsor
Создадим класс ShellInstaller.
class ShellInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container
.Register(Component.For<IWindsorContainer>().Instance(container))
.Register(Component.For<ShellViewModel>() /*.LifeStyle.Singleton*/);
}
}
В нём мы будем регистрировать все наши компоненты в коде с помощью fluent-синтаксиса. Есть возможность делать это через xml, см. документацию на сайте. Пока у нас есть один компонент — вью-модель главного окна. Регистрируем его как синглтон (можно явно не указывать, т.к. это LifeStyle по-умолчанию). Также зарегистрируем сам контейнер, чтобы иметь возможно обращаться к нему. Забегая вперёд — нам это понадобится при загрузке модулей.
Далее вносим изменения в наш бутсраппер:
public class ShellBootstrapper : BootstrapperBase
{
private readonly IWindsorContainer _container = new WindsorContain-er();
public ShellBootstrapper()
{
Initialize();
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<ShellViewModel>();
}
protected override void Configure()
{
_container.Install(new ShellInstaller());
}
protected override object GetInstance(Type service, string key)
{
return string.IsNullOrWhiteSpace(key)
? _container.Kernel.HasComponent(service)
? _container.Resolve(service)
: base.GetInstance(service, key)
: _container.Kernel.HasComponent(key)
? _container.Resolve(key, service)
: base.GetInstance(service, key);
}
}
Создаём контейнер. В переопределённом методе Configure применяем наш инсталлер. Переопределяем метод GetInstance. Его базовая реализация использует конструктор по-умолчанию для создания объекта. Мы же будем пытаться получить объект из контейнера.
Взаимодействие с модулями
Первым делом нам надо научиться загружать модули. А для этого давайте определимся, что же из себя представляет модуль?
Модуль (в нашем случае) — это сборка, содержащая набор классов, реализующих требуемый функционал. Один из этих классов должен реализовывать контракт IModule. Кроме того, так же как и шелл, модуль должен иметь инсталлер, регистрирующий компоненты (классы) модуля в DI-контейнере.
Теперь приступим к реализации загрузчика. Загрузка будет вызываться при старте шелла, а также может быть вызвана в процессе работы, поэтому создадим отдельный класс.
class ModuleLoader
{
private readonly IWindsorContainer _mainContainer;
public ModuleLoader(IWindsorContainer mainContainer)
{
_mainContainer = mainContainer;
}
public IModule LoadModule(Assembly assembly)
{
try
{
var moduleInstaller = FromAssembly.Instance(assembly);
var modulecontainer = new WindsorContainer();
_mainContainer.AddChildContainer(modulecontainer);
modulecontainer.Install(moduleInstaller);
var module = modulecontainer.Resolve<IModule>();
if (!AssemblySource.Instance.Contains(assembly))
AssemblySource.Instance.Add(assembly);
return module;
}
catch (Exception ex)
{
//TODO: good exception handling
return null;
}
}
}
Через конструктор ижектится контейнер шелла (помните, мы его специально для этого регистрировали ?). В методе LoadModule получаем инсталлер из сборки модуля. Создаём отдельный контейнер для компонент загружаемого модуля. Регистрируем его как дочерний по отношению к контейнеру шелла. Применяем инсталлер модуля. Пытаемся вернуть экземпляр IModule. Сообщаем Caliburn.Micro о сборке, чтобы он применил соглашения о наименованиях для компонентов в ней.
И не забываем зарегистрировать наш загрузчик модулей в ShellInstaller.
.Register(Component.For<ModuleLoader>()
Немного о «дочернем контейнере». Суть в том, что все его компоненты «видят» компоненты из родительского контейнера, помимо своего, но не наоборот. Компоненты разных дочерних контейнеров так же ничего не знают друг о друге. Получаем изоляцию шелла от модулей и модулей друг от друга, но не модулей от шелла — его они видят.
Далее реализуем контракт IShell, через который модули будут обращаться к шеллу.
class ShellImpl: IShell
{
private readonly ModuleLoader _loader;
private readonly ShellViewModel _shellViewModel;
public ShellImpl(ModuleLoader loader, ShellViewModel shellViewModel)
{
_loader = loader;
_shellViewModel = shellViewModel;
}
public IList<ShellMenuItem> MenuItems { get { return _shellViewModel.MenuItems; } }
public IModule LoadModule(Assembly assembly)
{
return _loader.LoadModule(assembly);
}
}
Регистрируем.
.Register(Component.For<IShell>().ImplementedBy<ShellImpl>())
Теперь нам нужно сделать так, чтобы модули загружались при запуске шелла. А откуда они возьмутся? В нашем примере шелл будет искать сборки с модулями рядом с Shell.exe.
Данный функционал следует реализовать в методе OnStartup:
protected override void OnStartup(object sender, StartupEventArgs e)
{
var loader = _container.Resolve<ModuleLoader>();
var exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var pattern = "*.dll";
Directory
.GetFiles(exeDir, pattern)
.Select(Assembly.LoadFrom)
.Select(loader.LoadModule)
.Where(module => module != null)
.ForEach(module => module.Init());
DisplayRootViewFor<ShellViewModel>();
}
Всё, шелл готов!
Пишем модуль
Наш тестовый модуль при загрузке будет добавлять в меню шелла два пункта. Первый пункт отобразит в правой части совсем простую форму с надписью. Второй — форму с кнопкой, с помощью которой можно будет догрузить модуль, выбрав его сборку в открывшемся диалоге выбора файла. Следуя соглашению о именах, создадим 2 папки Views и ViewModels. Затем наполним их.
Первые вью и вью-модель — тривиальны:
<UserControl x:Class="Module.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="60">Hello, I'm first !</TextBlock>
</Grid>
</UserControl>
class FirstViewModel
{
}
Вторая вью тоже не отличается сложностью.
<UserControl x:Class="Module.Views.SecondView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Button x:Name="Load" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="50">Load Module</Button>
</Grid>
</UserControl>
Во второй вью-модели реализуем загрузку выбранного модуля.
class SecondViewModel
{
private readonly IShell _shell;
public SecondViewModel(IShell shell)
{
_shell = shell;
}
public void Load()
{
var dlg = new OpenFileDialog ();
if (dlg.ShowDialog().GetValueOrDefault())
{
var asm = Assembly.LoadFrom(dlg.FileName);
var module = _shell.LoadModule(asm);
if(module!=null)
module.Init();
}
}
}
Реализуем контракт IModule. В методе Init добавляем пункты в меню шелла.
class ModuleImpl : IModule
{
private readonly IShell _shell;
private readonly FirstViewModel _firstViewModel;
private readonly SecondViewModel _secondViewModel;
public ModuleImpl(IShell shell, FirstViewModel firstViewModel, SecondViewModel secondViewModel)
{
_shell = shell;
_firstViewModel = firstViewModel;
_secondViewModel = secondViewModel;
}
public void Init()
{
_shell.MenuItems.Add(new ShellMenuItem() { Caption = "First", ScreenViewModel = _firstViewModel });
_shell.MenuItems.Add(new ShellMenuItem() { Caption = "Second", ScreenViewModel = _secondViewModel });
}
}
И последний штрих — инсталлер.
public class ModuleInstaller:IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container
.Register(Component.For<FirstViewModel>())
.Register(Component.For<SecondViewModel>())
.Register(Component.For<IModule>().ImplementedBy<ModuleImpl>());
}
}
Готово!
Исходники — на гит-хабе.
Заключение
В данной статье мы рассмотрели создание простейшего модульного WPF-приложения с помощью фреймворков Castle.Windwsor и Caliburn.Micro. Конечно, многие аспекты освещены не были, некоторые детали опущены и т.д., иначе получилась бы книга, а не статься. А более подробную информацию можно найти на официальных ресурсах, да и не только.
На все возникшие вопросы с удовольствием постараюсь ответить.
Спасибо за внимание!
Комментарии (7)
dymanoid
25.11.2015 22:23Одно из преимуществ Prism, например, в том, что описанный вами функционал загрузки модулей и регистрации их в контейнере доступен из коробки.
IL_Agent
26.11.2015 11:26Это очень простой функционал, несколько коротеньких строк :)
var moduleInstaller = FromAssembly.Instance(assembly); var modulecontainer = new WindsorContainer(); _mainContainer.AddChildContainer(modulecontainer); modulecontainer.Install(moduleInstaller); var module = modulecontainer.Resolve<IModule>(); if (!AssemblySource.Instance.Contains(assembly)) AssemblySource.Instance.Add(assembly);
А в Призме как? «Из коробки» ведь его тоже надо достать и приготовить. Проще ли? Гибче? Судя по примерам, что попадаются в выдаче — не очень.
Вообще, для сравнения правильней было бы реализовать весь приведённый в статье пример на Призме. Быть может, сделаю и напишу, как появится время.
astudent
Почему не Prism? Активно развивается, является частью .NET Foundation и есть версия даже для Xamarin.Forms.
IL_Agent
Я написал в начале статьи, почему. Как бы выглядело подобное приложение с Prism?
Caliburn.Micro тоже развивается и в версии 3.0 будет уметь Xamarin.Forms.
Holms
А как насчет MVVM Light?
IL_Agent
Никак :). Я с ним не сильно знаком, но, как я понимаю, это лишь набор хелперов вроде PropertyChangedBase. Все привязки все равно самому надо писать. Или нет?
Bonart
Caliburn Micro намного меньше и выразительнее.