Saturday, December 27, 2014

Подтверждение навигации в Prism 5

Сегодняшняя тема снова посвящена навигации в Prism, поскольку в предыдущих статьях "View Switching Navigation in Prism 5" и "Using navigation in Prism" эта тема раскрыта не полностью. Сегодня мы еще больше углубимся в тему навигации и рассмотрим, для чего нужен журнал навигации в Prism и как сделать такое поведение, в котором для нашей модели представления нужно делать подтверждение или отмену действий. Примером такого представления может быть форма заказа, которую нужно подтвердить или отменить, данные о пациенте, данные о покупке и т.д. Если вы опытный программист и умеете быстро вникать в код, возможно, вам будет лучше ознакомиться с примером использования призма, который называется Open QS - View-Switching Navigation.
В нем есть все, что будет рассмотрено в данном примере. Вы можете запустить этот пример и посмотреть его работу.
Сделан этот пример очень неплохо. И если есть знания по использованию объектов взаимодействия в Prism 5 (классы Confirmation, Notification и другие), то вашей задачей будет только разобраться с журналом событий. Если же вы не смотрели этот пример и хотите что-то попроще, для того чтобы понять эту область, или пример команды patterns&practices для вас показался слишком сложным, тогда эта статья, надеюсь, вам поможет. Для начала немного теории. Если пользователю необходимо подтвердить или отменить навигации, на помощь приходит интерфейс IConfirmNavigationRequest. Этот интерфейс наследован от интерфейса INavigationAware, который мы рассматривали в предыдущей статье "Using navigation in Prism", и в нем добавлен всего один новый метод ConfirmNavigationRequest.
// Summary:
//     Provides a way for objects involved in navigation to determine if a navigation
//     request should continue.
public interface IConfirmNavigationRequest : INavigationAware
{
    // Summary:
    //     Determines whether this instance accepts being navigated away from.
    //
    // Parameters:
    //   navigationContext:
    //     The navigation context.
    //
    //   continuationCallback:
    //     The callback to indicate when navigation can proceed.
    //
    // Remarks:
    //     Implementors of this method do not need to invoke the callback before this
    //     method is completed, but they must ensure the callback is eventually invoked.
    void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback);
}
Благодаря использованию этого интерфейса пользователь может как отменить, так и подтвердить навигацию. Если для этого нужно окно подтверждения, то вам понадобится объект Interaction Request, ремарка на который есть вначале статьи. Метод ConfirmNavigationRequest принимает первым параметром ссылку на навигационный контекст, а вторым − делегат, который нужно вызвать для продолжения навигации. Вы можете сохранить ссылку на делегат-продолжение, для его вызова после окончания взаимодействия с пользователем. Если приложение взаимодействует с пользователем через объекты Interaction Request, вы можете использовать этот делегат в качестве метода обратного вызова запроса взаимодействия. На картинке ниже показан полный цикл навигации с подтверждением или отменой, взятой с документации Prism по ссылке 8: Navigation Using the Prism Library 5.0 for WPF.
Следующие шаги описывают процесс подтверждения навигации при использовании объекта InteractionRequest.
  1. Навигация инициируется через вызов метода RequestNavigate.
  2. Если представление или модель представления реализуют интерфейс IConfirmNavigation, то вызывается метод ConfirmNavigationRequest.
  3. Модель представления вызывает событие начала взаимодействия (interaction request event).
  4. Представление отображает всплывающее окно с подтверждением и ждёт ответа от пользователя.
  5. Метод обратного вызова запроса взаимодействия вызывается после того, как пользователь закрывает всплывающее окно.
  6. Делегат-продолжение вызывается для продолжения или отмены прерванной операции навигации.
  7. Операция навигации завершается или отменяется.
Описание выше взято с переведенной документации на habrahabr. В принципе вы можете сами ознакомиться с оригиналом по ссылке, с которой я скопировал картинку в статью. В примере мы также будем использовать журнал навигации (Navigation Journal). Благодаря этому журналу мы можем делать перемещение в рамках нашего региона наподобие визарда. Для этого у нас должен быть регион, ответственный за координирование навигации.  Этот журнал доступен с NavigationContext. Как его использовать, рассмотрим по ходу написания примера. Пример, который я приготовил для статьи, занял у меня около двух дней. Вся проблема была в том, что я хотел построить навигацию в рамках одного региона, не используя для этих целей отдельный регион для навигации. Я убил много времени, и это мне так и не удалось. А учитывая тот факт, что примеров в интернете, где можно посмотреть, что ты делаешь не так, как кот наплакал, я с горем пополам доделал пример и поделюсь с вами, какие подводные камни могут встретиться на пути.
Для начала зайдем в Visual Studio и выберем новый WPF проект, указав что мы будем использовать фреймворк 4.5. Дадим название этому проекту TestConfirmNavigation.
Следующим делом нам нужно с помощью NuGet Package Manager установить Prism и Prism.UnityExtensions, для того чтобы мы могли использовать в качестве загрузчика UnityBootstrapper и IoC контейнер Unity. Структура проекта приведена ниже.
Краткое описание этой структуры:
  • Convertors – для данного примера сюда я вынес конверторы, которые используются в xaml-коде;
  • Helpers – вспомогательные классы и некоторые классы бизнес-логики, поскольку их немного, и выносить их в отдельную библиотеку не было особого желания;
  • Models – папка для хранения моделей;
  • Modules – модули в Prism 5 (разбиение проекта на модули с помощью интерфейса IModule);
  • ViewModels – модели представления;
  • Views – представления (контроли, окна и т.д.);
  • Regions – контролы, которые используются для заполнения регионов.
Начнем, пожалуй, с самого простого. На мой взгляд, это IBook, который отображает информацию о книге. Для этого переходим в папку Models и добавляем этот интерфейс.
public interface IBook
{
    int Id { get; set; }
    string Author { get; set; }
    string Title { get; set; }
    DateTime Year { get; set; }
    string SN { get; set; }
    int Count { get; set; }
    double Cost { get; set; }
}
Имплементация данного интерфейса приведена в классе Book. Этот класс также нужно добавить в папку Models.
public class Book : BindableBase, IBook
{
    private int _id;
    public int Id
    {
        get { return _id; }
        set { SetProperty(ref _id, value); }
    }

    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);
        }
    }
}
Больше у нас моделей нет. В отличие от предыдущих примеров по навигации, которые вы можете найти в моем блоге, по количеству моделей это самый маленький пример.
Теперь начнем с реализации вспомогательных классов, которые принимают участие в нашем проекте. Эти классы мы будем добавлять в папку Helpers. Начнем с класса RegionNames, который просто выступает как класс, в котором хранятся имена регионов в виде констант.
public class RegionNames
{
    public const String NavigationRegion = "NavigationRegion";
    public const String ContentRegion = "ContentRegion";
}
Следующим классом будет класс HelperGenerator. Этот класс сгенерирует тестовые данные для демонстрации. Поэтому в нем также не будет какой-то сверхординарной логики.
public static class HelperGenerator
{
    public static List<IBook> GenerateBooks()
    {
        var cshapInDepth = ServiceLocator.Current.GetInstance<IBook>();
        cshapInDepth.Id = 1;
        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.Id = 2;
        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.Id = 3;
        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
        };
    }
}
Затем реализуем классы, которые отвечают за бизнес-логику. Для более крупного проекта такую логику рекомендуется выносить в отдельную сборку, которая отвечает за бизнес-логику, потому что эта логика как раз таковой и является. Начнем, пожалуй, с интерфейса IBookService. Этот интерфейс позволяет нам осуществить доступ к книгам и получать информацию о них в асинхронном и синхронном режиме.
public interface IBookService
{
    Task<List<IBook>> GetBooks();
    Task<IBook> UpdateBook(IBook book);
    IBook GetBookById(int id);

    event Action<IBook> UpdateBookCompleted;
}
В ваших проектах это может быть паттерн репозиторий, наблюдатель, классы или интерфейсы, отвечающие за бизнес-логику. Имплементация этого интерфейса приведена ниже в классе BookService.
public class BookService : IBookService
{
    private readonly List<IBook> _books;

    public BookService()
    {
        _books = new List<IBook>(HelperGenerator.GenerateBooks());
    }

    public Task<List<IBook>> GetBooks()
    {
        return Task.FromResult(_books);
    }

    public Task<IBook> UpdateBook(IBook book)
    {
        if (book == null)
            throw new ArgumentException("Book is null");
        var foundBook = _books.FirstOrDefault(b => b.Id == book.Id);
        if (foundBook == null)
            throw new ArgumentException("Book not found");

        foundBook.Author = book.Author;
        foundBook.Title = book.Title;
        foundBook.Year = book.Year;
        foundBook.SN = book.SN;
        foundBook.Cost = book.Cost;
        foundBook.Count = book.Count;

        if (UpdateBookCompleted != null)
            UpdateBookCompleted(foundBook);

        return Task.FromResult(book);
    }

    public IBook GetBookById(int id)
    {
        return _books.FirstOrDefault(b => b.Id == id);
    }

    public event Action<IBook> UpdateBookCompleted;
}
Этот класс эмулирует работу с хранилищем информации о книгах. Для ваших же примеров это может быть запросто база данных. Немного пройдемся по методам и событиям данного класса. GetBooks позволяет получить список книг, UpdateBook обновляет информацию о книге, GetBookById позволяет получить книгу по идентификатору, и событие UpdateBookComplete вызывается, когда обновление завершено. Необходимо для того чтобы обновить, например, информацию о книге в модели представления.
Теперь перейдем к реализации представлений. Начнем, пожалуй, с главного окна программы. По терминологии Prism главное окно называется оболочкой, а поскольку мы придерживаемся терминологии призма, то необходимо наше главное окно MainWindow переместить в папку Views и переименовать в Shell (либо в ShellView, если вам так больше нравится). Проверьте, чтобы ваш класс MainWindow стал называться всюду как Shell. После этого переходим в App.xaml и удаляем свойство StartupUri, так как загрузка окна будет осуществляться с загрузчика наследуемого от класса UnityBotstrapper со сборки Prism. UnityExtensons, которую мы поставили с помощью NuGet Manager Pakages. После того как мы все это проделали, осталось только набросать для нашей оболочки разметку в xaml.
<Window x:Class="TestConfirmNavigation.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.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ContentControl Grid.Column="0" regions:RegionManager.RegionName="NavigationRegion"
                        Margin="5,0,5,5" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
        <ContentControl Grid.Column="1" regions:RegionManager.RegionName="ContentRegion"
                        Margin="5,0,5,5" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"/>
    </Grid>
</Window>
В нашей разметке используется два региона. Один из них (NavigationRegion) не отображается на UI, но он необходимый для управления навигации. К сожалению, если у нас нет родительского региона, который координирует навигацию, ваша навигация с помощью журнала навигации не будет работать. Именно с этой частью я очень долго повозился.
Дальше мы будем писать реализацию в таком порядке: модель представления и представление, которое относится к данной модели представления. Начнем с модели представления, которая у нас будет отвечать за координацию навигации. Эта модель представления ничего, собственно, не делает, кроме навигации в другой регион. Но вся логика журнала навигации отталкивается от этой модели представления. Поэтому просто рассматривайте ее как некоего рода контролер.
public class BookControllerViewModel
{
    private readonly IRegionManager _regionManager;
    public BookControllerViewModel(IRegionManager regionManager)
    {
        _regionManager = regionManager;
        _regionManager.RequestNavigate(RegionNames.ContentRegion, new Uri("/BookView", UriKind.Relative));
    }
}
Представление, которое относится к данной модели представления, называется BookNavigatedView, и оно тоже ничего, по сути, не реализует. Это просто пустышка.
<UserControl x:Class="TestConfirmNavigation.Views.Regions.BookNavigatedView"
             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>
</UserControl>
Если вы знаете какой-то способ, как можно обойтись одним регионом, чтобы не генерировать всяких пустышек, буду очень благодарен, если поделитесь этим секретом со мной.
После того, как реализовали контрол BookNavigatedControl, перейдем в класс BookNavigatedControl.xaml.cs  и свяжем нашу модель представления с данным представлением через атрибуты.
public partial class BookNavigatedView : UserControl
{
    public BookNavigatedView()
    {
        InitializeComponent();
    }

    [Dependency]
    public BookControllerViewModel ViewModel
    {
        set { DataContext = value; }
    }
}
Чтобы не повторять постоянно один и тот же код, вы просто сразу после реализации представления для новой модели представления добавляете новое свойство ViewModel с одним сеттером, который установит DataContext вашего представления в экземпляр вашей модели представления. Если же вы что-то пропустите, то сможете скачать пример и посмотреть, как это реализовал я.
Теперь перейдем к новой модели представления BookViewModel. Эта модель будет реализовывать логику по отображению списка книг. Задача этой модели представления, по сути, − только отображение, небольшая обработка кнопки по редактированию данных.
public class BookViewModel : BindableBase
{
    private readonly IRegionManager _regionManager;
    private readonly IBookService _bookService;
    private readonly DelegateCommand _editBookCommand;

    #region Constructor
    public BookViewModel(IRegionManager manager, IBookService bookService)
    {
        _regionManager = manager;
        _bookService = bookService;

        _bookService.UpdateBookCompleted += (book) =>
        {
            var foundBook = Books.First(b => b.Id == book.Id);
            foundBook.Author = book.Author;
            foundBook.Title = book.Title;
            foundBook.Year = book.Year;
            foundBook.SN = book.SN;
            foundBook.Cost = book.Cost;
            foundBook.Count = book.Count;
        };

        Books = new ObservableCollection<IBook>();
        _editBookCommand = new DelegateCommand(EditBook, CanEditBook);
        DownloadBooks();
    }

    private async void DownloadBooks()
    {
        var books = await _bookService.GetBooks();
        foreach (var book in books)
        {
            Books.Add(book);
        }
    }
    #endregion

    public ICommand EditBookCommand
    {
        get { return _editBookCommand; }
    }

    public ObservableCollection<IBook> Books { get; set; }

    private IBook _selectedBook;

    public IBook SelectedBook
    {
        get { return _selectedBook; }
        set
        {
            SetProperty(ref _selectedBook, value);
            _editBookCommand.RaiseCanExecuteChanged();
        }
    }

    private void EditBook()
    {
        var query = new NavigationParameters();
        query.Add("ID", _selectedBook.Id);
        _regionManager.RequestNavigate(RegionNames.ContentRegion,
            new Uri("BookEditorView" + query, UriKind.Relative));
    }

    private bool CanEditBook()
    {
        return SelectedBook != null;
    }
}
Мы загрузили асинхронно список книг с помощью сервиса IBookService. На этот список книг и происходит привязка в xaml. IBookService с помощью Unity зарегистрирован как singleton, поэтому здесь возможна привязка к событию UpdateBookComplete. В противном случае после навигации этот класс будет постоянно создаваться. IRegionMemberLifetime я стараюсь не использовать очень часто. Особенно в тех частях, где это можно решить с помощью Unity в одном месте. Такой код намного легче сопровождать.
Теперь перейдем к реализации самого представления. Перейдем в папку Regions и добавим новый UserControl с названием BookView.
<UserControl x:Class="TestConfirmNavigation.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"
             xmlns:convertors="clr-namespace:TestConfirmNavigation.Convertors"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
   
    <UserControl.Resources>
        <convertors:ObjectToVisibilityConverter x:Key="ObjectToVisibility"/>
       
        <DataTemplate x:Key="BookTemplate">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>

                <Label Grid.Row="0" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Autor:</Label>
                <TextBox Grid.Row="0" Grid.Column="1" IsReadOnly="True"  Text="{Binding Author}" Margin="0,5,0,0" AutomationProperties.AutomationId="ToTextBox"/>

                <Label Grid.Row="1" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Title:</Label>
                <TextBox Grid.Row="1" Grid.Column="1" IsReadOnly="True" Text="{Binding Title}" Margin="0,5,0,0" />

                <Label Grid.Row="2" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Year:</Label>
                <DatePicker Grid.Row="2" Grid.Column="1" IsEnabled="True" Text="{Binding Year}" Margin="0,5,0,0" />

                <Label Grid.Row="3" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Serial number:</Label>
                <TextBox Grid.Row="3" Grid.Column="1" IsReadOnly="True" Text="{Binding SN}" Margin="0,5,0,0" />
            </Grid>
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel HorizontalAlignment="Center" Grid.Row="0" Grid.Column="0" Margin="0, 5, 0, 5" Orientation="Horizontal">
            <Button Margin="2,0" Command="{Binding EditBookCommand}">Edit</Button>
        </StackPanel>
        <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">
                <Border CornerRadius="4,4,4,4" BorderBrush="#193441" Background="#FCFFF5" BorderThickness="2,2,2,2" Margin="5" Padding="5">
                    <StackPanel Orientation="Vertical">
                        <ListBox Name="CustomerCbx" Margin="5"  Width="Auto" HorizontalAlignment="Stretch"
                            ItemsSource="{Binding Books}"
                            SelectedItem="{Binding SelectedBook}">
                            <ListBox.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>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </StackPanel>
                </Border>
            </Grid>
        </Border>
        <GridSplitter Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="15" ShowsPreview="True" />
        <ContentControl  x:Name="Preview" Content="{Binding SelectedBook}"
                        Margin="5,15,5,5"
                        ContentTemplate="{StaticResource BookTemplate}"
                        Grid.Row="2" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
                        Visibility="{Binding SelectedBook, Converter={StaticResource ObjectToVisibility}}">
        </ContentControl>
    </Grid>
</UserControl>
В дизайнере это выглядит вот так:
P.S. Не забудьте для данного представления проставить свойство ViewModel, как описано выше.
Теперь перейдем к реализации модели представления, которая у нас будет редактировать данные о выбранной книге. Создавать мы эту модель представления будем по нажатию на кнопку “Edit”. В этой модели представления и будет реализовано подтверждение сохранения введенных данных. Назовем ее BookEditViewModel и посмотрим, как ее реализовал я, ниже.
public class BookEditViewModel : BindableBase, IConfirmNavigationRequest, IRegionMemberLifetime
{
    private readonly InteractionRequest<Confirmation> _confirmExitInteractionRequest;
    private IBook _book;
    private IRegionNavigationJournal _navigationJournal;
    private readonly ICommand _saveBookCommand;
    private readonly ICommand _cancelBookCommand;
    private readonly IBookService _bookService;

    public BookEditViewModel(IBookService bookService)
    {
        _saveBookCommand = new DelegateCommand(SaveBook);
        _cancelBookCommand = new DelegateCommand(Cancel);
        _bookService = bookService;
        _confirmExitInteractionRequest = new InteractionRequest<Confirmation>();
    }

    public ICommand SaveBookCommand
    {
        get { return _saveBookCommand; }
    }

    public ICommand CancelBookCommand
    {
        get { return _cancelBookCommand; }
    }

    public IInteractionRequest ConfirmExitInteractionRequest
    {
        get { return _confirmExitInteractionRequest; }
    }

    public IBook Book
    {
        get
        {
            return _book;
        }

        set
        {
            this.SetProperty(ref _book, value);
        }
    }


    public void OnNavigatedTo(NavigationContext navigationContext)
    {
        var bookId = navigationContext.Parameters["ID"];
        if (bookId != null)
        {
            var id = int.Parse(bookId.ToString());
            _book = _bookService.GetBookById(id);
        }

        _navigationJournal = navigationContext.NavigationService.Journal;
    }

    public bool IsNavigationTarget(NavigationContext navigationContext)
    {
        return false;
    }

    public void OnNavigatedFrom(NavigationContext navigationContext)
    {
           
    }

    public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
    {
        if (!State)
        {
            _confirmExitInteractionRequest.Raise(
                new Confirmation { Content = "Do you want to finish editing data?", Title = "Edit book" },
                c =>
                {
                    continuationCallback(c.Confirmed);
                });
        }
        else
        {
            continuationCallback(true);
        }
    }

    private bool _state = false;
    public bool State
    {
        get
        {
            return _state;
        }
        set
        {
            SetProperty(ref _state, value);
        }
    }

    private void SaveBook()
    {
        State = true;
        _bookService.UpdateBook(_book);
        if (_navigationJournal.CanGoBack)
        {
            _navigationJournal.GoBack();
        }
    }

    private void Cancel()
    {
        if (_navigationJournal.CanGoBack)
        {
            _navigationJournal.GoBack();
        }
    }

    public bool KeepAlive
    {
        get { return false; }
    }
}
А теперь пояснение по этой модели представления. Подтверждение Confirmation происходит благодаря использованию интерфейса IConfirmNavigationRequest.
Использование интерфейса IRegionMemberLifetime в данном примере аргументировано предпочтением, чтобы данное представление не пересоздавалось постоянно, а сгенерировалось один раз, и затем просто использовалось. Чтобы как-то узнать о том, что была нажата клавиша Save для сохранения данных о книге, добавлено свойство State. Оно необходимо, чтобы не выводить сообщение о том, что данные могут быть потеряны, при их сохранении. Метод ConfirmNavigationRequest использует модель взаимодействия с пользователем, которую предоставляет Prism. С самого начала статьи есть ссылка об использовании классов Confirmation и Notification, так как это целая отдельная тема, которую сложно вместить в эту статью. Метод GoBack с журнала навигации будет просто возвращаться к предыдущему представлению. В нашем случае это к BookViewModel.
Пришло время реализовать наше представление BookEditorView, которое будет отображать поля для редактирования информации о книге.
<UserControl x:Class="TestConfirmNavigation.Views.Regions.BookEditorView"
             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:system="clr-namespace:System;assembly=mscorlib"
             xmlns:interactionRequest="http://www.codeplex.com/prism"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <i:Interaction.Triggers>
            <interactionRequest:InteractionRequestTrigger SourceObject="{Binding ConfirmExitInteractionRequest}">
                <interactionRequest:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/>
            </interactionRequest:InteractionRequestTrigger>
        </i:Interaction.Triggers>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualState x:Name="Normal"/>
                <VisualState x:Name="Sending">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Control.IsEnabled)" Storyboard.TargetName="MainControl">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <system:Boolean>False</system:Boolean>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <ContentControl x:Name="MainControl" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" Margin="5">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>

                <Label Grid.Row="0" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Autor:</Label>
                <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Book.Author}" Margin="0,5,0,0" AutomationProperties.AutomationId="ToTextBox"/>

                <Label Grid.Row="1" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Title:</Label>
                <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Book.Title}" Margin="0,5,0,0" />

                <Label Grid.Row="2" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Year:</Label>
                <DatePicker Grid.Row="2" Grid.Column="1" Text="{Binding Book.Year}" Margin="0,5,0,0" />

                <Label Grid.Row="3" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Serial number:</Label>
                <TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Book.SN}" Margin="0,5,0,0" />

                <Label Grid.Row="4" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Count:</Label>
                <TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Book.Count}" Margin="0,5,0,0" />

                <Label Grid.Row="5" Grid.Column="0" Margin="0,0,5,0" VerticalAlignment="Bottom">Cost:</Label>
                <TextBox Grid.Row="5" Grid.Column="1" Text="{Binding Book.Cost}" Margin="0,5,0,0" />

                <StackPanel Grid.Row="6" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right"
                            VerticalAlignment="Bottom" Margin="5">
                    <Button Command="{Binding SaveBookCommand}" Width="60" Margin="6,0">
                        Save
                    </Button>

                    <Button Command="{Binding CancelBookCommand}" Width="60">
                        Cancel
                    </Button>
                </StackPanel>

            </Grid>
        </ContentControl>
    </Grid>
</UserControl>
Внешний вид в дизайнере выглядит следующим образом.
Здесь, наверное, самая интересная часть − это использование окна для подтверждения о том, что несохраненные данные будут потеряны. Вот эта часть логики:
<i:Interaction.Triggers>
    <interactionRequest:InteractionRequestTrigger SourceObject="{Binding ConfirmExitInteractionRequest}">
        <interactionRequest:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"/>
    </interactionRequest:InteractionRequestTrigger>
</i:Interaction.Triggers>
Логика, приведенная выше, реализована в самом Prism 5. Как видите, в призме есть множество интерфейсов, которые позволяют получать подтверждение действий пользователя, не нарушая логику паттерна MVVM с использованием классов, как, например, MessageBox. Не забываем главное добавить свойство ViewModel по аналогии, как это сделано для моделей, которые мы добавили раньше.
В нашей xaml-разметке используется один конвертер, который нужен для отображения контролов. Добавим его в папку Converters.
public class ObjectToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return value == null ? Visibility.Collapsed : Visibility.Visible;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return null;
    }
}
Теперь нам нужно реализовать модуль (интерфейс IModule), который зарегистрирует наши представления и свяжет их с нужными регионами. Для этого в папку Modules добавим класс MainModule.  
public class MainModule : IModule
{
    private readonly IRegionManager _regionManager;
    private readonly IUnityContainer _container;

    public MainModule(IRegionManager regionManager, IUnityContainer container)
    {
        _regionManager = regionManager;
        _container = container;
    }
    public void Initialize()
    {
        _container.RegisterType(typeof(Object), typeof(BookView), "BookView");
        _container.RegisterType(typeof(Object), typeof(BookEditorView), "BookEditorView");
        _regionManager.RegisterViewWithRegion(RegionNames.NavigationRegion, () => _container.Resolve<BookNavigatedView>());
    }
}
Как видите, он не делает ничего сложного кроме регистрации представлений и привязки представления с регионом, который отвечает за навигацию. Теперь чтобы это все у нас заработало, нужно добавить в корень проекта класс Bootstrapper, который необходимо наследовать от класса UnityBootstrapper. Задача этого класса − инициализировать все начальные настройки, данные для главного окна и т.д. Это удобно по той причине, что вся эта логика вынесена в одном месте.
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()
    {
        Container.RegisterType<IBook, Book>();
        Container.RegisterType<IBookService, BookService>(new ExternallyControlledLifetimeManager());
        base.ConfigureContainer();
    }
}
Именно в этом классе в методе ConfigureContainer вы сможете увидеть, как мы зарегистрировали интерфейс IBookService. Осталось при загрузке нашего App.xaml проекта запустить наш загрузчик, который выполнит всю необходимую логику. Нам для этого нужно переопределить метод OnStartup класса App.xaml.cs, как это показано в коде ниже.
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        var boostrapper = new Bootstrapper();
        boostrapper.Run();
    }
}
Наконец-то мы закончили с правкой и теперь можем запустить наш проект, чтобы убедиться в его работоспособности.
После нажатия на кнопку Edit мы перейдем в представление по редактированию данных по выбранной книге.
Если мы нажмем на кнопку Cancel, то получим следующее сообщение:
Мне кажется, что это именно то, что мы с вами пробовали получить с данной статьи. Как я ни старался, пример получился все-таки большой. Особенно его сложно было дописать из-за отпуска. Но так как до Нового Года не хотелось бы, чтобы оставались какие-то незавершенные примеры, пришлось собрать всю волю в кулак и завершить статью. Буду рад услышать ваши замечания или пожелания по статье. До встречи в Новом году.
Исходники: TestConfirmNavigation

No comments:

Post a Comment