Сегодняшняя статья посвящена продолжении темы
навигации в Prism 5. Я много
времени потратил на написание примера, поскольку никак не мог придумать пример,
который был бы простым, но и раскрывал суть рассмотренной темы. Особенно сложно
это бывает в том случае, когда в интернете нет нормального описания той темы,
которую ты пытаешься донести читателям. Сегодня мы рассмотрим, как можно шарить
данные между представлениями в Prism 5.
Поговорим об использовании RegionContext для
синхронизации данных. Так как в интернете очень мало информации о том, как
вообще можно управлять RegionContext с использованием
паттерна MVVM, а именно – с модели представления, это создает
дополнительные сложности. Если вы используете Prism в своих проектах, то благодаря этой теме сможете узнать, как
не натыкаться на те же шишки, на которые натыкался я. Пример получился не очень
сложным, и надеюсь, что он будет понятен для читателей. Ниже
представлена структура проекта, в которой основную часть составляют модели.
Давайте воссоздадим
у себя последовательно структуру этого проекта. Для этого создаем в Visual Studio новый WPF проект и назовем его “SharingDataBetweenRegions”. Учтите при создании проекта, что .NET Framework должен быть указан
не ниже, чем версия 4.5, поскольку только с этой версии работает Prism 5.
Следующим делом
заходим в NuGet Package Manager и устанавливаем библиотеки Prism и Prism.UnityExtensions.
Prism.UnityExtensions – это расширения
для IoC контейнера Unity.
Вместо MEF, я предпочитаю использовать какой-либо из
популярных IoC контейнеров. В
основном выбор падает либо на Unity, либо на Autofac. В последнее время больше приходится
работать все-таки с Unity из-за того, что
он используется в большинстве проектов, с которыми приходится сталкиваться.
Надеюсь, что вы уже знакомы с Prism хоть немного, и
такие понятия, как Shell, RegionManager, Bootstrapper
не вызывают у вас непонимания. Если же все-таки эти названия звучат как
заклинания, тогда вам лучше вначале начать ознакомление с Prism c
этих
статей: "Введение в Prism 5. Bootstrapper" и "Введение в Prism 5. Работа с регионами". Эти статьи, по
сути, – самые азы, которые нужны для того чтобы работать с Prism.
А мы продолжим написание нашего проекта.
Первым делом нужно разбить наш проект на такую структуру папок:
·
Helpers – разные вспомогательные классы для
проекта;
·
Models – модели с паттерна MVVM;
·
Modules – модули с Prism;
·
ViewModels – модели представления;
·
Views – представления;
· Regions – контролы, которые используются для заполнения
регионов.
Вроде, все очень
кратко, и пока ничего не упустил. Затем перейдем к реализации наших моделей, при этом кратко описывая функции модели. Начнем наше описание
с интерфейса IBook. Этот интерфейс
необходимый для того, чтобы показать структуру книги.
public interface IBook
{
string Author { get; set; }
string Title { get; set; }
DateTime Year { get; set; }
string SN { get; set; }
int Count { get; set; }
double Cost { get; set; }
}
В результате у нас выйдет что-то в виде системы,
которая позволяет посмотреть, кто приобрел какую книгу и в каком количестве.
Это вкратце о том, что будет делать наш проект. Реализация же интерфейса IBook приведена в классе Book.
public class Book : BindableBase, IBook
{
private string _author;
public string Author
{
get { return
_author; }
set
{
SetProperty(ref
_author, value);
}
}
private string _title;
public string Title
{
get { return
_title; }
set
{
SetProperty(ref
_title, value);
}
}
private DateTime _year;
public DateTime Year
{
get { return
_year; }
set
{
SetProperty(ref
_year, value);
}
}
private string _sn;
public string SN
{
get { return
_sn; }
set
{
SetProperty(ref _sn,
value);
}
}
private int _count;
public int Count
{
get { return
_count; }
set
{
SetProperty(ref
_count, value);
}
}
private double _cost;
public double Cost
{
get { return
_cost; }
set { SetProperty(ref
_cost, value); }
}
}
Для тех,
кто читает мой блог, может показаться немного странным использование функции SetProperty, для того чтобы установить значения для свойства и
автоматически вызвать функцию OnPropertyChanged,
вместо записей вроде этих:
private double _cost;
public double Cost
{
get { return _cost; }
set
{
_cost = value;
OnPropertyChanged(() => Cost);
}
}
Или же
private double _cost;
public double Cost
{
get { return _cost; }
set
{
_cost = value;
OnPropertyChanged("Cost");
}
}
Это я сделал больше для того, чтобы показать, что
такой тип записи тоже имеет место быть. Тем более, функция SetProperty выполняет, по сути, два действия. Первое:
устанавливает значение нужной переменной. Второе: вызывает функцию OnPropertyChanged, для
того чтобы указать представлению, что данные обновились.
Следующим
интерфейсом будет интерфейс ICustomer, который будет отображать
информацию о покупателях.
public interface ICustomer
{
int Id { get; set; }
string FirstName { get; set; }
string LastName { get; set; }
}
Этот интерфейс
очень примитивный, и комментировать его излишне. Поэтому просто давайте
посмотрим на имплементацию данного интерфейса.
public class Customer : BindableBase, ICustomer
{
private int _id;
public int Id
{
get { return
_id; }
set
{
SetProperty(ref
_id, value);
}
}
private string _firstName;
public string FirstName
{
get
{
return
_firstName;
}
set
{
SetProperty(ref
_firstName, value);
}
}
private string _lastName;
public string LastName
{
get
{
return
_lastName;
}
set
{
SetProperty(ref
_lastName, value);
}
}
}
Затем у нас есть еще интерфейс IOrder, который будет хранить информацию о покупке
книг покупателем.
public interface IOrder
{
int Id { get; set; }
int CustomerId { get; set; }
double Price { get; set; }
List<IBook> BuyList { get; set; }
int Count { get; }
}
Здесь свойство Count, вероятнее всего, излишнее, так как информацию
о количестве книг мы можем получить из свойства BuyList, которое тоже названо не очень красиво, к
сожалению. Реализация IOrder приведена в классе Order ниже.
public class Order : BindableBase, IOrder
{
private int _id;
public int Id
{
get
{
return
_id;
}
set
{
SetProperty(ref
_id, value);
}
}
private int _customerId;
public int CustomerId
{
get
{
return
_customerId;
}
set
{
SetProperty(ref
_customerId, value);
}
}
private double _price;
public double Price
{
get
{
return
_price;
}
set
{
SetProperty(ref
_price, value);
}
}
public int Count
{
get { return
BuyList.Count; }
}
private List<IBook> _buyList;
public List<IBook> BuyList
{
get { return
_buyList; }
set { SetProperty(ref
_buyList, value); }
}
}
Осталась последняя модель OrderInfo, которая
будет отображать информацию о покупке конкретной книги в модели. Это
вспомогательная модель только для отображения.
public class OrderInfo : BindableBase
{
private string _firstName;
public string FirstName
{
get
{
return
_firstName;
}
set
{
SetProperty(ref
_firstName, value);
}
}
private string _lastName;
public string LastName
{
get
{
return
_lastName;
}
set
{
SetProperty(ref
_lastName, value);
}
}
private double
_price;
public double Price
{
get
{
return
_price;
}
set
{
SetProperty(ref
_price, value);
}
}
private int _count;
public int Count
{
get { return
_count; }
set { SetProperty(ref
_count, value); }
}
}
Самая примитивная
логика у нас заняла довольно-таки много времени и прилично места в статье. Но
она необходима, для того чтобы сложить всю цепочку воедино. Затем нам нужно наше
главное окно в перенести папку View и переименовать его с MainWindow в Shell, чтобы оперировать терминами Prism. Посмотрите, чтобы у
вас в коде не было названий MainWindow. Не забудьте также убрать начальную
загрузку главного окна с App.xaml, в котором запуск окна указывается
параметром StartupUri. Удалите StartupUri со значением
совсем. Загрузка главного окна будет лежать на бутстраппере. Как его
реализовать, мы вернемся несколько позже. А для начала нужно сделать разметку для
главного окна (Shell.xaml). Выглядит она следующим образом.
<Window x:Class="SharingDataBetweenRegions.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:regions="http://www.codeplex.com/CompositeWPF"
Title="MainWindow" Height="350" Width="525">
<Window.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFFFFFFF" Offset="0"/>
<GradientStop Color="#FCFFF5" Offset="0.992"/>
<GradientStop Color="#3E606F" Offset="0.185"/>
</LinearGradientBrush>
</Window.Background>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Margin="10,5,10,10" Grid.Row="1" CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FFC8DDC5" BorderThickness="2,2,2,2">
<Grid Width="Auto" Height="Auto" Margin="10,10,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".4*"/>
<ColumnDefinition Width=".6*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FCFFF5" BorderThickness="2,2,2,2" Margin="5" Padding="5">
<StackPanel Orientation="Vertical">
<ContentControl regions:RegionManager.RegionName="BookRegion" />
</StackPanel>
</Border>
<Border Grid.Column="1" CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FCFFF5" BorderThickness="2,2,2,2" Margin="5" Padding="5">
<StackPanel Orientation="Vertical">
<ItemsControl regions:RegionManager.RegionName="OrderRegion" />
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
</Window>
В дизайнере это
чудо выглядит вот так:
В левой части мы
сделаем возможность выбора книги, а в правой части будет выведена информация о
покупателях, цене и количестве купленных книг. Приступим к реализации
заполнителя для региона BookRegion. Для этого перейдем в папку Regions и добавим новый UserControl с именем BookView.
<UserControl x:Class="SharingDataBetweenRegions.Views.Regions.BookView"
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>
<ComboBox Name="CustomerCbx" Margin="5" Width="Auto" HorizontalAlignment="Stretch"
ItemsSource="{Binding
Books}"
SelectedItem="{Binding SelectedBook}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Author}"
/>
<TextBlock Text="{Binding Title}" Margin="5,0,0,0"/>
<TextBlock Text="{Binding Cost}" Margin="5,0,0,0"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</UserControl>
Реализация очень
примитивная и представляет собой обычный ComboBox переопределённым стилем для отображения
данных. Вы, конечно же, можете пойти дальше и написать просто свой адаптер (класс
IRegionAdapter), для того чтобы
использовать ComboBox явно. Если вы все-таки захотите это реализовать, то можете воспользоваться готовым адаптером ItemsControlRegionAdapter. Возможно, мы рассмотрим тему адаптеров в какой-то из следующих статей, поскольку
примеров по использованию адаптеров в призме очень мало. Нам же осталось реализовать
заполнитель, который будет использоваться для региона OrderRegion. Для этого также в папке Regions создадим новый UserControl с именем OrderView. Это представление также очень простое и представляет
собой простой список с переопределённый темплейтом для отображения данных.
<UserControl x:Class="SharingDataBetweenRegions.Views.Regions.OrderView"
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>
<StackPanel>
<ListBox ItemsSource="{Binding OrderDetails}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}"
/>
<TextBlock Text="{Binding LastName}" Margin="5,0,0,0" />
<TextBlock FontWeight="Bold" Text="Price:" Margin="5,0,0,0"/>
<TextBlock Text="{Binding Price}" Margin="5,0,0,0"/>
<TextBlock FontWeight="Bold" Text="Count:" Margin="5,0,0,0"/>
<TextBlock Text="{Binding Count}" Margin="5,0,0,0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</UserControl>
Поскольку с
представлениями мы закончили, по сути, осталось для каждого из этих
представлений написать соответственную модель представления и связать ее с
нужным представлением. Начнем, пожалуй, с модели представления для региона BookRegion. Для этого перейдем
в папку ViewModels и создадим класс BookViewModel.
public class BookViewModel : BindableBase
{
private readonly IRegionManager _regionManager;
#region Constructor
public BookViewModel(IRegionManager manager)
{
_regionManager = manager;
Books = new ObservableCollection<IBook>(Helpers.HelperGenerator.GenerateBooks());
}
#endregion
public ObservableCollection<IBook> Books { get; set; }
private IBook _selectedBook;
public IBook SelectedBook
{
get { return
_selectedBook; }
set
{
SetProperty(ref
_selectedBook, value);
_regionManager.Regions["BookRegion"].Context = _selectedBook;
}
}
}
Только вот проблема, что если вы добавите себе это класс, то он у вас сразу подсветит строку
Books = new ObservableCollection<IBook>(Helpers.HelperGenerator.GenerateBooks());
Все дело в том, что
мы не добавили себе класс HelperGenerator. Этот статический класс, по сути, генерирует
нам фейковые данные, для того чтобы проверить работоспособность нашего творения.
Обычно такой подход используют дизайнеры, чтобы проверить, что их творение
работает нормально. Ниже приведена реализация данного класса со всеми
методами.
public static class HelperGenerator
{
public static List<IBook> GenerateBooks()
{
var cshapInDepth = ServiceLocator.Current.GetInstance<IBook>();
cshapInDepth.Author = "Jon Skeet";
cshapInDepth.Title = "C# in Depth";
cshapInDepth.Count = 3;
cshapInDepth.SN = "ISBN: 9781617291340";
cshapInDepth.Year = new DateTime(2013, 9, 10);
cshapInDepth.Cost = 44.3;
var refactoringBook = ServiceLocator.Current.GetInstance<IBook>();
refactoringBook.Author = "Martin Fowler";
refactoringBook.Title = "Refactoring: Improving the Design of
Existing Code";
refactoringBook.Count = 2;
refactoringBook.SN = "ISBN-10: 0201485672";
refactoringBook.Year = new DateTime(1999, 7, 8);
refactoringBook.Cost = 52;
var clrViaCsharp = ServiceLocator.Current.GetInstance<IBook>();
clrViaCsharp.Author = "Jeffrey Richter";
clrViaCsharp.Title = "CLR via C# (Developer Reference)";
clrViaCsharp.Count = 5;
clrViaCsharp.SN = "ISBN-10: 0735667454";
clrViaCsharp.Year = new DateTime(2012, 12, 4);
clrViaCsharp.Cost = 32.5;
return new List<IBook>
{
cshapInDepth,
refactoringBook,
clrViaCsharp
};
}
public static List<ICustomer> GenerateCustomers()
{
var firstCustomer = ServiceLocator.Current.GetInstance<ICustomer>();
firstCustomer.Id = 1;
firstCustomer.FirstName = "Aleksandr";
firstCustomer.LastName = "Polukhovich";
var secondCustomer = ServiceLocator.Current.GetInstance<ICustomer>();
secondCustomer.Id = 2;
secondCustomer.FirstName = "Irina";
secondCustomer.LastName = "Polukhovich";
var thirdCustomer = ServiceLocator.Current.GetInstance<ICustomer>();
thirdCustomer.Id = 3;
thirdCustomer.FirstName = "Viktoria";
thirdCustomer.LastName = "Polukhovich";
return new List<ICustomer>
{
firstCustomer,
secondCustomer,
thirdCustomer
};
}
public static List<IOrder> GenerateOrders()
{
var books =
GenerateBooks();
var customers =
GenerateCustomers();
var orders = new List<IOrder>();
int i = 1;
foreach (var
customer in customers)
{
var
order = ServiceLocator.Current.GetInstance<IOrder>();
;
order.BuyList = books;
order.CustomerId = customer.Id;
order.Id = i;
order.Price = books.Sum(x =>
x.Cost);
i++;
orders.Add(order);
}
return orders;
}
}
Добавляйте себе
этот класс в папку Helpers. Она для этого и предназначена в данном проекте.
Следующим любопытным моментом будет установка свойства Context для региона.
public IBook SelectedBook
{
get { return _selectedBook; }
set
{
SetProperty(ref
_selectedBook, value);
_regionManager.Regions["BookRegion"].Context = _selectedBook;
}
}
Как только мы
выбираем другую книгу, то автоматически устанавливаем данную книгу как контекст
для региона. После этого тот регион, который должен реагировать на изменение
контекста BookRegion, будет
реализовывать свою логику. Как это будет выглядеть, мы увидим в следующей модели
представления CustomerOrderViewModel. Этот класс, как мы уже помним, нужно
добавить в папку ViewModels. В этом классе мы добавим функцию UpdateOrderDetails, которая будет принимать значение книги, по которой мы хотим узнать всю
интересующую нас информацию.
public class CustomerOrderViewModel
{
#region Constructor
public CustomerOrderViewModel()
{
Orders = new ObservableCollection<IOrder>(Helpers.HelperGenerator.GenerateOrders());
Customes = new ObservableCollection<ICustomer>(Helpers.HelperGenerator.GenerateCustomers());
OrderDetails = new ObservableCollection<OrderInfo>();
}
#endregion
public ObservableCollection<IOrder> Orders { get; set; }
public ObservableCollection<ICustomer> Customes { get; set; }
public ObservableCollection<OrderInfo> OrderDetails { get; set; }
public void UpdateOrderDetails(IBook
book)
{
if(book == null)
return;
OrderDetails.Clear();
foreach (var
order in Orders)
{
var
books = order.BuyList.Where(x => x.Author == book.Author &&
x.SN == book.SN).ToList();
var
customer = Customes.First(c => c.Id == order.CustomerId);
var orderInfo = new OrderInfo
{
FirstName = customer.FirstName,
LastName = customer.LastName,
Count = books.Count,
Price = books.Sum(x =>
x.Cost)
};
OrderDetails.Add(orderInfo);
}
}
}
Логика упрощена по максимуму.
Никаких команд,
навигаций и других плюшек призма. Только та тема, которую мы рассматриваем на
протяжении данной статьи, а именно: работа с контекстом региона. Теперь нам
нужно связать наши представления с моделями представления. Тут нам приходит на
помощь понятие модулей с Prism. Для того чтобы создать новый модуль, достаточно перейти в папку Modules и создать новый класс MainModule, который необходимо
наследовать от интерфейса IModule.
public class MainModule : IModule
{
#region Variables
private readonly IRegionManager _regionManager;
private readonly IUnityContainer _container;
#endregion
#region Constructor
public MainModule(IRegionManager regionManager, IUnityContainer container)
{
_regionManager = regionManager;
_container = container;
}
#endregion
#region Public Methods
public void Initialize()
{
var bookViewModel =
_container.Resolve<BookViewModel>();
var bookView =
_container.Resolve<BookView>();
bookView.DataContext = bookViewModel;
var customerViewModel =
_container.Resolve<CustomerOrderViewModel>();
var orderView =
_container.Resolve<OrderView>();
orderView.DataContext =
customerViewModel;
var regionContext = RegionContext.GetObservableContext(bookView);
regionContext.PropertyChanged += (s, e)
=>
customerViewModel.UpdateOrderDetails(
regionContext.Value as IBook);
_regionManager.Regions["BookRegion"].Add(bookView);
_regionManager.Regions["OrderRegion"].Add(orderView);
}
#endregion
}
Поскольку свойство RegionContext изменяется и ему
присваиваются новые значения в модели представления BookViewModel, нам необходимо реагировать на эти изменения.
Для этого нужно подписаться на событие PropertyChanged в обработчик события RegionContextChanged. Основная логика происходит в этих строках:
var regionContext = RegionContext.GetObservableContext(bookView);
regionContext.PropertyChanged
+= (s, e)
=> customerViewModel.UpdateOrderDetails(
regionContext.Value as IBook);
Теперь давайте
пройдемся по тому, что же здесь происходит. Метод GetObservableContext возвращает класс ObservableObject<T>, который является оберткой над
значением RegionContext. Это свойство может
быть установлено на любое из представлений. Для этих целей даже существуют
разные behaviours, которые позволяют
задать поведение RegionContext с XAML (ссылка на MSDN). Как обычно, описание этих поведений минимальное,
и в интернете примеров нет. Поэтому если вы хотите попробовать
использовать для своего RegionContext какое-то специфическое поведение, вам нужно
будет самому набить себе шишки и наступить на все грабли впервые. Некоторые
вещи призма до сих пор очень плохо документированы. Возможно, разработчики предполагали,
что их никогда не будут использовать.
Как указано выше, для
того чтобы следить за изменением RegionContext, мы подписываемся на событие PropertyChanged и затем передаем значение с данного контекста в метод UpdateOrderDetails модели представления CustomerOrderViewModel. Теперь остался финальный шаг, чтобы
связать это все воедино, и чтобы у нас это все запустилось. Для этого нам
понадобится загрузчик, который необходимо наследовать от UnityBootstrapper. Назовем этот
загрузчик Bootstrapper и добавим его к корень
проекта.
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return Container.Resolve<Shell>();
}
protected override void InitializeShell()
{
Application.Current.MainWindow = (Window) Shell;
Application.Current.MainWindow.Show();
}
protected override void InitializeModules()
{
IModule module =
Container.Resolve<MainModule>();
module.Initialize();
}
protected override void ConfigureContainer()
{
RegisterTypeIfMissing(typeof(IBook), typeof(Book), false);
RegisterTypeIfMissing(typeof(ICustomer), typeof(Customer), false);
RegisterTypeIfMissing(typeof(IOrder), typeof(Order), false);
base.ConfigureContainer();
}
}
У него
есть множество функций. Здесь приведена лишь часть из них. Методы своим
названием отражают тот факт, что эти методы делают. Например, метод CreateShell создает главное окно, InitializeShell задает начальные параметры
запуска.
Теперь осталось
запустить сам Bootstrapper, чтобы он сделал всю
магию, которая в нем предусмотрена, и мы получили тот результат, который ожидаем. Для этого нужно перейти в класс App.xaml.cs и переопределить метод OnStartup, как показано ниже в примере.
protected override void OnStartup(StartupEventArgs e)
{
var bootstrapper = new Bootstrapper();
bootstrapper.Run();
}
После этого смело
нажимаем на кнопку Run и смотрим, что у нас получилось.
На этом, пожалуй, завершу данную статью. Надеюсь, что данное краткое введение помогло вам понять,
как использовать RegionContext для синхронизации. Буду рад за комментарии к статье, если какие-то нюансы остались непонятными, или какие темы по призму вы бы
хотели еще увидеть.
No comments:
Post a Comment