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