Friday, May 9, 2014

Введение в Prism 5. Работа с регионами

Здравствуйте, уважаемые читатели. Эта статья будет продолжением серии статей о таком мощном инструменте, как Prism 5. Недавно вышло обновление Prism с версии 4.1 до пятой версии. Я решил немного освежить свои знания  в этом направлении, и тут, как говорят, "понеслось". Постараюсь разобрать разработку с использованием паттерна MVVM через Prism 5 и простеньких примеров, постоянно усложняя логику работу с ними, чтобы начинающий разработчик, который пройдет через все этапы разработки с данным инструментом, понимал, как устроена логика в более сложном бизнес-проекте. В этой статье мы рассмотрим работу с регионами. 
Регионы (Regions) – это логические заполнители, определенные в пределах пользовательского интерфейса приложения (в оболочке или в представлениях), в которых выводятся на экран представления. Регионы позволяют обновлять внешний вид UI, не требуя изменений в логике приложения. В качестве регионов могут использоваться такие элементы управления, как ContentControl, ItemsControl, ListBox или TabControl, позволяя представлениям автоматически выводиться на экран в виде контента этих элементов. Представления могут быть выведены на экран в пределах региона программно или автоматически. Prism также предоставляет поддержку навигации с использованием регионов. Регионы могут быть заданы через компонент RegionManager, который использует адаптеры регионов (RegionAdapter) и поведения регионов (RegionBehavior) для координации отображения представлений в пределах определенных регионов. Для этого создадим WPF-приложение, назовем его PrismRegionSample.
Если вы посмотрите пример, который идет в поставке с Prism 5, скажем, EventAggregation_Desktop, то увидите, что в данном примере регионы сделаны в отдельных библиотеках. Это не обязательно должно быть так. На регионы можно делить в рамках одного проекта, если он небольшой. В нашем примере мы будем использовать разделения на регионы в рамках нашего проекта, не создавая для этих целей отдельные проекты.
Следующим этапом необходимо привести нашу структуру проекта, к структуре, которая позволит нам использовать паттерн MVVM. Ниже приведена структура проекта, которую предпочитаю использовать для простых MVVM сценариев с использованием регионов.
Пройдемся кратко по этой структуре:
  • Assets – стили для проекта
  • Helpers – вспомогательные классы
  • Models – хранение моделей (MVVM)
  • Modules – разбиение на модули (Prism)
  • ViewModels – для хранения моделей представлений (MVVM)
  • Views – для хранения представлений (MVVM)
  • Regions – для хранения регионов (Prism)
Возможно, вы предпочитаете другую структуру, это дело вашего вкуса. Я предлагаю базовый вариант. Базовая логика представления в Prism построена вокруг оболочки (Shell). Shell  это элементы управления UI, которые инкапсулируют пользовательский интерфейс для определенной функции или функциональной области приложения. В нашем случае в качестве оболочки выступает главное окно MainWindows.xaml. Мы можем оставить наш пример без изменений, просто переместив наше окно в папку Views, предназначенную для хранения всех UI интерфейсов. Но я предпочитаю использовать подход, который практикует Prism, и переименовать наше окно с MainWindows в Shell. Если вы смотрели примеры с Prism 5, которые идут в поставке, то вы наверняка заметили тот факт, что Shell.xaml лежит на верхнем уровне проекта.

Куда вы поместите данную оболочку,  это ваше личное предпочтение. Поэтому можете идти по такому подходу, как будет продемонстрировано в статье, либо если у вас уже есть опыт с Prism, делать себе такую структуру проекта, которая вам больше по душе. Для этого нажмите на окне MainWindows.xaml и выберите меню в Visual Studio "Rename" (в зависимости от того, какая студия у вас стоит). После этого перейдите в класс Shell.xaml.cs и поменяйте код на такой:
namespace PrismRegionSample.Views
{
       /// <summary>
       /// Interaction logic for MainWindow.xaml
       /// </summary>
       public partial class Shell : Window
       {
             public Shell()
             {
                    InitializeComponent();
             }
       }
}
включая изменение пространства имен. После чего переходим в Shell.xaml и меняем имя класса.
<Window x:Class="PrismRegionSample.Views.Shell"
Это необходимо, чтобы у нас нормально инициализировались контролы и отработал метод InitializeComponent(). Также нужно удалить запуск главного окна с App.xaml. Это строчка
StartupUri="MainWindow.xaml"
Мы будем использовать для запуска проекта UnityBootstrapper, описанный ранее. Можете собрать проект, чтобы проверить его работоспособность. После того как мы убедились в работоспособности проекта, нужно установить с помощью NuGet Packages пакет самого Prism и Prism.UnityExtensions для использования IoC контейнера Unity.
После этого добавим возможность в Shell.xaml работe с регионами. Оболочка будет иметь следующий код:
<Window x:Class="PrismRegionSample.Views.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Microsoft.Practices.Prism.Regions;assembly=Microsoft.Practices.Prism.Composition"
        Title="Shop" Height="400" Width="600">
    <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">
                        <TextBlock>Books</TextBlock>
                        <ItemsControl cal: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">
                        <TextBlock>Customers</TextBlock>
                        <ItemsControl cal:RegionManager.RegionName="CustomerRegion"  />
                    </StackPanel>
                </Border>
            </Grid>
        </Border>
    </Grid>
</Window>
Внешний вид окна в дизайнере форм приведен ниже.
Мы указали два региона. Один – для работы с книгами, который назвали BookRegion, второй – для вывода покупателей, CustomerRegion. Добавим модель для работы с книгами Book в нашу папку Models.
public class Book : BindableBase
{
       private string _author;
       public string Author
       {
             get { return _author; }
             set
             {
                    _author = value;
                    OnPropertyChanged("Author");
             }
       }

       private string _title;
       public string Title
       {
             get { return _title; }
             set
             {
                    _title = value;
                    OnPropertyChanged("Title");
             }
       }

       private DateTime _year;
       public DateTime Year
       {
             get { return _year; }
             set
             {
                    _year = value;
                    OnPropertyChanged("Year");
             }
       }

       private string _sn;
       public string SN
       {
             get { return _sn; }
             set
             {
                    _sn = value;
                    OnPropertyChanged("SN");
             }
       }

       private int _count;
       public int Count
       {
             get { return _count; }
             set
             {
                    _count = value;
                    OnPropertyChanged("Count");
             }
       }
}
Класс BindableBase с пространства имен Prism.Mvvm используется вместо NotificationObject, потому что данный класс помечен атрибутом Obsolete (что означает, что он устарел и будет исключен в ближайшее время из использования). BindableBase используется для того чтобы нам самим не нужно было реализовывать интерфейс INotifyPropertyChanged. По такому же принципу реализуем модель для клиента.
public class Customer : BindableBase
{
       private string _firstName;
       public string FirstName
       {
             get { return _firstName; }
             set
             {
                    _firstName = value;
                    OnPropertyChanged("FirstName");
             }
       }

       private string _lastName;
       public string LastName
       {
             get { return _lastName; }
             set
             {
                    _lastName = value;
                    OnPropertyChanged("LastName");
             }
       }

       private int _age;
       public int Age
       {
             get { return _age; }
             set
             {
                    _age = value;
                    OnPropertyChanged("Age");
             }
       }
}
Следующим делом нам необходимо реализовать модели представления в папке ViewModels  LibraryViewModel для книг, и CustomerViewModel – для работы с клиентами. Ниже приведен пример реализации LibraryViewModel.
public class LibraryViewModel
{
       #region Constructor
       public LibraryViewModel()
       {
             Books = new ObservableCollection<Book>();
             Books.Add(new Book { Author = "Jon Skeet", Title = "C# in Depth", Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
             Books.Add(new Book { Author = "Martin Fowler", Title = "Refactoring: Improving the Design of Existing Code", Count = 2, SN = "ISBN-10: 0201485672", Year = new DateTime(1999, 7, 8) });
             Books.Add(new Book { Author = "Jeffrey Richter", Title = "CLR via C# (Developer Reference)", Count = 5, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) });
       }
       #endregion

       #region Public Properties
       public ObservableCollection<Book> Books { get; set; }

       #endregion
}
И такую же простую реализацию нужно добавить для клиентов.
public class CustomerViewModel
{
       #region Constructor
       public CustomerViewModel()
       {
              Customers = new ObservableCollection<Customer>(new[]
                    {
                           new Customer { Age = 21, FirstName = "Filip", LastName = "Morris"},
                           new Customer { Age = 35, FirstName = "Dunkan", LastName = "Maklaud"},
                           new Customer { Age = 34, FirstName = "Nikolas", LastName = "Petrol"}
                    });
       }
       #endregion

       #region Public Properties
       public ObservableCollection<Customer> Customers { get; set; }
       #endregion
}
После проделанных действий перейдем к реализации регионов. Для этого в нашу папку добавим новый UserControl с именем BookView.xaml для отображения информации о книгах. Если у вас под рукой есть Expression Blend, то вы сможете нарисовать данный контрол на свой вкус. Я реализовал его следующим способом, используя один из стилей, который нашел на просторах инета. Исходный код данного контрола приведен ниже.
<UserControl x:Class="PrismRegionSample.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"
             Width="Auto">
    <Grid>
            <ListBox x:Name ="library" Grid.Column ="0" ItemsSource="{Binding Books}">
                <ListBox.ItemContainerStyle>
                    <Style TargetType="{x:Type ListBoxItem}">
                        <Setter Property="Background" Value="LightSteelBlue"/>
                        <Setter Property="Margin" Value="5"/>
                        <Setter Property="Padding" Value="5"/>
                        <Style.Triggers>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="Foreground" Value="White"/>
                                <Setter Property="BorderThickness" Value="1"/>
                                <Setter Property="BorderBrush" Value="Black"/>
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </ListBox.ItemContainerStyle>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <WrapPanel>
                            <TextBlock Text="Author: " />
                            <TextBlock Text="{Binding Author}" FontWeight="Bold" />
                            <TextBlock Text=", " />
                            <TextBlock Text="Caption: " />
                            <TextBlock Text="{Binding Title}" FontWeight="Bold" />
                            <TextBlock Text="Count: " />
                            <TextBlock Text="{Binding Count}" FontWeight="Bold" />
                        </WrapPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
    </Grid>
</UserControl>
Код небольшой, поэтому надеюсь, что у вас не составит проблем понять, что в нем реализовано. Затем нам необходимо в этой же папке добавить новое представление для клиентов. Это делается по аналогии с представлением для книг. Назовем наше представление CustomerView.xaml. Ниже приведена реализация данного контрола.
<UserControl x:Class="PrismRegionSample.Views.Regions.CustomerView"
             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"
             Width="Auto">
    <Grid>
        <Border Grid.Column ="0" CornerRadius="10" BorderBrush="Black" BorderThickness="1" Margin="1">
            <ListBox x:Name ="customers" 
                 ItemsSource="{Binding Customers}" Background="Transparent">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Background" Value="Transparent"/>
                    <Setter Property="Margin" Value="5"/>
                    <Setter Property="Padding" Value="5"/>
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="Foreground" Value="White"/>
                            <Setter Property="BorderThickness" Value="1"/>
                            <Setter Property="Background" Value="LightBlue" />
                            <Setter Property="BorderBrush" Value="Black"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemTemplate>
                <DataTemplate>
                        <Border Grid.Column ="0" CornerRadius="10" BorderBrush="Black" BorderThickness="1" Margin="1">
                            <WrapPanel>
                                <TextBlock Text="{Binding FirstName}" FontWeight="Bold" Width="100"/>
                                <TextBlock Text="{Binding LastName}" FontWeight="Bold" Width="100"/>
                                <TextBlock Text="{Binding Age}" FontWeight="Bold" Width="100"/>
                            </WrapPanel>
                        </Border>
                    </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        </Border>
    </Grid>
</UserControl>
Проверьте, получилась ли у вас такая структура, как на рисунке.
Весь наш каркас мы будем строить с помощью модулей (интерфейс IModule).
Модуль является логическим набором функциональных компонентов и ресурсов, собранных в одном месте. Модули могут быть разработаны, протестированы, развернуты и интегрированы в приложение по отдельности. Пакет может включать одну или более сборок, которые могут располагаться по отдельности или собираться в едином XAP файле. У каждого модуля имеется центральный класс, ответственный за инициализацию и интеграцию функциональности модуля в приложение. Этот класс реализует интерфейс IModule. Присутствия класса, реализующего интерфейс IModule, достаточно, чтобы идентифицировать пакет как модуль. У интерфейса IModule есть единственный метод Initialize, в пределах которого можно реализовать любую логику, необходимую для инициализации и интеграции функциональности модуля в приложение. В зависимости от цели модуля, он может регистрировать представления в регионах пользовательского интерфейса, делать доступными для приложения дополнительные службы или расширять его функциональность.   
Давайте построим логику модуля, который будет делать всю работу связанною со связывание модели и модели представления для наших книг (Books). Назовем новый класс BookModule и поместим его в каталог Modules. Реализация этого модуля приведена в примере ниже.
public class BookModule : IModule
{
    public BookModule(IUnityContainer container, IRegionManager regionManager)
    {
        Container = container;
        RegionManager = regionManager;
    }
    public void Initialize()
    {
           var bookViewModel = Container.Resolve<LibraryViewModel>();
           var view = Container.Resolve<BookView>();
           view.DataContext = bookViewModel;
           RegionManager.Regions["BookRegion"].Add(view);
    }

    public IUnityContainer Container { get; private set; }
    public IRegionManager RegionManager { get; private set; }
}
Наш модуль выступает связующим звеном и связывает все воедино. Основным и единственным методом интерфейса IModule есть метод Initialize(). Добавляем в наш класс конструктор с параметрами, в который передадим нужный IoC контейнер и RegionManager. В методе Initialize() мы установили наш DataContext а также указали регион, к которому добавляем наше представление. Подобная реализация модуля CustomerModule будет для связывания логики по клиентам.
public class CustomerModule : IModule
{
       public CustomerModule(IUnityContainer container, IRegionManager regionManager)
    {
        Container = container;
        RegionManager = regionManager;
    }
    public void Initialize()
    {
           var viewModel = Container.Resolve<CustomerViewModel>();
             var view = Container.Resolve<CustomerView>();
             view.DataContext = viewModel;
             RegionManager.Regions["CustomerRegion"].Add(view);
    }

    public IUnityContainer Container { get; private set; }
    public IRegionManager RegionManager { get; private set; }
}
В данном контексте эти методы практически идентичные. Вы можете по желанию сделать для них базовый класс, и реализовать все в нем. Теперь осталось реализовать последнее связующее звено, которое свяжет всю нашу логику воедино,  Bootstrapper. Как уже упоминалось ранее в этой статье, мы будем использовать UnityBootstrapper.
public class Bootstrapper : UnityBootstrapper
{
       protected override DependencyObject CreateShell()
       {
             Shell shell = Container.Resolve<Shell>();
             shell.Show();

             return shell;
       }

       protected override void InitializeModules()
       {
             IModule moduleBook = Container.Resolve<BookModule>();
             IModule moduleCustomer = Container.Resolve<CustomerModule>();

             moduleBook.Initialize();
             moduleCustomer.Initialize();
       }
}
Рекомендации. Поместите ваш класс Bootstrapper в корне проекта, не вынося в какой-либо каталог, или создайте папку, в которой будете хранить только основные классы, от которых зависит логика работы. Эту папку можно назвать Core.
В нашем классе Bootstrapper используются две функции. Функция CreateShell() создает наше главное окно (в терминах Prism это Shell (оболочка) с помощью IoC контейнера Unity. Если вы читали мою предыдущую статью "Введение в Prism 5. Boostrapper" либо работали раньше с призмом, тонаверняка знаете, что можете использовать вместо явного использования контенера Unity, паттерн Service Locator (класс ServiceLocator), с помощью которого вы сможете не привязываться жёстко к конкретному IoC контейнеру. Правда, как показывает практика в крупном проекте, если обусловлен переход с одного IoC контейнера на другой, использование ServiceLocator не спасает. Поэтому, повторюсь, исходите из ваших предпочтений. Вторая функция InitializeModules() инициирует инициализацию и связывание модулей. Посмотрите на структуру проекта, которая у нас должна получиться, и убедитесь в том, что она такая же, как у вас.
Ваш проект должен как минимум нормально собраться. Теперь осталось при старте проекта создать объект нашего класса Bootstrapper и посмотреть, что у нас в итоге получилось. Для этого перейдем в класс App.xam.cs и переопределим метод OnStartup следующим образом:
public partial class App : Application
{
       protected override void OnStartup(StartupEventArgs e)
       {
             var bootstrapper = new Bootstrapper();
             bootstrapper.Run();
       }
}
После этого пробуем запустить наш проект, чтобы посмотреть на результат. Должно получиться что-то вроде этого:

Итоги
В этой статье мы рассмотрели возможность использования регионов в своих приложениях, а также ознакомились с разделением на модули с помощью Prism 5. Плюсы использования компонентов Prism– в том, что в его использовании нет ничего сложного. Возможно, несколько сложнее, чем использовать готовый тулкит для MVVM. Но есть один нюанс, который мне очень не по душе. Это время запуска приложения. На моем компьютере с Intel Core I5 – 3330 CPU, 3 Gz, с оперативной памятью 8 Gb и с 64-разрядной Windows 7 проект собирался и запускался порядка 15-20 секунд. То же самое было заметно на Windows 8.1 с процессором от AMD на 4 ядра по 3 Gz. Собранный проект запускается довольно шустро. Но сборка тяжеловато проходит для столь немного компонентов. Надеюсь, что данный обзор позволит вам преодолеть страх перед использованием Prism 5.

No comments:

Post a Comment