Wednesday, December 3, 2014

View Switching Navigation in Prism 5

Здравствуйте, уважаемые читатели моего блога. Сегодня мы продолжим ознакомление с библиотекой Prism 5, которая помогает нам создавать надежные, с богатым интерфейсом системы, используя возможности декларативного языка разметки XAML в WPF/Silverlight приложениях. 
В этой статье мы рассмотрим, как по мне, самую тяжелую тему с которой можно столкнуться при изучении библиотеки Prism 5. Это навигация на основе представлений (View-Based Navigation). Чтобы сильно вас не пугать, расскажу, почему эта тема такая сложная при изучении Prism. Навигация на основе представлений базируется на том, что вы можете подменять одно представление на другое. Если вы вспомните, как работает еще один вид анимации на основе состояний (State-Based Navigation), то увидите огромную разницу. Навигация на основе состояний позволяет нам подменять внешний вид одного и того же представления с помощью стилей, а также используя возможности класса VisualStateManager. По сути, основная работа в этом стиле анимации лежит на дизайнере и в разработке красивых эффектов при переходе и отображении представления. Если вы не сталкивались с навигацией на основе состояний, то можете посмотреть мою предыдущую статью об этом: "Введение в Prism 5. Навигация на основе состояний (State-Based Navigation)"
Теперь давайте вернемся к нашей навигации на основе представлений. Задача спроектировать грамотный переход с одного представления на другое – сама по себе не так уж проста. А в приложениях, которые уходят на внешней рынок, это вообще одна из самых трудных задач касательно архитектуры приложения. Поэтому если вы начинающий разработчик, то вероятность того, что вам нужно будет продумывать такой тип навигации, небольшая. Но если вы понимаете, что ваша задача – развиваться как разработчик и уметь проектировать сложное построение систем, надеюсь, что данная тема будет полезна. Для того чтобы продемонстрировать, как работает навигация, основанная на представлениях, в поставке с библиотекой Prism 5 идет проект View-Switching Navigation.
Если вы запустите данный проект, то сможете увидеть на экране результат, который приведен на рисунке ниже.
Пример, который приводят для ознакомления разработчики с patterns & practices, не такой простой для понимания. Нужно потратить прилично времени чтобы понять, что и как в нем работает. Мы же сделаем самый простой и примитивный вариант, чтобы понять базовую логику работы навигации. Зачастую навигация на основе представлений непосредственно связана с использованием регионов. Отображаемое представление в пределах региона идентифицируется с помощью Uri, который по умолчанию ссылается на имя представления, к которому осуществляется навигация. Использовать навигацию можно программно, используя метод RequestNavigate с интерфейса INavigateAsync. Ниже приведена реализация данного интерфейса.
public interface INavigateAsync
{
    void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback);

    void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters);
}
Теперь поговорим немного о данном классе более детально, так как он претерпел изменений, по сравнению с версией Prism 4.1. Одно из изменений – это переименование класса UriQuery с Prism 4.1 в класс NavigationParameters и перемещение в пространство имен Regions. Этот факт приведен для общего развития, так как эти изменения относятся к разряду ключевых изменений пятой версии Prism. Также есть небольшое дополнение, которое описано также в документации к Prism. Несмотря на то что в названии интерфейса INavigateAsync присутствует приставка Async (а если вспомнить, то к терминологии .Net приставка async добавляется к тем конструкциям, которые могут работать в асинхронном режиме), интерфейс не работает в асинхронном режиме.  Вот выписка об этом с официальной документации:
Несмотря на название, интерфейс INavigateAsync не подразумевает асинхронную навигацию, выполняемую в отдельном потоке. Наоборот, INavigateAsync подразумевает проведение псевдо-асинхронной навигации. Метод RequestNavigate может завершиться синхронно, после окончания навигации, или он может завершиться до окончания навигации, например, когда пользователю нужно подтвердить навигацию. Позволяя вам задавать метод обратного вызова во время навигации, Prism даёт возможность поддержки таких сценариев без сложностей обращения с фоновыми потоками.
Интерфейс INavigateAsync реализует также класс Region, позволяя инициировать навигацию в этом регионе. Есть небольшое примечание, которое касается регистрации представления для навигации.
Имя, используемое для регистрации и навигации, не обязательно должно быть связано с именем типа представления, подойдёт любая строка. Для примера, вместо строки, вы можете явно использовать полное имя типа:typeof(InboxView).FullName;
Во всех описаниях, которые касаются регистрации, связывания и т.д., которые подразумевают использования какого-то загрузчика, я предпочитаю использовать UnityBootstrapper с IoC контейнером Unity, вместо MEF. Поэтому все описание, которое касается загрузчика и тому подобного, можно сразу проецировать на использование Unity. На навигации остановимся более детально, поскольку там имеется довольно большое количество разных интерфейсов, а затем приступим к реализации.
Для примера рассмотрим самый простой вариант, в котором модель представления не принимает участия. Затем рассмотрим более сложные сценарии, когда модель представления принимает в этом всем непосредственное участие. Давайте создадим новый WPF проект, указав при создании в качестве фреймворка минимум фреймворк 4.5 и укажем следующее имя для нашего проекта: “SimplePrismNavigation”.
Для примера я выбрал самый последний фреймворк, который стоял у меня версии 4.5.1. Минимальный фреймворк, на котором вы можете попробовать возможности Prism 5, – это .NET Framework 4.5.
Затем наша задача – загрузить с помощью Manager NuGet Packages библиотеки призма себе в проект. Также сразу загрузите сборку Prism.UnityExtensions для работы с загрузчиком Prism, который использует для этих целей IoC контейнер Unity.
Затем в нашем проекте создаем следующую структуру каталогов:
Я постараюсь ее откомментировать по минимуму, поскольку тема с навигацией очень объемная, и нужно очень много еще описать. Данная структура позволяет хранить данные в вашем проекте, используя термины MVVM и Prism. Например, директории Regions хранит контролы, стили, окна, которые используются как заполнители регионов, Modules – разделение на модули с использованием Prism 5 (для этого используется интерфейс IModule с библиотеки Prism, поэтому этот термин специфический для Prism). Так как для всех проектов, которые пишу на Prism, поддерживаюсь терминологии, которая присутствует в нем, мы переименуем наше главное окно MainWindow.xaml  на Shell.xaml и переместим в папку Views. Затем в классе App.xaml удалим StartupUri, так как мы будем использовать в качестве загрузчика UnityBootstrapper. Посмотрите, чтобы у вас в коде не было названий MainWindow, а было вместо них именование Shell. В Prism основное окно приложения называется оболочкой (Shell), поэтому используем такое название. Папки Models/ViewModels/Views представляют собой структуру для хранения данных паттерна MVVM.
  • Views – представления
  • ViewModels – модели представления
  • Models – модели
Теперь приступим к реализации. Пример мы возьмем тот же, который использовали в статье "Динамическая загрузка View в Prism 5". Только чуточку переделаем его под наши нужды. Для начала просто в наше главное окно Shell.xaml добавим регион, в который будем выводить наши представления.
<Window x:Class="SimplePrismNavigation.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>
        <ContentControl regions:RegionManager.RegionName="MainRegion" />
    </Grid>
</Window>
Представления будут немного сложнее. Для этого перейдем в папку Regions и создадим новый UserControl с именем FirstView. Xaml разметка данного контрола приведена ниже.
<UserControl x:Class="SimplePrismNavigation.Views.Regions.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>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
      <RowDefinition Height="50"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <Border Background="Gold" Margin="2" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0">
      <StackPanel Orientation="Horizontal">
        <Button Content="Button1" Margin="3,2" Command="{Binding ButtonCommand}"/>
        <Button Content="Button2" Margin="3,2"/>
      </StackPanel>
    </Border>

    <Grid Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Background="ForestGreen">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <ComboBox Grid.Column="0" />
    </Grid>

    <StackPanel Grid.Column="0"  Grid.Row="2" Background="DodgerBlue" Margin="5,3"/>
    <StackPanel Grid.Column="1"  Grid.Row="2" Background="DodgerBlue" Margin="5,3"/>

    <StackPanel Grid.Column="0"  Grid.Row="3" Grid.ColumnSpan="2" Background="Firebrick" Margin="5,10"/>
  </Grid>

</UserControl>
Второй контрол добавляем тоже в папку Regions и называем его SecondView.
<UserControl x:Class="SimplePrismNavigation.Views.Regions.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>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <Grid Grid.Row="0" Background="ForestGreen">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <ComboBox Grid.Column="0" />
    </Grid>

    <StackPanel Grid.Column="0"  Grid.Row="1" Background="DodgerBlue" Margin="5,3"/>

    <Border Background="Gold" Margin="2" Grid.Row="1" Height="25" HorizontalAlignment="Left" VerticalAlignment="Top">
      <StackPanel Orientation="Horizontal">
        <Button Content="Button1" Margin="3,2"/>
        <Button Content="Button2" Margin="3,2" Command="{Binding ButtonCommand}"/>
        <Button Content="Button3" Margin="3,2"/>
      </StackPanel>
    </Border>
  </Grid>

</UserControl>
Я сразу добавил команду для одной кнопки. Теперь осталось добавить для каждого представления свою модель представления (ViewModel). Класс, который будет реализовывать логику для представления FirstView, назовем FirstViewModel.
public class FirstViewModel : BindableBase
{
    #region Private Variables
    private readonly IRegionManager _regionManager;
    #endregion

    #region Constructor
    public FirstViewModel(IRegionManager regionMananger)
    {
        _regionManager = regionMananger;
        ButtonCommand = new DelegateCommand(ShowSecondView);
    }
    #endregion

    #region Commands
    public DelegateCommand ButtonCommand { get; private set; }

    #endregion

    #region Private Methods
    private void ShowSecondView()
    {
        _regionManager.RequestNavigate("MainRegion", new Uri("SecondView", UriKind.Relative));
    }

    #endregion
}
И для второго представления реализовываем аналогично SecondViewModel.
public class SecondViewModel : BindableBase
{
    #region Private Variables
    private readonly IRegionManager _regionManager;
    #endregion

    #region Constructor
    public SecondViewModel(IRegionManager regionMananger)
    {
        _regionManager = regionMananger;
        ButtonCommand = new DelegateCommand(ShowFirstView);
    }
    #endregion

    #region Commands
    public DelegateCommand ButtonCommand { get; private set; }

    #endregion

    #region Private Methods
    private void ShowFirstView()
    {
        _regionManager.RequestNavigate("MainRegion", new Uri("FirstView", UriKind.Relative));
    }
    #endregion
}
Если мы посмотрим внимательно на метод ShowFirstView и ShowSecondView в созданных выше моделях представления, то сможем увидеть пример использования навигации в регионах. Чтобы это заработало, нам необходимо помнить об одной важной особенности. Дело в том, что при создании представления навигационным сервисом, он запрашивает объект типа Object из контейнера с именем, предоставленным в навигационном URI. Для того чтобы это заработало в Unity, нам нужно зарегистрировать наши представления следующим образом:
Container.RegisterType<object, SecondView>("SecondView");
Container.RegisterType<object, FirstView>("FirstView");
А теперь осталась основная задача: где это все зарегистрировать. У нас также есть проблема в том, что при создании представления у нас модель представления не будет автоматически привязана к представлению. Чтобы все-таки связать модель представления с представлением, нам нужно установить для конкретного представления свойство DataContext как модель представления, которая будет отвечать за обработку команд пользователя. Я предпочитаю для этих целей использовать один из простеньких вариантов: указать для конкретной модели представления атрибут Dependency. Посмотрим, как это будет выглядеть. Для этого перейдем в класс FirstView.xaml.cs и чуточку его подправим.
/// <summary>
/// Interaction logic for FirstView.xaml
/// </summary>
public partial class FirstView : UserControl
{
    public FirstView()
    {
        InitializeComponent();
    }

    [Dependency]
    public FirstViewModel ViewModel
    {
        set { this.DataContext = value; }
    }
}
Есть конечно другой способ проделать то же самое через property injection. Но как по мне, то лучше уж такой способ, чем конфигурировать наш IoC контейнер с использованием property injection. Теперь аналогичным образом изменим класс SecondView.xaml.cs.
/// <summary>
/// Interaction logic for SecondView.xaml
/// </summary>
public partial class SecondView : UserControl
{
    public SecondView()
    {
        InitializeComponent();
    }

    [Dependency]
    public SecondViewModel ViewModel
    {
        set { this.DataContext = value; }
    }
}
Теперь нам нужно написать модуль, который свяжет при старте приложения наше представление FirstView с регионом MainRegion. Для этого перейдем в папку Modules и добавим новый класс с названием MainModules.
В нем мы просто переопределим один метод Initialize(). Этот класс очень простой, и если вы хоть минимум работали с IoC контейнером и регионами, то для вас ничего нового здесь не будет.
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()
    {
        var view = _container.Resolve<FirstView>();
        _regionManager.Regions["MainRegion"].Add(view);
    }
}
Осталась самая малость: написать свой загрузчик, который будет все инициализировать и запускать, и вызвать этот загрузчик в App.xaml.cs в методе OnStartup. Добавим в наш проект новый класс Bootstrapper,/ который унаследуем от класса UnityBootstrapper (который работает с использованием IoC контейнера Unity). Большой интерес, наверное, в этом классе представляет метод ConfigureContainer, который регистрирует наши представления, для того чтобы их мог использовать навигационный сервис.
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()
    {
        var module = Container.Resolve<MainModule>();
        module.Initialize();
    }

    protected override void ConfigureContainer()
    {
        Container.RegisterType<object, SecondView>("SecondView");
        Container.RegisterType<object, FirstView>("FirstView");

        base.ConfigureContainer();
    }
}
Ну и последний штрих – переопределить метод OnStartup в App.xaml.cs следующим образом:
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        var bootstrapper = new Bootstrapper();
        bootstrapper.Run();
    }
}
После того как мы проделали эти действия, аккуратно нажимаем на кнопочку Run, для того чтобы запустить проект J.  Результат того, что получилось, вы можете увидеть на рисунке ниже.

На этом, пожалуй, завершу свой рассказ об использовании навигационного сервиса в Prism 5. Следующую статью мы продолжим с рассмотрения данного сервиса, с этим же примером, но для более сложного сценария, в котором наши модели представления будут принимать непосредственное участие. Буду рад ответить на ваши вопросы в комментариях. Теперь вы как минимум знаете еще один пример использования динамической загрузки регионов в Prism и навигацию в них. 

No comments:

Post a Comment