В руководстве по Prism можно найти небольшое упоминание о том, как обрабатывать запрос на взаимодействие с пользователем с помощью класса InteractionRequest. Напомню, о чём там шла речь:
Один из подходов к осуществлению взаимодействия с пользователем при использовании шаблона MVVM — позволить модели представления посылать запрос на взаимодействие непосредственно в представление. Это можно осуществить с помощью объекта запроса взаимодействия (interaction request), сопряжённого с поведением в представлении. Объект запроса содержит детали запроса на взаимодействие, а также делегат обратного вызова, вызываемый при закрытии диалога. Также, данный объект содержит событие, сообщающее о начале взаимодействия. Представление подписывается на это событие для получения команды начала взаимодействия с пользователем. Представление обычно содержит в себе внешний облик данного взаимодействия и поведение (behavior), которое связано с объектом запроса, предоставленным моделью представления.
Данных подход предоставляет простой, но довольно гибкий механизм, сохраняющий разделение ответственности между моделью представления и представлением. Это позволяет модели представления инкапсулировать логику взаимодействия, в то время, как представление содержит только визуальные аспекты. Логика взаимодействия, располагающаяся в модели представления и может быть легко протестирована, а дизайнеры пользовательского интерфейса могут полностью сосредоточиться на внешнем виде взаимодействия.
Подход на основе запроса взаимодействия хорошо согласуется с шаблонном MVVM и позволяет представлению отображать изменения состояния модели представления. Также, используя двустороннее связывание данных, можно добиться передачи данных от пользователя в модель представления и обратно. Всё это очень похоже на объект
Библиотека Prism прямо поддерживает данный шаблон при помощи интерфейса
Класс
Prism предоставляет предопределённые классы контекста, поддерживающие распространённые сценарии взаимодействия. Класс
Класс
Для использования класса
Так как объект запроса взаимодействия определяет только логику взаимодействия, то всё остальное должно быть задано в представлении. Для этого часто используются поведения, что позволяет дизайнеру выбрать необходимое поведения и связать его с объектом запроса взаимодействия в модели представления.
Представление должно реагировать на событие начала взаимодействия и предоставлять подходящий для него облик. Microsoft Expression Blend Behaviors Framework поддерживает концепцию триггеров и действий. Триггеры используются для инициации действий, всякий раз, когда возникает соответствующее событие.
Стандартный
После возникновения события,
Следующий пример показывает, как, используя
Когда пользователь взаимодействует со всплывающим окном, объект контекста обновляется в соответствии с привязками, определенными во всплывающем окне, или в шаблоне данных, используемом для отображения содержимого свойства
Для поддержки других механизмов взаимодействия, могут быть определены другие триггеры и действия. Реализация классов
По умолчанию, библиотека Prism не предоставляет какого-либо класса действия для WPF, который показывал бы всплывающее окно, или что-то подобное. Это упущение я и постараюсь далее исправить.
Для начала создадим заготовку главного окна с моделью представления.
Модель представления создаётся непосредственно в XAML. Она содержит свойство с запросом на взаимодействие и свойство команды, которая инициирует этот запрос.
Как видно, при нажатии кнопки, вызывается метод
Данный класс наследуется от класса
В результате получим такое всплывающее окошко:
В библиотеке примитивов WPF, есть замечательный класс Popup, который представляет собой всплывающее окно с содержимым. Действовать будем так: при присоединении действия, будем создавать popup и хранить его в приватном поле в закрытом состоянии. Данный popup необходимо добавить в корневой элемент главного окна. Для этого проверим, является ли корневым элементом класс, производный от
В MainWindows изменим объявление действия:
Теперь шаблон сообщения будет браться из ресурсов. Так как действие присоединяется до того, как инициализируется библиотека ресурсов главного окна, объявление шаблона необходимо расположить в
Для закрытия сообщения, необходимо найти в дереве элементов
Вот таким нехитрым способом можно организовать взаимодействие с пользователем, при полном разделении ответственности между представлением и моделью представления.
Архив с проектом.
Ссылка на пост в моём блоге.
Использование объектов запроса на взаимодействие
Один из подходов к осуществлению взаимодействия с пользователем при использовании шаблона 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. В итоге получаем всплывающее окно следующего вида:Вот таким нехитрым способом можно организовать взаимодействие с пользователем, при полном разделении ответственности между представлением и моделью представления.
Архив с проектом.
Ссылка на пост в моём блоге.