Saturday, November 22, 2014

View Discovery и View Injection в Prism 5

Сегодня спустя столь долгого перерыва в пять месяцев я решил снова вернуться к статьям по Prism 5, так как вижу, что эта тема интересна для читателей, один из которых недавно поделился проблемами, которые он пытался решить с помощью призма. Интересные моменты, которые, возможно, пригодятся вам для решения подобных проблем, мы рассмотрим сегодня в статье, а также в следующих статьях по призму. Надеюсь, что тема будет нескучная и полезная. А мне достаточно того факта, что я поборол свою лень и апатию и смог наконец-то вернуться к написанию статей по программированию. Поэтому сегодня мы будем играться с регионами в Prism. Мне почему-то казалось, что использование регионов не должно вызвать сложностей, но, оказывается, здесь действительно есть много нюансов, которые сложны для понимания. 
Мы рассмотрим два способа компоновки Views (представлений): с помощью View Discovery и View Injection. Если вы только начинаете свое знакомство с регионами, то рекомендую статью "Введение в Prism 5. Работа с регионами", в которой на простом примере продемонстрировано использование регионов. Сегодня также каждую тему, связанную с регионами, мы будем демонстрировать на практике. Поэтому рекомендую сразу сделать себе заготовку, в которой мы последовательно будем разбирать каждый отдельный этап. Поехали. 
Как и в предыдущих статьях, я использую последний Prism 5, поэтому если вы работаете с версией 4.1, то вам, возможно, придется слегка модифицировать ваш код. Но в целом мы не будем использовать много разных моделей (Model) и моделей представлений (ViewModel) с классического паттерна MVVM, а будем в основном обходиться только представлениями (View). Для демонстрации работы регионов этого будет достаточно. Давайте создадим новый WPF проект с выбором фреймворка 4.5. Назовем наш проект "RegionViewerSample".
Дальше, как обычно, нужно создать структуру для нашего проекта. Поэтому создаем такую структуру папок:
Эта структура, как несложно догадаться, реализует классическую схему паттерна MVVM, где в папку Models мы складываем модели, в ViewModels – модели представления, и в папку Views – сами представления (внешний вид контролов, окна и т.д.). Две папки добавлены уже специально под логику призма. Modules – работа с модулями, Regions UI контролы для заполнения регионов. Ничего сложного на этом этапе не должно возникнуть. 
Так как я использую Prism, то стараюсь оперировать его понятиями, а в призме основное окно называется оболочкой (Shell). Поэтому первое, что я сделаю, – это переименую мой класс MainWindow.xaml в Shell.xaml. Затем перенесу это класс в папку Views. Затем уберу с App.xaml метод StartupUri для запуска главного окна. После этого нужно создать загрузчик, который будет создавать и инициализировать наше главное окно, а также, если это необходимо, устанавливать все начальные параметры. 
Я использую для таких случаев в 90% загрузчик UnityBootstrapper. Скачивать его нужно отдельно, как и саму библиотеку Prism. Сделать это можно с помощью Manager NuGet Packages. Для этого открываем Manager NuGet Packages, вводим слово Prism и устанавливаем библиотеки призма, которые идут самые первые.
Затем на второй странице также устанавливаем Prism.UnityExtensions, который и содержит загрузчик UnityBootstrapper. Этот загрузчик использует IoC контейнер Unity для управления зависимостями. Этот IoC контейнер мне нравится своими возможностями, поэтому предпочитаю использовать именно его. Затем переходим к созданию класса загрузчика. Назовем этот класс просто: 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();
    } 
}
Не забывайте, что мы уже переименовали с MainWindow.xaml на Shell.xaml и все поправили в соответствии с этим. Затем нам нужно завершить финальную стадию подготовки нашего проекта. Для этого нужно перейти в класс App.xaml.cs и переопределить метод OnStartup на приведенный ниже. 
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        var bootstrapper = new Bootstrapper();
        bootstrapper.Run();
    }
}
После этого ваш проект должен успешно собраться и запуститься, показав главное пустое окно. Если у вас что-то не получилось, не расстраивайтесь. Просто посмотрите вот на эту статью, в которой эти шаги описаны более детально: "Введение в Prism 5. Bootstrapper". Со временем на подготовку проекта у вас будет уходить очень немного времени. Или вы можете создать свой темплейт, который будете использовать в дальнейшем в своих проектах. Например, по аналогии, как это сделано в MVVM Light. У вас подготовка проекта для работы с призмом займет считанные минуты.
А теперь вернемся к нашим регионам и рассмотрим, как с ними работать. Сначала немного теории, но лишь чуть-чуть. Просто для того, чтобы понимать суть происходящего и иметь представление о том, почему было сделано так, а не иначе. С регионами мы можем работать с помощью менеджера регионов, а также адаптеров регионов. Что первое, что второе – довольно сложная тема, мы ее рассмотрим только поверхностно. Постараюсь детальнее к ней вернуться в следующих статьях. Вернемся для начала к менеджеру регионов (RegionManager). Это класс, который отвечает за то, чтобы создать регионы и связать их с элементами управления. Ниже показан рисунок, взятый с документации, который демонстрирует работу класса RegionManager.
С помощью RegionManager мы можем создавать регионы как в xaml-разметке, так и в коде. Приложение может содержать один или несколько экземпляров RegionManager. Есть возможность выбирать, в каком экземпляре менеджера регионов необходимо зарегистрировать регион. Это может оказаться полезным, если вы хотите передвинуть элемент управления в визуальном дереве, но не хотите, чтобы регион очистился после очищения присоединённого свойства.
RegionManager предоставляет присоединённое свойство RegionContext, с помощью которого можно обмениваться данными между регионами. У меня недавно спрашивали, как передать данные в другой регион для обмена. Вот один из ответов. Например, через RegionContext, хотя я больше предпочитаю белее сложное управление через навигацию на основе моделей. 
Мы так много говорим о регионах; пора бы уже, наверное, подойти вплотную к определению этого понятия. В призме регионом называется класс, который реализует интерфейс IRegion. По сути, это контейнер, в котором содержится динамическое содержимое пользовательского интерфейса. Еще мы упомянули о таком понятии, как адаптеры регионов. Так вот: для того чтобы подставить элемент в регион, он должен иметь необходимый адаптер. Каждый адаптер регионов адаптирует определённый тип элементов управления. Prism предоставляет следующие адаптеры:
  • ContentControlRegionAdapter. Он адаптирует ContentControl и его наследники.
  • SelectorRegionAdapter адаптирует Selector и его наследники, такие как TabControl.
  • ItemsControlRegionAdapter адаптирует ItemsControl и его наследники.
Давайте рассмотрим использование регионов на простеньком примере. Для этого откроем редактор нашей оболочки Shell.xaml и добавим следующий код.
Window x:Class="RegionViewerSample.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">
    <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>
   
      <ItemsControl regions:RegionManager.RegionName="ComboBoxRegion" Grid.Row="0"/>
      <ItemsControl regions:RegionManager.RegionName="GroupBoxRegion" Grid.Row="1"/>
      <ContentControl regions:RegionManager.RegionName="CompositeRegion"  Grid.Row="2"/>
    </Grid>
</Window>
Если мы выполним наш код, то ничего, к сожалению, не увидим на экране, кроме главного окна. Это потому что мы не указали, какие представления мы должны отображать в данных регионах. Для этого нам понадобится использовать класс RegionManager и один из способов композиции представлений. Композицией представлений является способ создания представлений. Представления могут быть созданы как автоматически, через обнаружение представлений (View Discovery), так и программно, через внедрение представлений (View Injection). Мы рассмотрим сначала первый способ с обнаружением представлений. При этом подходе необходимо создать отношение между именем региона и представлением через класс. RegionViewRegistry, используя метод RegisterViewWithRegion. Но можно пойти проще, так как для класса RegionManager написано куча методов расширения, то у нас есть метод такой же метод для интерфейса IRegionManager (ну и, понятное дело, что этот метод реализует внутри себя класс RegionViewRegistry).
А если мы заглянем в исходники библиотеки и откроем пространство имен Regions, то можем увидеть, сколько логики и невиданных дебрей для нас припасено.
Я и сам не знаю большей части из этого, так как не было возможности все использовать из-за специфики использования призма. Теперь давайте перейдем в папочку Regions в нашем проекте и добавим туда несколько UserControl, которые будут заполнять наши регионы. Первым добавим контрол ComboBoxView.xaml. Реализация этого контрола приведена ниже.
<UserControl x:Class="RegionViewerSample.Views.Regions.ComboBoxView"
             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>
        <ComboBoxItem Content="Test1"/>
        <ComboBoxItem Content="Test2"/>
        <ComboBoxItem Content="Test3"/>
      </ComboBox>
    </Grid>
</UserControl>
Реализация его очень примитивная. Обычный комбобокс с трема пунктами выбора. Следующим создадим контрол GroupBoxView.xaml.
<UserControl x:Class="RegionViewerSample.Views.Regions.GroupBoxView"
             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>
      <GroupBox>
        <StackPanel Orientation="Vertical">
          <RadioButton Content="1" />
          <RadioButton Content="2" />
          <RadioButton Content="3" />
          <RadioButton Content="4" />
          <RadioButton Content="5" />
        </StackPanel>
      </GroupBox>
    </Grid>
</UserControl>
Ну и напоследок создадим контрол, который внутри себя будет содержать еще дочерние регионы CompositeView.xaml.
<UserControl x:Class="RegionViewerSample.Views.Regions.CompositeView"
             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"
             xmlns:regions="http://www.codeplex.com/CompositeWPF"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
      <Border Background="DarkSeaGreen" Margin="5">
        <Grid>
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>
          <ContentControl regions:RegionManager.RegionName="RegionOne" Grid.Column="0"/>
          <ContentControl regions:RegionManager.RegionName="RegionTwo" Grid.Column="1"/>
        </Grid>
      </Border
    </Grid>
</UserControl>
Теперь давайте это все свяжем с помощью модулей в призме. Для этого перейдем в папку Modules и создадим класс MainModule. Реализация этого класса приведена ниже.
public class MainModule : IModule
{
    private readonly IRegionManager _regionManager;

    public MainModule(IRegionManager regionManager)
    {
        _regionManager = regionManager;
    }

    public void Initialize()
    {
        _regionManager.RegisterViewWithRegion("ComboBoxRegion",
                typeof(ComboBoxView));
        _regionManager.RegisterViewWithRegion("GroupBoxRegion",
                typeof(GroupBoxView));
        _regionManager.RegisterViewWithRegion("CompositeRegion",
                typeof(CompositeView));
        _regionManager.RegisterViewWithRegion("RegionOne",
                typeof(ComboBoxView));
        _regionManager.RegisterViewWithRegion("RegionTwo",
                typeof(GroupBoxView));
    }
}
Код, как мне кажется, очень прост для понимания. Мы указываем, какой регион какой контрол будет отображать. Затем нам нужно инициализировать этот модуль в нашем загрузчике (класс Bootstrapper который мы добавили раньше). Для этого нужно переопределить метод InitializeModules().
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 mainModule = Container.Resolve<MainModule>();
        mainModule.Initialize();
    }
}
После этого мы можем запустить проект на экране и увидеть результат того, что у нас получилось.
Вид этого, конечно, не товарный. Но наша задача – научиться пока это использовать, а не рисовать красивый интерфейс в xaml. А теперь давайте в этом же примере вместо View Discovery воспользуемся внедрением представлений (View Injection). В этом случае ваш код получает ссылку на регион, и затем программно добавляет представление в него. Обычно это делается во время загрузки модуля или в ответ на действие пользователя. Ваш код должен запросить RegionManager для необходимого региона по его имени, после чего внедрить в него представление. Так вы имеете гораздо больший контроль над тем, как представления создаются и подставляются в регионы. Этот код намного лучше подходит для динамичной компоновки регионов. Но для наше примера он тоже неплохо подойдет. Для этого перейдем в класс MainModule и модифицируем его следующим образом:

public class MainModule : IModule
{
    private readonly IRegionManager _regionManager;
    public readonly IUnityContainer _container;

    public MainModule(IUnityContainer container, IRegionManager regionManager)
    {
        _container = container;
        _regionManager = regionManager;
    }

    public void Initialize()
    {
        _regionManager.Regions["ComboBoxRegion"].Add(_container.Resolve<ComboBoxView>());
        _regionManager.Regions["GroupBoxRegion"].Add(_container.Resolve<GroupBoxView>());
        _regionManager.Regions["CompositeRegion"].Add(_container.Resolve<CompositeView>());
        _regionManager.Regions["RegionOne"].Add(_container.Resolve<ComboBoxView>());
        _regionManager.Regions["RegionTwo"].Add(_container.Resolve<GroupBoxView>());
    }
}

Дело в том, что зачастую вам придётся использовать View Injectionтак как такой подход внедрения представлений более гибкий и позволяет вам управлять изменением содержимого региона самим. На этом закончу свою небольшую статью по работе с регионами и управлению в них представлениями. В следующей статье постараемся рассмотреть еще что-то интересное по Prism 5.

No comments:

Post a Comment