Модульное Приложение В Wpf + Caliburn.micro + Castle.windsor

Во-первых, я хочу определить, что в этой статье подразумевается под модульным приложением.

Итак, модульным приложением мы будем считать приложение, состоящее из т.н.

оболочка и набор плагинов.

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

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



Модульное приложение в WPF + Caliburn.Micro + Castle.Windsor

Пожалуй, самая известная платформа для создания WPF-приложений с такой архитектурой — это Призма .

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

Если читатель, знающий Prism, аргументированно укажет мне на мою неправоту и преимущества этого фреймворка, я буду благодарен.

В этой статье будет обсуждаться разработка простого модульного приложения с использованием этих инструментов.



Калиберн.

Микро

Калиберн.

Микро — это фреймворк, который значительно упрощает описание 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="Авторизоваться" /> </StackPanel>



public bool CanLogin(string username, string password) { return !String.IsNullOrEmpty(username) && !String.IsNullOrEmpty(password); } public string Login(string username, string password) { .

}

Нет команды и параметра команды.

В случае крайней необходимости соглашения могут быть пересмотрены.

Конечно, Caliburn.Micro может гораздо больше.

Мы рассмотрим некоторые из них ниже; Об остальном вы можете прочитать в документации.



Замок.

Виндзор

Замок.

Виндзор — один из самых известных и наиболее функциональных 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, тип которого может быть абсолютно любым.

Когда вы выбираете пункт меню, представление, соответствующее этой модели представления, будет отображаться в правой части окна.

Как определить, какой вид подходит? 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 опирается на соглашения об именах.

В этом случае из имени класса модели представления удаляется слово «Модель» и получается имя класса соответствующего представления (Shell.ViewModels.ShellViewModel — Shell.Views.ShellView).

Роль представления может быть 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="Главное окно" 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*/); } }

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

Это можно сделать через 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>(); }

Все, панцирь готов!

Написание модуля

Наш тестовый модуль при загрузке добавит два пункта в меню оболочки.

Первый пункт отобразит очень простую форму с надписью справа.

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

Следуя соглашению об именах, мы создадим две папки 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>()); } }

Готовый! Источники - в git-хабе .



Заключение

В этой статье мы рассмотрели создание простого модульного приложения WPF с использованием фреймворков Castle.Windwsor и Caliburn.Micro. Конечно, многие аспекты не были освещены, некоторые детали опущены и т. д., иначе получилась бы книга, но это не так.

А более подробную информацию можно найти на официальных ресурсах и не только.

Я с радостью постараюсь ответить на любые ваши вопросы.

Спасибо за внимание! Теги: #C++ #wpf #.

NET #castle.windsor #caliburn.micro #.

NET #C++ #Windows Development

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.