Prism 4 文档 ---第9章 松耦合组件之间通信
当构建一个大而负责的应用程序时,通用的做法时将功能拆分到离散的模块程序集中。将模块之间的静态引用最小化。这使得模块可以被独立的开发,测试,部署和升级,以及它迫使松散耦合的沟通。
当在模块之间通信时,你需要知道不同通信方式之间的区别,那样你才能确定哪种方式对于你的特定的场景最合适,Prism类库提供了以下几种通信方式:
- 命令。当希望对于用户的交互马上采取动作时使用。
- 事件聚合。用于ViewModel,展现层,或者控制之间没有所期望的直接动作时。
- 区域上下文。使用它可以提供宿主和宿主区域的View之间上下文信息。这种方式与DataContext有些相似,但是不依赖与DataContext。
- 共享的服务。调用者可以调用服务中的一个可以引发接收消息者事件的方法。当以上方式都不能使用时可使用这种方式。
命令
如果你需要对用户的手势做出回应,例如一个命令调用的点击(例如,一个按钮或者菜单项),并且如果你想要基于业务逻辑使得调用变得可用,可以使用命令。
WPF提供了RoutedCommand,它在命令调用之间联系非常擅长,例如菜单项和按钮与可视化树中的具有键盘焦点的当前项相关的命令处理。
然而,在一个组合的场景中,命令处理经常是在一个没有任何与可视化树相关联的元素或者并非焦点元素的ViewModel中。为了支持这种场景,Prism类库提供了DelegateCommand,它允许你在命令执行是调用一个委托方法,同时也提供了CompositeCommand,它允许你组合多个命令。这些命令与内建支持的RoutedCommand不同,它将会路由命令执行并且沿着可视树向上和向下处理。这允许你在可视树中某一点触发一个命令,在一个更高的级别处理这个命令。
CompositeCommand是ICommand接口的一个实现,所以它可以绑定调用者。CompositeCommands可以与几个子命令相关联;当CompositeCommand被调用时,子命令也会被调用。
CompositeCommands支持启用(enablement)。CompositeCommands监听了每一个关联的命令的CanExecuteChanged事件,它可以通过引发这个事件来通知它的调用者(们)。调用者(们)通过调用CompositeCommand的CanExecute方法来对这个事件做出反应。CompositeCommand然后调用它的每个子命令的CanExecute方法。如果任何子命令的CanExecute方法返回值为false,CompositeCommand将会返回false,这将会使得调用不可用。
它在模块间的通信反面是如何帮助你的?基于Prism类库的应用程序可以获得在Shell中定义的模块间的CompositeCommands,例如Save,Save All 和Cancel。模块可以将他们自己的命令注册到这些全局的命令中病参与它们的执行。
注意:
关于WPF 路由事件和路由命令
路由事件是可以调用元素树中多个监听者的处理逻辑的一类事件,而不是仅仅通知某个事件的直接订阅对象。WPF路由命令在可视树中的UI元素间传力命令消息。但是可视树以外的元素不会收到这些消息,因为它们只会从焦点元素向上冒泡或者向下传递或者传递到指定状态的目标对象。路由事件可以用于元素树内元素的通信,因为路由事件的数据对于路由中的每个元素都是长期保存的。一个元素可以改变事件数据中的一些内容,这些改变的内容也可以被路由中的下一个元素访问到。因此。在以下场景中你应该使用WPF路由事件:在一个通用的根中定义通用的处理或者定义自定义控件类时。
创建一个委托命令
为了创建一个委托命令,在ViewModel的构造方法中实例化一个DelegateCommand字段,然后暴露为一个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; } } }
创建一个组合命令
为了创建一个组合命令,在构造方法中实例化一个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; } } }
使一个命令全局可用
通常,为了创建一个全局可用的命令,创建一个DelegateCommand或者CompositeCommand的示例,并将其通过一个静态类对外暴露。
public static class GlobalCommands { public static CompositeCommand MyCompositeCommand = new CompositeCommand(); }
在你的模块中,将子命令同全局命令关联。
GlobalCommands.MyCompositeCommand.RegisterCommand(command1); GlobalCommands.MyCompositeCommand.RegisterCommand(command2);
注意:
为了提升代码的可测试性,你可以使用一个代理类来访问全局命令并在测试中模拟这个代理类。
绑定到一个全局可用的命令
下面的diamond示例展示了在WPF中如何将一个命令绑定到一个按钮。
<Button Name="MyCompositeCommandButton" Command="{x:Static local:GlobalCommands.MyCompositeCommand}">Execute My Composite Command </Button>
Silverlight没有提供对x:static的支持,所以在Silverlight中执行以下几步来绑定一个命令到一个按钮:
- 在View Model中,创建一个公共属性从静态类中获取命令。代码如下。
public ICommand MyCompositeCommand { get { return GlobalCommands.MyCompositeCommand; } }
- 通常,通过View的DataContext传递Model(这是在View的后台代码中实现的)。下面的代码展示了如何将Model设置为View的DataContext,通过这样做,你可以在XAML中声明式将命令绑定到View的控件上。
view.DataContext = model;
- 确保下面的XML命名空间添加到了View的XAML文件的跟元素中。
xmlns:prism="clr-namespace:Microsoft.Practices.Prism.Commands;assembly=Microsoft.Practices.Prism"
- 在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}}" 来添加一个命令的调用。
区域上下文
将会有大量的你可能想要在承载了Region的View和在Region中的View之间共享上下文信息的场景。例如,一个View的细节展示了一个业务实体并且暴露了一个region来显示业务实体的额外详细信息。Prism类库使用一个名称为RegionContext概念在Region的宿主和Region中加载的View之间共享一个对象,如下图所示。
基于这个场景,你可以选择共享信息的的一个方面(例如一个标识)或者一个共享的Model。View可以检索RegionContext,然后标记变化通知。View也可以改变RegionContext的值。这里有关于暴露和消费RegionContext的几种方式:
- 可以在XAML中将RegionContext暴露给一个Region。
- 可以在代码中将RegionContext暴露给一个Region。
- 可以从region中的一个内部View消费RegionContext。
注意:
Prism类库目前仅支持从region中的一个内部View消费RegionContext,这个View需要是一个DependencyObject。如果这个View不是一个DependencyObject(例如,你正使用WPF自动的数据模板并且直接在Region中添加View),考虑创建一个自定义的RegionBehavior来转发RegionContext给你的View对象。
注意:
关于数据上下文属性
数据上下文是一个允许元素集成他们父元素关于用于绑定的数据源信息的概念。子元素自动继承父元素的DataContext。数据沿着可视树向下流转。在Silverlight中将ViewModel绑定的Vied的最好的方式就是使用DataContext属性;这就是为什么在大多数情况下使用DataContext来存储ViewModel的原因。因此,除非是非常简单的View,不推荐使用DataContext属性作为松耦合的不同View之间的通信机制。
共享的服务
模块间通信的另一种方法是通过共享的服务。当模板被加载,模块向服务定位器中添加它们的服务。通常,从一个服务定位器中通过通用接口类型注册和检索服务。这使得模块可以使用其他模块提供的服务而不需要模块的静态引用。服务实例在模块间是共享的,所以你可以在模块间共享数据已经传递消息。
在Stock Trader RI中,Market模块提供了IMarketFeedService接口的实现。Position模块通过使用提供了服务定位和分辨的应用程序依赖注入容器的Shell消费这些服务,IMarketFeedService意在被其他模块消费,所以在StockTraderRI.Infrastructure通用程序集中可以被找到,但是这个接口的具体实现不需要被共享,所以它被之间订阅在了Market模块并且可以被单独的升级。
想要知道这些服务是如何被注册到依赖注入容器Unity中的,查看MarketModule.cs文件,如下所示,Position模块的ObservablePosistion通过构造注入来接收IMarketFeedService服务。
protected void RegisterViewsAndServices() { _container.RegisterType<IMarketFeedService, MarketFeedService>(new ContainerControlledLifetimeManager()); //... }
这样有助于模块间的通信,因为服务的消费这不需要服务提供模块的静态的引用。这个服务可以用于在模块间发送和接收数据。
事件聚合
Prism类库提供了事件机制能让应用程序中的松耦合组件相互通信。这种机制建立在事件聚合服务上,允许发布者和订阅者通过事件通信,不许要彼此直接引用
EventAggregator提供了多点传送发布/订阅功能。这意味着可能有可以触发同一事件多个发布者和可以监听同一事件的订阅者。考虑使用EventAggregator 来发布一个事件,贯穿多个模块和发送消息在业务逻辑代码间,像控制器和展示。
例如,在Stock Trader RI中,当Process Order 按钮别点击而且订单被成功处理,在这种情况下,同时其他模块需要知道订单被成功处理,以便它们能更新它们的
View。
Prism类库创建的事件是类型事件。这意味着你能在运行应用程序之前通过编译类型检测错误的优点,Prism类库中,EventAggregator允许订阅者
或发布者定位指定的EventBase.事件聚集允许多个发布者和多个订阅者。正如下面的插图所示。
注意:
关于.NET Framework 事件
使用.NET Framework事件是最简单和直观的方式用于非松散耦合需求的组件。.NET Framework 事件实现了发布-订阅模式,但是是对一个对象订阅,你需要直接引用那个对象, 这个在复杂的应用程序中,这个对象通常属于另一模块。这样导致的后果就是一种紧耦合设计。因此,.NET Framework事件用于模块内的通信而不是模块间的通信。如果你使用.NET Framework 事件, 你不得不非常小心内存泄露,特别是如果你有一个非静态的或者短暂的组件订阅了一静态或者长时间存在的事件。如果你没有取消订阅的订阅者,发布者将保存订阅者,而且阻止订阅者被垃圾回收。
IEventAggregator
EventAggregator类在容器中被作为一个服务并且可以通过IEventAggregator接口检索。事件聚合负责定位或者构建事件以及保持系统中事件集合。
public interface IEventAggregator { TEventType GetEvent<TEventType>() where TEventType : EventBase; }
EventAggregator在第一次被构造的时候构建事件。这减轻了发布者或订阅者在确定该事件是否是可用的的负担。
CompositePresentationEvent
发布者和订阅者之间的关联工作实际是CompositePresentationEvent类在起作用。它是Prism类库中包含的EventBase类的唯一的实现类。这个类维护这订阅者的列表以及处理调度给订阅者的事件。
CompositePresentationEvent是一个要求有效载荷(payload)类型定义为通用类型的通用类。这有助于保证在编译时,发布者和订阅者为成功的事件集合提供了正确的方法。下面的代码展示了一个定义CompositePresentationEvent类的部分代码。
public class CompositePresentationEvent<TPayload> : EventBase { ... public SubscriptionToken Subscribe(Action<TPayload> action); public SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption); public SubscriptionToken Subscribe(Action<TPayload> action, bool keepSubscriberReferenceAlive) public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive); public virtual SubscriptionToken Subscribe(Action<TPayload> action, ThreadOption threadOption, bool keepSubscriberReferenceAlive, Predicate<TPayload> filter); public virtual void Publish(TPayload payload); public virtual void Unsubscribe(Action<TPayload> subscriber); public virtual bool Contains(Action<TPayload> subscriber) ... }
创建和发布事件
接下来的几节介绍了如何使用IEventAggregator接口实现CompositePresentationEvent类的创建,发布和订阅。
创建一个事件
CompositePresentationEvent<TPayload>意在作为应用程序或者模块事件的基类。TPayLoad是有效事件的类型。有效载荷(payload)是在事件发布后将会传递给订阅着的参数。
例如,下面的代码展示了Stock Trader RI中的TickerSymbolSelectedEvent类,有效载荷是一个包含了公司标志的字符串。注意这个类的实现是空的。
public class TickerSymbolSelectedEvent : CompositePresentationEvent<string>{}
注意:
在一个组合的应用程序中,事件是经常在多个模块间共享的,所以,它们被定义在了一个公共的位置,在Stock Trader RI中看,定义在了 StockTraderRI.Infrastructure 项目中。
发布一个事件
发布者通过引发一个从EventAggregator检索的事件,调用Publish方法。为了获取EventAggregator,你可以通过在类的构造方法中增加一个IEventAggregator类型的参数来使用依赖注入。
例如,下面的代码演示了TickerSymbolSelectedEvent的发布。
this.eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish("STOCK0");
订阅事件
订阅者可以通过使用CompositePresentationEvent类的可用的重载的任意一个Subscribe方法来增加一个事件。使用下面的标准带帮助你决定那种操作最适合你的需要:
- 如果你需要在收到事件时能够更新UI元素,订阅在UI 线程上接收事件
- 如果你需要筛选一个事件,当订阅时提供一个筛选的委托
- 如果你关系事件的性能,考虑在订阅时使用一个强引用委托,并且手动的从CompositePresentationEvent中取消订阅。
- 如果以上都不试用,使用一个默认的订阅。
接下来的几节讲述这些选择。
在UI线程订阅
经常,订阅者需要在事件的响应中更新UI元素。在WPF和Silverlight中,只有UI线程才能更新UI元素。
默认情况,订阅者在发布者的线程中接收事件。如果发布者在UI线程中发送事件,订阅者可以更新UI,然而,如果发布者线程是后台线程,订阅者将不能直接更新UI元素。在这种情况下,订阅者需要需要安排使用Dispatcher类的UI线程上的更新。
Prism类库提供的CompositePresentationEvent可以帮助订阅者自动的在UI线程上接收事件,下面的代码展示了在订阅过程中指明。
public void Run() { ... this.eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Subscribe(ShowNews, ThreadOption.UIThread); ); } public void ShowNews(string companySymbol) { this.articlePresentationModel.SetTickerSymbol(companySymbol); }
下面的选项对于ThreadOption可以使用:
- PublisherThread.使用这个设置,在发布者进程中接收事件,这是默认设置。
- BackgroundThread. 使用这个设置在.NET Framework线程池中异步接收事件。
- UIThread. 使用这个设置在UI线程接收事件。
订阅筛选
订阅者可能不需要处理发布者每个事件的示例。在这些情况下,订阅者可以使用filter参数。filter参数是System.Predicate<TPayLoad>类型的,并且是一个当事件发布后如果发布的事件的有效载荷与标准要求的设置参数匹配决定调用订阅这回调的委托。如果有效载荷没有满足指定的标准,订阅者的回调委托不会被执行。
经常,filter被应用为lambda表达式,如下代码所示:
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>(); fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false, fundOrder => fundOrder.CustomerId == this.customerId);
注意:
Silverlight不支持lambda表达式或匿名委托的的弱引用。
对于Silverlight,你需要调用分离的功能方法,如下代码所示。
public bool FundOrderFilter(FundOrder fundOrder) { return fundOrder.CustomerId == this.customerId; } ... FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>(); subscriptionToken = fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false, FundOrderFilter);
注意:
Subscibe方法返回一个Microsoft.Practices.Prism.Events.SubscriptionToken类型的订阅标识,它可以在后面用于移除订阅事件。这个标识在你使用匿名委托或者lambda表达式作为回调委托的时候或者使用不同filter订阅相同事件的时候非常有用。
注意:
不推荐在回调委托中修改有效载荷对象,因为在一些线程同时可能会访问有效载荷对象。要保证有效载荷的不变来避免出现错误。
使用强引用订阅
如果你在一个短时间内引发多个事件并且关心它们的性能,你可能需要使用一个强引用委托来订阅。如果你那样做,你需要在销毁订阅者的时候手动的取消对事件的订阅。
默认情况下,CompositePresentationEvent维护着一个与订阅者处理逻辑和订阅筛选逻辑的弱引用。这就意味这,CompositePresentationEvent拥有的引用不会阻止垃圾回收机制对于订阅者的回收,使用弱引用可以减轻订阅者对于取消订阅的负担以及允许正确的垃圾回收。
然而,维护者这种弱引用相比相应的强引用要慢。对于大多数的应用程序,将不会主要这点性能。但是,如果你的应用程序在短时间内发布了大量的事件,你需要使用CompositePresentationEvent的强引用。如果你使用了强引用,你的订阅者需要能够在当订阅对象不在使用时去掉订阅并正确的回收它。
为了订阅一个强引用,在Subscribe方法中使用keepSubscriberReferenceAlive参数,如下所示。
FundAddedEvent fundAddedEvent = eventAggregator.GetEvent<FundAddedEvent>(); bool keepSubscriberReferenceAlive = true; fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, keepSubscriberReferenceAlive, fundOrder => fundOrder.CustomerId == _customerId);
keepSubscriberReferenceAlive参数是bool类型的:
- 当被设置为true时,事件的实例保存这订阅者示例的强引用,因此,不允许它被垃圾回收。有关如果取消订阅的更多内容,参考本章中接下来的“取消订阅事件”一节。
- 当被设置为false时(参数缺省时的默认值),事件的实例维护着对订阅者的弱引用,因此,在没有对它的引用时它们可以被回收来销毁订阅者实例。当订阅者实例被回收,事件也就自动的取消订阅了。
默认订阅
作为最小的或者默认的订阅,订阅者必须提供一个接收事件通知的适当的回调方法的签名。例如,TickerSymbolSelectedEvent的处理逻辑要求这个方法需要一个字符串参数,如下所示。
public void Run() { ... this.eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Subscribe(ShowNews); } public void ShowNews(string companySymbol) { articlePresentationModel.SetTickerSymbol(companySymbol); }
取消订阅事件
如果订阅者不在想要接收事件,你可以通过订阅者的处理逻辑来取消订阅也可以通过使用一个订阅标识来取消订阅。
下面代码展示了如何在处理逻辑中直接的取消订阅。
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>(); fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.PublisherThread); fundAddedEvent.Unsubscribe(FundAddedEventHandler);
下面的代码展示了如何使用一个订阅标识取消订阅,这个标识是由Subscibe方法的返回参数提供的。
FundAddedEvent fundAddedEvent = this.eventAggregator.GetEvent<FundAddedEvent>(); subscriptionToken = fundAddedEvent.Subscribe(FundAddedEventHandler, ThreadOption.UIThread, false, fundOrder => fundOrder.CustomerId == this.customerId); fundAddedEvent.Unsubscribe(subscriptionToken);
更多信息
有关弱引用的更过信息,请查看MSDN上的"Weak References": http://msdn.microsoft.com/en-us/library/ms404247.aspx .