因为我的笔记本电脑硬盘彻底的被我玩坏了,所以第五章剩下的部分,第六章,第七章,第八章全部都已经消失到大海中去了,为他们默哀吧。在完成9 10 11章后,我会补全这些内容的。最近新买了一台电脑,同时我也对我的个人作息时间表进行了一次更新,所以翻译时间暂时定为每天晚上的8:00-10:00,并且在10:30左右发布。我将对该作息时间试运行2周以观察情况。
从第六章开始(虽然他已经走远了),我将改变翻译形式,从原来的机械式翻译改为在保证信达雅的前提下改变一定的语序,我不会刻意向其中添加任何我个人的见解(当然不自觉的除外,这个我无法保证),但是会修改原文语序以使其更符合中文的阅读习惯。并且所有自我感觉不佳的地方全部加入括号并且说明我本人的见解或者翻译的方法,欢迎各位批评指正。谢谢。
本节导读:
本节说明了如何使用Command,RegieonContext,和Share Service提供模块间的通信。
错别字改了一遍,描述不清的地方改了几处,今天的计划完成,睡觉! 2012-2-2
第九章 组件间的松耦合通信法
当制作大型复杂的应用程序时,常用的方法就是将它分成若干个独立的程序集。这时将静态连接的数目最小化将更有利于使模块可以被独立的开发,测试,部署和升级,而实现它们的关键就是松耦合交互。
当模块间需要通信时,熟悉每种通信方法间的异同将有助于更好的决定在某个特定场景中使用何种方法。Prism提供以下通信方法:
l 命令。用于即时响应用户UI上可以预期的交互。
l 事件聚合器。用以在View Model,Presenters,Controller之间的没有直接作用于反作用的交互。
l 区域上下文。该方法提供了View及其主体间的上下文信息。该方法类似于 DataContext,但不依赖于它。
l 共享服务。使用者调用服务内的事件向接收者提供消息。如果前几种方法都不适用,那就用它吧。
9.1 命令
如果需要响应一个用户行为,比如单击一个事件触发者(例如一个按钮或者菜单中的某一项),又比如需要触发者可以根据业务逻辑改变自身可用状态,那就使用命令。
Windows Presentation Foundation(WPF)提供了RoutedCommand,它专注于提供菜单项或者按钮之类的命令触发者与响应该视觉树中拥有焦点的当前项相关联的命令主体之间的连接。
(译注:因为我对WPF知之甚少,故而也没有用过这个类,此处的翻译我个人认为有问题,两个定语从句所修饰的目标我无法确定,附原文如下。WPF provides RoutedCommand, which is good at connecting command invokers, such as menu items and buttons, with command handlers that are associated with the current item in the visual tree that has keyboard focus. 我的理解方法如下:
RouteCommand (is good at 专注于) (connection command invokers with command handler 连接命令触发者与命令主体 handler可以翻译为处理程序,理解为主体应该没问题。我去掉了那个同位语) {that(用以修饰command handler我理解为定语从句) (are associated with 与XX相关联)(current item in the visual tree 视觉树中的当前项) that(这个从句应该说明什么是当前项) has keyboard focus]})
但是在一些复杂的场景中,命令主体往往是一个不与任何视觉树中的元素相关联或者不关注特定元素的View Model。为了实现这种情况,Prism提供了可以在命令执行时调用相应的委托方法的DelegateCommand,和可以组合多个命令的CompositeCommand。这些命令与内建的RoutedCommand都不同,RoutedCommand是将命令路由到视觉树中(译注:这句话肯定没有翻译好,我认为应该翻译成“将命令路由到视觉树某节点及它的上层”会更好)。它使得命令可以在视觉树的某个节点被触发,但是在上层被处理。
CompositeCommand是ICommand的一个实现所以它可以被调用者绑定。CompositeCommand可以绑定数个子命令,当CompositeCommand被触发时,所有子命令都会执行。
CompositeCommand支持Enable状态。CompositeCommand监视每个子命令的CanExecuteChanged事件。并且向调用者(们)引发这一事件。而调用者则通过调用CompositeCommand的CanExecute方法响应这一事件。这时CompositeCommand重新调用所有子命令的CanExecute方法以确定是否可以执行。如果有一个子命令的CanExecute返回了false,则CompositeCommand也会返回false,从则禁用了它的调用者(们)。
那么这些内容如何对跨模块间通信提供帮助呢?基于Prism的应用程序提供了一些全局的CompositeCommand,它们都是在Shell中定义的也就是说这些命令都是可以跨模块的,比如Save,Save All,和Cancel。模块可以将它们的命令向这些全局命令注册并且参与到它们的执行中去。
【注意】:关于WPF的Routed Events和Routed Commands
Routed Event(路由事件,参见MSDN)是将事件交由元素树中的多个监听者处理的事件,而普通事件则是仅由订阅事件的对象处理事件。而WPF-Routed Command的将命令消息是由视觉树中的UI节点交接的,那些不在树中的元素是无法接收到这些消息的,因为它们仅仅是与焦点有冒泡关系或者是一个明确规定的目标元素。(原文:because [they 代指the element outside the tree] only bubble up or down from the focused element or an explicitly stated target element)路由事件可以用于在元素树中的通信,因为事件中的数据在路由上的每个元素中都有保存。每个元素都可以改变事件数据,这些改变也将在它的下一个节点中被应用。
因此在以下场景中可以使用路由事件:在根命令处定义命令处理者或者定义自定义控件类。
9.1.1创建代理命令
要创建代理命令,那么在View Model的构造函数中实例化一个DelegateCommand的对象(field,在类里是数据域的意思,但是放在这里不好听,所以instantiate a xxx field我翻译为实例化XXX对象,下文同),以ICommand属性的形式将它暴露出去。
public class ArticleViewModel : NotificationObject
{
private readonly ICommand showArticleListCommand;
public ArticleViewModel(INewsFeedService newsFeedService,
IRegionManager regionManager,
IEventAggregator eventAggregator)
{
this.showArticleListCommand = new DelegateCommand(this.ShowArticleList);
}
public ICommand ShowArticleListCommand
{
get { return this.showArticleListCommand; }
}
}
9.1.2 创建一个复合命令
要创建一个复合命令,在构造器中实例化一个CompositeCommand的对象,向里面添加命令,最后以ICommand属性的形式将它暴露出去。
public class MyViewModel : NotificationObject
{
private readonly CompositeCommand saveAllCommand;
public ArticleViewModel(INewsFeedService newsFeedService,
IRegionManager regionManager,
IEventAggregator eventAggregator)
{
this.saveAllCommand = new CompositeCommand();
this.saveAllCommand.RegisterCommand(new SaveProductsCommand());
this.saveAllCommand.RegisterCommand(new SaveOrdersCommand());
}
public ICommand SaveAllCommand
{
get { return this.saveAllCommand; }
}
}
9.1.3 使命令全局有效
通常,创建一个全局有效的命令就是实例化DelegateCommand或者CompositeCommand的对象,并且将其通过静态类的形式暴露。
public static class GlobalCommands
{
public static CompositeCommand MyCompositeCommand = new CompositeCommand();
}
在模块中,将子命令与其关联。
GlobalCommands.MyCompositeCommand.RegisterCommand(command1);
GlobalCommands.MyCompositeCommand.RegisterCommand(command2);
【注意】:为了增加代码的可测试性,创建一个用以访问全局命令(Global Available Command 下文同)的代理类并且在测试中模拟该代理类。
9.1.4 绑定全局命令
下文说明了如何将命令绑定到WPF的按钮上。
<Button Name="MyCompositeCommandButton" Command="{x:Static local:GlobalCommands.MyCompositeCommand}">Execute My Composite Command </Button>
Silverlight不提供对x:static的支持,所以在Silverlight中需要执行以下步骤。
1、 在View Model中,创建一个公有属性以获得静态类中的命令,如下所示。
public ICommand MyCompositeCommand
{
get { return GlobalCommands.MyCompositeCommand; }
}
2、 通常,Model都会通过View的DataContext与传递到View上(在View的后置代码中完成)。以下代码演示了如何将Model绑定到View的DataContext上。完成以后,就可以在View的XAML文件中将命令绑定到控件上了。
view.DataContext = model;
3、 确定以下XML命名空间已经添加到View的XAML的根节点中。
xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Commands;assembly=Microsoft.Practices.Prism"
4、 在Silverlight中使用Click.Command附加属性向按钮绑定命令,如下所示。
<Button Name="MyCommandButton" prism:Click.Command="{Binding MyCompositeCommand}"/>Execute MyCommand</Button>
【注意】:通过将命令储存为一个App.xaml文件中的Application.Resources节中的资源。然后在设置生效后创建的View中设置prism: Click.Command="{Binding MyCompositeCommand, Source={StaticResource GlobalCommands}}"来定义事件调用者也是创建全局命令的方法。
9.2 区域上下文
在很多场景中,都需要在区域的主视图和区域的子视图中共享上下文信息。举例而言,一个用以显示业务实体的视图的Master Detail(这货我真不知道怎么翻译)和一个用以显示这个实体额外信息的区域。Prism用一个叫RegionContext的概念将对象在区域的主视图和任何在区域内加载的视图间共享,如下图所示。
根据不同的场景,可以选择只共享数据的某一部分(比如标识符)或者共享模型。View会检索RegionContext,而后会注册Change Notification。View也可以改变RegionContext的值。以下是一些暴露和使用RegionContext的方法。
l RegionContext可以通过XAML暴露给区域
l RegionContext可以通过代码暴露给区域
l RegionContext可以被区域中的视图使用
【注意】:Prism现在仅对区域中的那些成为区域的DependencyObject的视图访问RegionContext提供支持(译注:这话好难写,我的理解就是,如果区域中的视图要使用RegionContext必须要是区域的DependencyObject)。如果你的视图不是DependencyObject(比如,在区域中使用WPF自动数据模块再添加View Model),那就要创建一个自定义的RegionBehavior以使RegionContext传递到View对象中。
【注意】:关于数据上下文属性
数据上下文的概念是允许元素继承其母元素中用以绑定的数据源的信息。子元素自动继承母元素的DataContext。数据从视觉树自上向下流。
所以在Silverlight中为View绑定View Model的最佳方法就是使用DataContext;这也就是为什么在大多数情况DataContext中存的是View Model。也就是因为如此,除非视图非常简单,否则不要使用DataContext属性作为视图间松耦合通信的机制。
9.3 共享服务
共享服务也是一种模块间通信的方法。当模块被加载时,模块将它的服务添加到Service Locator中。通常,服务以通用接口的形式注册到服务定位器或者在其中进行检索。这可以使模块在不添加静态引用的情况下使用其它模块提供的服务。服务的实例在不同的模块中共享,所以可以在模块间共享数据和传递消息。
在the Stock Trader Reference Implementation (Stock Trader RI Prism的一个实例),Market模块提供了IMarketFeedService的实现。目标模块通过Shell程序的依赖注入容器使用这些服务,依赖注入容器提供了服务定位器和解析器。也就意味着IMarketFeedService被其它模块所使用,所以它可以在通用程序集StockTraderRI.Infrastructure中找到,但是该服务的具体实现就不必共享,所以直接在Market模块中定义,它也可以独立更新。
若要了解这些模块如何将服务注册到Unity依赖注入容器,如下所示,参见MarketModule.cs文件。目标模块ObservablePosition则通过构造函数注入得到IMarketFeedService服务。
protected void RegisterViewsAndServices()
{
_container.RegisterType<IMarketFeedService, MarketFeedService>(new ContainerControlledLifetimeManager());
//...
}
共享服务有助于跨模块通信是因为服务使用者并且不需要与服务提供者建立静态引用。服务可以用于模块间发送或者接收数据。