在软件开发中,解耦是一个重要的概念,它有助于提高代码的可维护性和可测试性。本文将介绍如何使用Silverlight和Prism框架来实现弹出窗口的解耦,并展示如何通过事件聚合器模式进行单元测试。
首先,需要构建一个消息载体,用于传递弹出窗口的消息。消息载体的定义如下:
public class MessageBoxPayload
{
public object DataContext { get; set; }
public bool AllowCancel { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public Action<MessageBoxPayload> ResultHandler { get; set; }
public bool Result { get; set; }
private MessageBoxPayload() { }
public static MessageBoxPayload GeneratePayload(object dataContext, bool allowCancel, string title, string message, Action<MessageBoxPayload> resultHandler)
{
MessageBoxPayload retVal = new MessageBoxPayload
{
AllowCancel = allowCancel,
Title = title,
Message = message,
ResultHandler = resultHandler,
DataContext = dataContext
};
return retVal;
}
}
在这个载体中,不假设任何状态,而是通过一个Action来传递载体类型,允许请求者提供一个方法,在对话框关闭时被调用。
接下来,定义一个事件,用于请求弹出消息框。这个事件基于Prism提供的事件机制:
public class MessageBoxEvent : CompositePresentationEvent<MessageBoxPayload>
{
}
这里继承了CompositePresentationEvent,这是一个所有参与事件聚合器服务的事件的基类。
现在可以轻松地开始发布事件。例如,如果有一个带有删除按钮的数据网格,事件可能看起来像这样:
private void Button_Delete_Click(object sender, System.Windows.RoutedEventArgs e)
{
Button src = e.OriginalSource as Button;
if (src != null)
{
_eventService.GetEvent<MessageBoxEvent>().Publish(
MessageBoxPayload.GeneratePayload(src.DataContext, true, "请确认", "确定要删除这个项目吗?", DeleteItem));
}
}
public void DeleteItem(MessageBoxPayload payload)
{
if (payload.Result)
{
MyItem item = payload.DataContext as MyItem;
Delete(item);
}
}
在这个例子中,删除点击事件将消息包装成载体并发布事件。提供了一个委托来回调"DeleteItem",以获取对话框的结果。如果用户确认,则使用数据上下文来获取给定行的实体,并执行删除命令。
_eventService的定义如下:
private readonly IEventAggregator _eventService;
IEventAggregator是在视图的构造函数中作为参数提供的,它由依赖注入框架自动连接。要将事件聚合器传递到对象中,需要两个步骤。
首先,在引导程序中注册一个单一实例:
protected override void ConfigureContainer()
{
base.ConfigureContainer();
// 提供事件聚合器的引用
Container.RegisterInstance<IEventAggregator>(Container.Resolve<EventAggregator>());
}
这里使用"resolve"来获取实例。这是在使用依赖注入/控制反转框架时的良好实践。当创建对象时,必须选择适当的构造函数并注入适当的值。通过调用Resolve,要求容器根据当前的配置和注册提供实现。
接下来,将服务注入到适当的弹出窗口处理程序中。最简单的方法是创建一个公共模块。该模块可以包含在项目之间共享的元素。首先,将创建一个新的子窗口并称之为"Popup"。XAML代码如下:
<controls:ChildWindow x:Class="Modules.Common.Views.Popup" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls" Width="Auto" Height="Auto" Title="{Binding Title}">
<Grid x:Name="LayoutRoot" Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock TextWrapping="Wrap" Text="{Binding Message}" FontFamily="Arial" FontSize="12" TextAlignment="Center" Grid.Row="0"/>
<Button x:Name="CancelButton" Content="取消" Click="CancelButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,0,0" Grid.Row="1"/>
<Button x:Name="OKButton" Content="确定" Click="OKButton_Click" Width="75" Height="23" HorizontalAlignment="Right" Margin="0,12,79,0" Grid.Row="1"/>
</Grid>
</controls:ChildWindow>
主要需要注意的是使用Auto来确保对话框根据内容调整大小。接下来是代码背后的实现。
弹出窗口的构造函数接受载体并设置数据上下文,然后设置取消按钮的可见性。然后,按钮的事件绑定将设置适当的结果,然后调用对话框关闭事件的处理器。
public partial class Popup
{
public Popup()
{
InitializeComponent();
}
public Popup(MessageBoxPayload payload)
{
InitializeComponent();
DataContext = payload;
CancelButton.Visibility = payload.AllowCancel ? Visibility.Visible : Visibility.Collapsed;
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
MessageBoxPayload result = (MessageBoxPayload)DataContext;
result.Result = true;
result.ResultHandler(result);
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
MessageBoxPayload result = (MessageBoxPayload)DataContext;
result.Result = false;
result.ResultHandler(result);
}
}
这里的关键是一个接受载体的构造函数,并设置数据上下文,然后设置取消按钮的可见性。然后,按钮的事件绑定将设置适当的结果,然后调用对话框关闭事件的处理器。
如果这是一个真正解耦的模块,它将如何"知道"定义的区域?此外,即使迭代区域集合并将它注入到找到的第一个区域,会发现一个奇怪的、空的弹出窗口只是挂在那里。这不是想要的!为了管理这个弹出窗口,将使用一个控制器。
弹出窗口控制器看起来像这样:
public class PopupController
{
public PopupController(IEventAggregator eventService)
{
eventService.GetEvent<MessageBoxEvent>().Subscribe(PopupShow);
}
public void PopupShow(MessageBoxPayload payload)
{
Popup popupWindow = new Popup(payload);
popupWindow.Show();
}
}
现在可以设置模块来调用控制器。
公共模块的初始化如下:
public class CommonModule : IModule
{
private readonly IUnityContainer _container;
public CommonModule(IUnityContainer container)
{
_container = container;
}
public void Initialize()
{
_container.RegisterInstance(_container.Resolve(typeof(PopupController)));
}
}
注意没有使用区域进行注册。相反,只是解析控制器的单一实例。这将订阅弹出窗口事件。因为使用容器来解析控制器,容器将自动引用EventAggregator并将其注入到构造函数中。最后,这个模块只需要在模块目录中注册:
模块目录的注册如下:
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog();
catalog.AddModule(typeof(MySpecificModule));
catalog.AddModule(typeof(CommonModule));
return catalog;
}