Thursday, May 29, 2014

Паттерны взаимодействия с пользователем в Prism 5

В этой статье мы рассмотрим способы взаимодействия с пользователем с помощью библиотеки Prism 5. Часто бывают случаи, когда пользователя нужно спросить о подтверждении какого-то действия или уведомить его о том, что некое действие произошло. Примеры таких действий могут модальными для пользователя, отображаясь в виде диалоговых окон, или немодальными, например, всплывающее окно или уведомление. Одной из самых популярных проблем в этом случае является разделение ответственности при использовании паттерна MVVM. Так, если брать просто пример без MVVM, то в коде вы просто можете вызвать, скажем, показ окна с помощью MessageBox, чтобы получить от пользователя какой-то ответ. Вызвать MessageBox в модели представления или модели является неправильным решением, так как нарушает разделение ответственности между представлением и моделью представления.
С точки зрения паттерна MVVM, модель представления ответственна за инициирование взаимодействия с пользователем, за получение и обработку ответа, в то время как представление ответственно за взаимодействие с пользователем. 
Сохранение четкого разделения ответственности между логикой представления, реализованной в модели представления, и пользовательским интерфейсом, реализованным представлением, помогает улучшить тестируемость и гибкость.
Существует два способа реализации такого сценария. Первый способ заключается в реализации службы, которая будет использована в модели представления, чтобы всю реализацию о взаимодействии с пользователем отделить от уровня представления. Второй подход основан на событиях, генерируемых моделью представления для выражения намерения взаимодействовать с пользователем, наряду с компонентами в представлении, которые связываются с этими событиями и управляют визуальными аспектами взаимодействия.

Использование службы взаимодействия
При использовании службы взаимодействия модель представления полагается на службу взаимодействия, которая взаимодействует с пользователем через окно сообщения. Благодаря этому подходу мы имеем разделение ответственности и возможность протестировать реализацию. Обычно такое поведение достигается через внедрение зависимости или локатор служб (модель представления через внедрение зависимости хранит ссылку на службу взаимодействия)Поскольку в модели представления есть ссылка на службу взаимодействия, она может при необходимости запросить взаимодействие с пользователем. Пример работы такой службы можно посмотреть на рисунке ниже, взятом с официальной документации.
Рассмотрим простой пример использования такого подхода. Для начала создадим новый WPF приложение, назовем его InteractivitySample.
Я стараюсь заставлять себя писать правильный код, который при надобности к нему обратиться через некоторое время не сложно будет понять и сопровождать далее. Поэтому, как и в предыдущих примерах, основанных на библиотеке Prism 5, в этом примере будет использоваться загрузчик UnityBootstrapper, который работает с IoC контейнером Unity. Как использовать такой загрузчик, вы можете посмотреть в статье "Введение в Prism 5. Bootstrapper".  Но для начала тем, кому не нужна пошаговая инструкция, я вкратце расскажу, что нужно проделать. Первым делом создаем начальную структуру проекта.
В этой структуре проекта нет ничего сложного. Она подстроена под паттерн MVVM. где Views – папка для представлений, Models – модели данных, ViewModels – подели представлений и Helpers – вспомогательные классы. Следующим этапом установим через NuGet Packages сам Prism 5, а также пакет Prism.UnityExtensions для работы с Unity. Следующим этапом, поскольку я стараюсь работать в пределах терминов Prism, является перенос окна MainWindows.xaml в папку Views. После этого данное окно переименуем на Shell.xaml, изменяем пространство имен на соответствующее. Убираем из файла App.xaml строку StartupUri. Следующим шагом является добавления загрузчика Bootstrapper, наследуемого от класса UnityBootstrapper. Ниже приведена реализация этого загрузчика.
public class Bootstrapper : UnityBootstrapper
{
       protected override DependencyObject CreateShell()
       {
             return ServiceLocator.Current.GetInstance<Shell>();
       }

       protected override void InitializeShell()
       {
             Application.Current.MainWindow = (Window)Shell;
             Application.Current.MainWindow.Show();
       }
}
После этого нужно в файл App.xaml.cs добавить код для запуска нашего загрузчика при страте приложения.
protected override void OnStartup(StartupEventArgs e)
{
       Bootstrapper bootstrapper = new Bootstrapper();
       bootstrapper.Run();
}
Если вы проделаете все действия, описанные выше, ваш проект должен нормально собраться.
Начнем, пожалуй, из создания нашего сервиса для работы с диалоговыми окнами. Для этого перейдем в папку Helpers и добавим новый интерфейс IDialogService для работы с диалоговыми окнами. Для того чтобы слишком не загромождать логикой данный сервис, я покажу самую простую его реализацию.
public interface IUiDialogService
{
       bool? ShowDialog(string title, object datacontext);

       void ShowDialog(string title, object datacontext, Action<bool?> responseResult);

       MessageBoxResult ShowMessageBox(
                    string messageBoxText,
                    string caption,
                    MessageBoxButton button,
                    MessageBoxImage icon);

       void ShowMessageBox(
                    string messageBoxText,
                    string caption,
                    MessageBoxButton button,
                    MessageBoxImage icon,
             Action<MessageBoxResult> responseResult);

}
В данном сервисе методы разделены на две части: те, которые возвращают результат, и те, которые позволяют работать асинхронно и возвращать результат через функцию обратного вызова. В данном примере это параметр responseResult. Использование данного интерфейса приведено ниже.
public class DialogService : IUiDialogService
{
       public bool? ShowDialog(string title, object datacontext)
       {
             var win = new WindowDialog();
             win.Title = title;
             win.DataContext = datacontext;

             return win.ShowDialog();
       }

       public void ShowDialog(string title, object datacontext, Action<bool?> responseResult)
       {
             var dialogResult = ShowDialog(title, datacontext);
             if (responseResult != null)
             {
                    responseResult(dialogResult);
             }
       }

       public MessageBoxResult ShowMessageBox(
             string messageBoxText,
             string caption,
             MessageBoxButton button,
             MessageBoxImage icon)
       {
             return MessageBox.Show(messageBoxText, caption, button, icon);
       }

       public void ShowMessageBox(string messageBoxText, string caption, MessageBoxButton button, MessageBoxImage icon,
                                     Action<MessageBoxResult> responseResult)
       {
             var result = ShowMessageBox(messageBoxText, caption, button, icon);
             if (responseResult != null)
             {
                    responseResult(result);
             }
       }
}
Теперь добавим модель представления MainViewModel, которая продемонстрирует, как использовать наш только что созданный сервис.
public class MainViewModel
{
       private readonly IUiDialogService _dialogService;

       public MainViewModel(IUiDialogService dialogService)
       {
             _dialogService = dialogService;
             ShowDialogBoxCommmand = new DelegateCommand(ShowDialogWindow);
             ShowMessageBoxCommmand = new DelegateCommand(ShowMessageBoxWindow);
       }

       public ICommand ShowMessageBoxCommmand { get; private set; }

       public ICommand ShowDialogBoxCommmand { get; private set; }

       #region Private Methods
       private void ShowMessageBoxWindow()
       {
             var result =
                    _dialogService.ShowMessageBox(
                           "Are you sure you want to cancel this operation?",
                           "Confirm",
                           MessageBoxButton.OK,
                           MessageBoxImage.Information);
             if (result == MessageBoxResult.OK)
             {
                    //Cancel
             }

             //Async model
             //_dialogService.ShowMessageBox(
             //           "Are you sure you want to cancel this operation?",
             //           "Confirm",
             //           MessageBoxButton.OK,
             //           MessageBoxImage.Information,
             //           dialogResult =>
             //                  {
             //                         if (dialogResult == MessageBoxResult.Yes)
             //                         {
             //                                //CancelRequest();
             //                         }
             //                  });
       }

       private void ShowDialogWindow()
       {
             var result =
                    _dialogService.ShowDialog(
                           "Hello world",
                           null);
             if (result == true)
             {
                    //Cancel
             }
       }
       #endregion
}
Если мы посмотрим внимательно на метод ShowMessageBoxWindow, то можем увидеть откомментированный вариант, который показывает пример работы в синхронном режиме, и закомментированный вариант, который позволяет работать в асинхронном режиме. Вы можете построить асинхронную реализацию на тасках (класс Task), главное в этом всем – показать идею принципа работы.
У вас класс DialogService будет выдавать ошибку о том, что он не может найти класс WindowDialog, так как мы его не успели добавить. Для того чтобы добавить реализацию диалогового окна, перейдем в папку Views и добавим новое окно WindowDialog, как показано ниже.
После того как мы добавили данное окно, необходимо добавить в это окно логику обработки. Первым делом добавим реализацию в WindowDialog.xaml код.
<Window x:Class="InteractivitySample.Views.WindowDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WindowDialog" Height="320" Width="400"
        WindowStyle="SingleBorderWindow"
        ResizeMode="NoResize"
        WindowStartupLocation="CenterOwner"
        >
    <Grid >
        <Grid.RowDefinitions >
            <RowDefinition Height="240" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ContentPresenter Grid.Row="0" x:Name="DialogPresenter" Content="{Binding}">

        </ContentPresenter>
        <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button x:Name="OkButton" Content="OK" Width="100" Margin="3" Click="OkButton_Click"/>
            <Button x:Name="CancelButton" Content="Cancel" Width="100" Margin="3" Click="CancelButton_Click"/>
        </StackPanel>
    </Grid>
</Window>
Затем перейдем в класс WindowDialog.xaml.cs и реализуем обработчики нажатия на кнопки "OK" и "Cancel".
public partial class WindowDialog : Window
{
       public WindowDialog()
       {
             InitializeComponent();
       }

       private void OkButton_Click(object sender, RoutedEventArgs e)
       {
             DialogResult = true;
       }

       private void CancelButton_Click(object sender, RoutedEventArgs e)
       {
             DialogResult = false;
       }
}
Теперь нужно добавить обработчики в нашу оболочку Shell.xaml, чтобы проверить, как будет отрабатывать реализованный выше код.
<Window x:Class="InteractivitySample.Views.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel HorizontalAlignment="Center" Margin="3">
            <Button Content="Показать MessageBox" Height="25" Margin="3"
                    Command="{Binding ShowMessageBoxCommmand}"/>
            <Button Content="Показать DialogBox" Height="25" Margin="3"
                    Command="{Binding ShowDialogBoxCommmand}"/>
        </StackPanel>
    </Grid>
</Window>
Последним этапом нам нужно установить объект MainViewModel как свойство DataContext нашей оболочки. Для этого нам понадобится внести небольшие изменения в наш загрузчик Bootstrapper.
public class Bootstrapper : UnityBootstrapper
{
       protected override DependencyObject CreateShell()
       {
             Container.RegisterType<IUiDialogService, DialogService>();
             return Container.Resolve<Shell>();
       }


       protected override void InitializeShell()
       {
             Application.Current.MainWindow = (Window)Shell;
                   
             var viewModel = Container.Resolve<MainViewModel>();
             Application.Current.MainWindow.DataContext = viewModel;
             Application.Current.MainWindow.Show();
       }
}
Давайте посмотрим на структуру проекта, которая у нас получилась в результате.
Теперь можно и запустить проект, чтобы убедиться в том, что у нас все работает.
Нажмем на кнопку с надписью "Показать MessageBox", и мы должны получить результат, как на рисунке ниже.
Если мы проверим результат нажатия кнопки "Показать DialogBox", то должны получить просто диалоговое окно.

Детальнее хотелось бы остановиться на реализации асинхронного режима, который у нас в примере не отличался от синхронного режима. Этот режим обеспечивает большую гибкость при реализации сервиса взаимодействия, позволяя создавать модальное и немодальное взаимодействие. В этой статье мы рассмотрели один из подходов реализации паттерна взаимодействия с пользователем на основе службы взаимодействия. В продолжении этой статьи мы рассмотрим использование объектов запроса взаимодействия, который библиотека Prism поддерживает через интерфейс IInteractionRequest. Реализацию данного паттерна вы можете посмотреть в следующей статье.