在WPF应用程序开发中,经常需要根据视图或视图模型的逻辑动态地打开窗口、对话框或消息框。但是,视图模型不应该直接创建或使用视觉对象,这就需要寻找其他解决方案。一种方法是使用服务来封装这些操作,提供一个接口,使得视图模型不直接与上层或WPF交互。然而,这种方法存在一定的问题,因为服务需要在表示层实现,并且需要以某种方式暴露给视图模型。虽然可以使用依赖注入容器来实现,但这种方法仍然有些棘手。那么,还有其他的选择吗?
假设有一个视图模型,其中包含一系列电子邮件消息,视图将其渲染为一个ItemsControl。现在,希望在触发某些操作时,比如双击邮件或按下Enter键(就像Outlook那样),在单独的窗口中显示邮件消息的详细信息。视图模型的某个属性变化时,比如ShowMessageDetails属性变化,也会触发这一操作。
在讨论解决方案之前,想先谈谈这两种触发方式:一种是通过视图触发窗口打开,另一种是通过视图模型的属性变化触发。对于第一种方式,当路由事件被触发或路由命令被执行时,打开窗口相对简单。在这种情况下,可以有一个OpenWindowAction,由事件或命令触发。对于第二种方式,由于属性是一个状态,并且可能与窗口状态同步(打开或关闭),因此可能会更加复杂。在这种情况下,改变属性应该打开或关闭窗口,同时关闭窗口也应该更新属性。
考虑到窗口可能由视图或视图模型基于事件、命令或属性变化打开,提出了两种解决方案:自定义动作和行为。在这篇文章中,将介绍自定义动作的解决方案,而在下一篇文章中,将介绍行为解决方案,后者稍微复杂一些。
假设有MessageListViewModel和MessageListView用于邮件消息视图,以及MessageDetailsViewModel和MessageDetailsView用于邮件详细信息视图,这些视图应该在窗口MessageDetailsDialog中呈现。在MessageListViewModel中有一个SelectedMessage属性,让看看MessageListView:
<UserControl x:Class="WPFOutlook.PresentationLayer.Views.MessageListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewmodels="http://schemas.sela.co.il/advancedwpf"
xmlns:views="clr-namespace:WPFOutlook.PresentationLayer.Views"
xmlns:behaviors="clr-namespace:WPFOutlook.PresentationLayer.Behaviors"
xmlns:i="clr-namespace:System.Windows.Interactivity; assembly=System.Windows.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<viewmodels:MessageListViewModel />
</UserControl.DataContext>
<Grid>
<DataGrid ItemsSource="{Binding Messages}" SelectedItem="{Binding SelectedMessage}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="From" Binding="{Binding From}" IsReadOnly="True" />
<DataGridTextColumn Header="Subject" Binding="{Binding Subject}" IsReadOnly="True" />
<DataGridTextColumn Header="Received" Binding="{Binding Received}" IsReadOnly="True" />
<DataGridTextColumn Header="Size" Binding="{Binding Size}" IsReadOnly="True" />
</DataGrid.Columns>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<behaviors:OpenWindowAction WindowUri="/Dialogs/MessageDetailsDialog.xaml" IsModal="True" Owner="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}" DataContext="{Binding SelectedMessage}" CloseCommand="{Binding CloseMessageDetailsCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGrid>
</Grid>
</UserControl>
如所见,在第30行,将DataGrid与一个Blend触发器关联,监听MouseDoubleClick路由事件。每当此事件被触发时,就会调用自定义的OpenWindowAction,它目前负责显示窗口。
OpenWindowAction动作具有以下属性:
当然,可以添加其他属性,例如:窗口的类型而不是或除了WindowUri之外,一个属性表示视图模型应该在弹出窗口中显示而不是在窗口中,等等。
运行这个示例并双击单个邮件,将注意到以下情况:
现在让看看OpenWindowAction:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Interactivity;
using System.ComponentModel;
using System.Windows.Input;
using WPFOutlook.ApplicationLayer.Common;
using System.Windows.Threading;
using System.Diagnostics;
namespace WPFOutlook.PresentationLayer.Behaviors
{
public class OpenWindowAction : TriggerAction<DependencyObject>
{
protected override void Invoke(object parameter)
{
Assert(CloseCommandProperty.Name, CloseCommand, null);
Assert(WindowUriProperty.Name, WindowUri, null);
if (DataContext == null)
{
return;
}
if (_isOpen)
{
return;
}
var window = (Window)Application.LoadComponent(WindowUri);
window.Owner = Owner;
window.DataContext = DataContext;
window.Closing += window_Closing;
if (IsModal)
{
window.Show();
}
else
{
window.ShowDialog();
}
_isOpen = true;
}
private void window_Closing(object sender, CancelEventArgs e)
{
var window = sender as Window;
bool canClose = CloseCommand.CanExecute(window.DialogResult);
if (canClose)
{
CloseCommand.Execute(window.DialogResult);
_isOpen = false;
}
e.Cancel = !canClose;
}
}
}