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

в 18:37, , рубрики: .net, Prism, wpf, интерфейсы, метки: , , ,

В руководстве по Prism можно найти небольшое упоминание о том, как обрабатывать запрос на взаимодействие с пользователем с помощью класса InteractionRequest. Напомню, о чём там шла речь:

Использование объектов запроса на взаимодействие

Один из подходов к осуществлению взаимодействия с пользователем при использовании шаблона MVVM — позволить модели представления посылать запрос на взаимодействие непосредственно в представление. Это можно осуществить с помощью объекта запроса взаимодействия (interaction request), сопряжённого с поведением в представлении. Объект запроса содержит детали запроса на взаимодействие, а также делегат обратного вызова, вызываемый при закрытии диалога. Также, данный объект содержит событие, сообщающее о начале взаимодействия. Представление подписывается на это событие для получения команды начала взаимодействия с пользователем. Представление обычно содержит в себе внешний облик данного взаимодействия и поведение (behavior), которое связано с объектом запроса, предоставленным моделью представления.

Использование объекта взаимодействия запрос для взаимодействия с пользователем

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

Подход на основе запроса взаимодействия хорошо согласуется с шаблонном MVVM и позволяет представлению отображать изменения состояния модели представления. Также, используя двустороннее связывание данных, можно добиться передачи данных от пользователя в модель представления и обратно. Всё это очень похоже на объект DelegateCommand и поведение InvokeCommandBehavior.

Библиотека Prism прямо поддерживает данный шаблон при помощи интерфейса IInteractionRequest и класса InteractionRequest<T>. Интерфейс IInteractionRequest определяет событие начала взаимодействия. Поведение в представлении связывается с этим интерфейсом и подписывается на данное событие. Класс InteractionRequest<T> реализует интерфейс IInteractionRequest и определяет два метода Raise для инициации взаимодействия и задания контекста запроса, а также, при желании, делегат обратного вызова.

Инициация взаимодействия из модели представления

Класс InteractionRequest<T> координирует взаимодействие модели представления с представления во время запроса взаимодействия. Метод Raise позволяет модели представления инициировать взаимодействие и определить контекстный объект (типа T) и делегат обратного вызова, который вызывается при окончании взаимодействия. Объект контекста позволяет модели представления передавать данные и состояние в представление, для использования во время взаимодействия с пользователем. Если определён делегат обратного вызова, то объект контекста будет передан обратно в модель представления, во время его вызова. Это позволяет передать обратно любые изменения, произошедшие во время взаимодействия.

public interface IInteractionRequest
{
    event EventHandler<InteractionRequestedEventArgs> Raised;
}

public class InteractionRequest<T> : IInteractionRequest
{
    public event EventHandler<InteractionRequestedEventArgs> Raised;

    public void Raise(T context, Action<T> callback)
    {
        var handler = this.Raised;
        if (handler != null)
        {
            handler(
                this,
                new InteractionRequestedEventArgs(
                    context,
                    () => callback(context)));
        }
    }
}

Prism предоставляет предопределённые классы контекста, поддерживающие распространённые сценарии взаимодействия. Класс Notification является базовым классом для всех объектов контекста. Он используется, когда запрос взаимодействия должен сообщить пользователю о каком-либо событии, произошедшем в приложении. Он предоставляет два свойства — Title и Content. Обычно, это сообщение односторонние, то есть, предполагается, что пользователь не будет менять значения контекста во время взаимодействия.

Класс Confirmation наследуется от класса Notification и добавляет третье свойство — Confirmed — используемое для того, чтобы определить, подтвердил пользователь операцию, или отменил её. Класс Confirmation используется для осуществления взаимодействия в стиле MessageBox, в котором необходимо получить от пользователя ответ да/нет. Можно определить свой собственный класс контекста, наследуемый от класса Notification, для хранения необходимых для взаимодействия данных и состояний.

Для использования класса InteractionRequest<T>, модель представления должна создать экземпляр данного класса и задать свойство только для чтения, чтобы позволить представления создать привязку к данному свойству.

public IInteractionRequest ConfirmCancelInteractionRequest
{
    get
    {
        return this.confirmCancelInteractionRequest;
    }
}

this.confirmCancelInteractionRequest.Raise(
    new Confirmation("Are you sure you wish to cancel?"),
    confirmation =>
    {
        if (confirmation.Confirmed)
        {
            this.NavigateToQuestionnaireList();
        }
    });
}
Использование поведения для задания визуального облика взаимодействия

Так как объект запроса взаимодействия определяет только логику взаимодействия, то всё остальное должно быть задано в представлении. Для этого часто используются поведения, что позволяет дизайнеру выбрать необходимое поведения и связать его с объектом запроса взаимодействия в модели представления.

Представление должно реагировать на событие начала взаимодействия и предоставлять подходящий для него облик. Microsoft Expression Blend Behaviors Framework поддерживает концепцию триггеров и действий. Триггеры используются для инициации действий, всякий раз, когда возникает соответствующее событие.

Стандартный EventTrigger, предоставляемый Expression Blend, может быть использован для отслеживания событий начала взаимодействия, через связывание его с объектом запроса взаимодействия, определённым в модели представления. Однако, библиотека Prism содержит собственный EventTrigger, названный InteractionRequestTrigger, который автоматически подключается к подходящему событию Raised интерфейса IInteractionRequest.

После возникновения события, InteractionRequestTrigger запускает заданные в нём действия. Для Silverlight, библиотека Prism предоставляет класс PopupChildWindowAction, который отображает пользователю всплывающее окно. После отображения дочернего окна, его DataContext устанавливается в  параметр контекста, заданный в объекте запроса. Используя свойство ContentTemplate, можно определить шаблон данных, используемый для отображения переданного контекста. Заголовок всплывающего окна связан со свойством Title объекта контекста.

Следующий пример показывает, как, используя InteractionRequestTrigger и PopupChildWindowAction, отобразить всплывающее окно, для получения от пользователя подтверждения.

<i:Interaction.Triggers>
    <prism:InteractionRequestTrigger
            SourceObject="{Binding ConfirmCancelInteractionRequest}">

        <prism:PopupChildWindowAction
                  ContentTemplate="{StaticResource ConfirmWindowTemplate}"/>

    </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

<UserControl.Resources>
    <DataTemplate x:Key="ConfirmWindowTemplate">
        <Grid MinWidth="250" MinHeight="100">
            <TextBlock TextWrapping="Wrap" Grid.Row="0" Text="{Binding}"/>
        </Grid>
    </DataTemplate>
</UserControl.Resources>

Когда пользователь взаимодействует со всплывающим окном, объект контекста обновляется в соответствии с привязками, определенными во всплывающем окне, или в шаблоне данных, используемом для отображения содержимого свойства Content объекта контекста. После закрытия всплывающего окна, объект контекста передаётся обратно в модель представления через метод обратного вызова, сохраняя все изменённые пользователем данные. В данном примере, свойство Confirmed устанавливается в true, если пользователь нажимает кнопку OK.

Для поддержки других механизмов взаимодействия, могут быть определены другие триггеры и действия. Реализация классов InteractionRequestTrigger и PopupChildWindowAction, может быть использована, как база для написания своих триггеров и действий.

Создание собственной реализации всплывающего окна

По умолчанию, библиотека Prism не предоставляет какого-либо класса действия для WPF, который показывал бы всплывающее окно, или что-то подобное. Это упущение я и постараюсь далее исправить.

Простая реализация в виде дочернего окна

Для начала создадим заготовку главного окна с моделью представления.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:localInter="clr-namespace:PrismNotifications.Notifications"
        xmlns:inter="clr-namespace:Microsoft.Practices.Prism.Interactivity.InteractionRequest;assembly=Microsoft.Practices.Prism.Interactivity"
        xmlns:local="clr-namespace:PrismNotifications" x:Class="PrismNotifications.MainWindow" Title="MainWindow" Height="350"
        Width="525">
    <Window.DataContext>
        <local:MainWindowsViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        
        <i:Interaction.Triggers>
            <inter:InteractionRequestTrigger SourceObject="{Binding ShowNotificationInteractionRequest}">
                <localInter:ShowChildWindowsAction>
                    <DataTemplate DataType="{x:Type inter:Notification}">
                        <Grid Width="200" Height="150">
                            <TextBlock Text="{Binding Content}" />
                        </Grid>
                    </DataTemplate>
                </localInter:ShowChildWindowsAction>
            </inter:InteractionRequestTrigger>
        </i:Interaction.Triggers>
        
        <StackPanel HorizontalAlignment="Right" Margin="10" Grid.Row="1">
            <Button Command="{Binding ShowNotificationCommand}">
                    Show notificaiton windows
            </Button>
        </StackPanel>
    </Grid>
</Window>

Модель представления создаётся непосредственно в XAML. Она содержит свойство с запросом на взаимодействие и свойство команды, которая инициирует этот запрос.

using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Interactivity.InteractionRequest;
using Microsoft.Practices.Prism.ViewModel;

namespace PrismNotifications {
    /// <summary>
    /// Модель представления для главного окна.
    /// </summary>
    public class MainWindowsViewModel : NotificationObject {
        public MainWindowsViewModel() {
            ShowNotificationInteractionRequest = new InteractionRequest<Notification>();

            ShowNotificationCommand = new DelegateCommand(
                () => ShowNotificationInteractionRequest.Raise(
                    new Notification {
                                         Title = "Заголовок",
                                         Content = "Сообщение."
                                     }));
        }

        /// <summary>
        /// Запрос взаимодействия для показа сообщения.
        /// </summary>
        public InteractionRequest<Notification> ShowNotificationInteractionRequest { get; private set; }

        /// <summary>
        /// Команда, инициирующая запрос <see cref="ShowNotificationInteractionRequest"/>.
        /// </summary>
        public DelegateCommand ShowNotificationCommand { get; private set; }
    }
}

Как видно, при нажатии кнопки, вызывается метод Raise, в который передаётся экземпляр класса Notification, с заданными свойствами Title и Content. В элементе Grid располагается триггер InteractionRequestTrigger, связанный со свойством ShowNotificationInteractionRequest, которое и представляет собой запрос взаимодействия. Внутрь триггера помещено действие ShowChildWindowsAction, в котором задан шаблон данных.

using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Markup;
using Microsoft.Practices.Prism.Interactivity.InteractionRequest;

namespace PrismNotifications.Notifications {
    /// <summary>
    /// Действие по показу дочернего окна.
    /// </summary>
    [ContentProperty("ContentDataTemplate")]
    public class ShowChildWindowsAction : TriggerAction<UIElement> {
        /// <summary>
        /// Шаблон, для отображения контента.
        /// </summary>
        public DataTemplate ContentDataTemplate { get; set; }

        protected override void Invoke(object parameter) {
            var args = (InteractionRequestedEventArgs) parameter;
        }
    }
}

Данный класс наследуется от класса TriggerAction<T>, где T — тип объекта, к которому присоединяется триггер. С помощью атрибута ContentPropertyAttribute указываем, что свойство ContentDataTemplate будет являться свойством содержимого. При возникновении запроса взаимодействия, будет вызван метод Invoke, в который будет передан параметр типа InteractionRequestedEventArgs, содержащий контекст и делегат обратного вызова. Сделаем так, чтобы при вызове этого метода, отображалось дочернее окно с заголовком, определённом в свойстве args.Context.Title и содержимым, заданным в свойстве args.Context. Также, необходимо не забыть вызвать метод обратного вызова (если он задан), при закрытии окна.

        protected override void Invoke(object parameter) {
            var args = (InteractionRequestedEventArgs) parameter;

            // Получаем ссылку на окно, содержащее объект, в который помещён триггер.
            Window parentWindows = Window.GetWindow(AssociatedObject);

            // Создаём дочернее окно, устанавливаем его содержиомое и его шаблон.
            var childWindows =
                new Window {
                               Owner = parentWindows,
                               WindowStyle = WindowStyle.ToolWindow,
                               SizeToContent = SizeToContent.WidthAndHeight,
                               WindowStartupLocation = WindowStartupLocation.CenterOwner,
                               Title = args.Context.Title,
                               Content = args.Context,
                               ContentTemplate = ContentDataTemplate,
                           };

            // Обрабатываем делегат обратного вызова при закрытии окна.
            childWindows.Closed +=
                (sender, eventArgs) => {
                    if (args.Callback != null) {
                        args.Callback();
                    }
                };

            // Показываем диалог.
            childWindows.ShowDialog();
        }

В результате получим такое всплывающее окошко:

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

Использование класса Popup.

В библиотеке примитивов WPF, есть замечательный класс Popup, который представляет собой всплывающее окно с содержимым. Действовать будем так: при присоединении действия, будем создавать popup и хранить его в приватном поле в закрытом состоянии. Данный popup необходимо добавить в корневой элемент главного окна. Для этого проверим, является ли корневым элементом класс, производный от Panel, и, если да, добавим popup в коллекцию его дочерних элементов. Если нет, то создадим новый Grid и заменим корневой элемент им, добавив существующий в его коллекцию элементов. При открытии popup будем блокировать содержимое окна, а при закрытии — разблокировать и вызывать делегат обратного вызова. При перемещении окна, popup по умолчанию не перемещается вместе с ним, поэтому необходимо вручную заставлять его обновлять своё расположение. При создании popup, можно задать его свойство PopupAnimation = PopupAnimation.Fade и AllowsTransparency = true, для плавного его появления и исчезновения.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;
using System.Windows.Markup;
using Microsoft.Practices.Prism.Interactivity.InteractionRequest;

namespace PrismNotifications.Notifications {
    /// <summary>
    /// Действие по показу всплывающего окна.
    /// </summary>
    [ContentProperty("ContentDataTemplate")]
    public class ShowPopupAction : TriggerAction<UIElement> {
        private Action _callback;
        private Popup _popup;
        private ContentControl _popupContent;
        private Panel _root;

        /// <summary>
        /// Шаблон, для отображения контента.
        /// </summary>
        public DataTemplate ContentDataTemplate { get; set; }

        protected override void OnAttached() {
            // Получаем корневое окно.
            Window window = Window.GetWindow(AssociatedObject);
            if (window == null) {
                throw new NullReferenceException("Windows is null.");
            }

            // Проверяем, является ли корневым элементом Grid, 
            // если нет - создаём новый.
            _root = window.Content as Panel;
            if (_root == null) {
                _root = new Grid();
                _root.Children.Add((UIElement) window.Content);
                window.Content = _root;
            }

            // Контент всплывающего окна.
            _popupContent =
                new ContentControl
                    {
                        ContentTemplate = ContentDataTemplate,
                    };

            // Создаём всплывающее окно, задаём его визуальные свойства и контент.
            _popup =
                new Popup
                    {
                        StaysOpen = true,
                        PopupAnimation = PopupAnimation.Fade,
                        AllowsTransparency = true,
                        Placement = PlacementMode.Center,
                        Child = _popupContent,
                    };

            _popup.Closed += PopupOnClosed;

            window.LocationChanged += (sender, a) => UpdatePopupLocation();

            _root.Children.Add(_popup);
        }

        private void UpdatePopupLocation() {
            // При смене положения главного окна, 
            // необходимо обновить положение всплывающего окна.
            // Делаем это с помощью такого нехитрого трюка.
            if (!_popup.IsOpen) {
                return;
            }
            const double delta = 0.1;
            _popup.HorizontalOffset += delta;
            _popup.HorizontalOffset -= delta;
        }

        private void PopupOnClosed(object sender, EventArgs eventArgs) {
            // Вызываем делегат обратного вызова и снимаем блокировку с главного окна.
            if (_callback != null) {
                _callback();
            }
            _root.IsEnabled = true;
        }

        protected override void Invoke(object parameter) {
            var args = (InteractionRequestedEventArgs) parameter;

            _callback = args.Callback;
            _popupContent.Content = args.Context;

            // Блокируем содержимое главного окна и показываем всплывающее окно.
            _root.IsEnabled = false;
            _popup.IsOpen = true;
        }
    }
}

В MainWindows изменим объявление действия:

        <i:Interaction.Triggers>
            <inter:InteractionRequestTrigger SourceObject="{Binding ShowNotificationInteractionRequest}">
                <localInter:ShowPopupAction ContentDataTemplate="{StaticResource popupTemplate}" />
            </inter:InteractionRequestTrigger>
        </i:Interaction.Triggers>

Теперь шаблон сообщения будет браться из ресурсов. Так как действие присоединяется до того, как инициализируется библиотека ресурсов главного окна, объявление шаблона необходимо расположить в App.xaml.

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:localInter="clr-namespace:Microsoft.Practices.Prism.Interactivity.InteractionRequest;assembly=Microsoft.Practices.Prism.Interactivity"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" x:Class="PrismNotifications.App"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <DataTemplate DataType="{x:Type localInter:Notification}" x:Key="popupTemplate">
            <Border Width="200" Height="150" Background="{StaticResource {x:Static SystemColors.WindowBrushKey}}"
                    BorderBrush="{StaticResource {x:Static SystemColors.WindowFrameBrushKey}}" BorderThickness="1" CornerRadius="2"
                    Padding="5">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>
                    <TextBlock Text="{Binding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"
                               Grid.Row="1" />
                    <Button Content="Close" HorizontalAlignment="Right" Grid.Row="2">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="Click">
                                <ei:ChangePropertyAction
                                    TargetObject="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Popup}}" PropertyName="IsOpen"
                                    Value="False" />
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Button>
                    <TextBlock HorizontalAlignment="Center" Text="{Binding Title}" />
                </Grid>
            </Border>
        </DataTemplate>
    </Application.Resources>
</Application>

Для закрытия сообщения, необходимо найти в дереве элементов Popup и изменить его свойство IsOpen в false. Это можно сделать с помощью триггеров и действий из Expression Framework. В итоге получаем всплывающее окно следующего вида:

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

Вот таким нехитрым способом можно организовать взаимодействие с пользователем, при полном разделении ответственности между представлением и моделью представления.

Архив с проектом.
Ссылка на пост в моём блоге.

Автор: Unrul

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js