Использование объектов запроса на взаимодействие
Один из подходов к осуществлению взаимодействия с пользователем при использовании шаблона 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(); }
В результате получим такое всплывающее окошко:

Использование класса 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. В итоге получаем всплывающее окно следующего вида:
Вот таким нехитрым способом можно организовать взаимодействие с пользователем, при полном разделении ответственности между представлением и моделью представления.
Архив с проектом.
Ссылка на пост в моём блоге.
