CAL学习之一:UI的组织(翻译)
用户界面的组成:
为了构建你的用户界面,你需要一个架构,可以创建一个layout,这个layout是由一些松耦合的可视化单元组成的。并且你要提供一种松耦合的通讯给这些可视化单元。你需要定义下列策略:
- View Composition(view的组织)
- 命令
- 事件
下边我们来逐一具体看。
View的组织:
在一个组合应用程序中,来自不同module 的View需要在运行时被显示在用户界面的恰当位置。为了达到这个目的,开发人员需要定义一些位置来供view显示,并且需要定义一个view是怎么被创建以及以何种方式显示的。
View在什么地方显示是通过定义一个含有命名位置的布局。在运行时,view将显示在这些命名位置中。View可以通过程序或者自动的填入相应的位置。通过程序的我们叫做 view 注入,后者我们称为view感知。这两种技术决定了怎样在用户界面中显示view.
View通常用分层的模式来表现,比如 MVP模式。这可以将业务逻辑和表现层隔离。View和一个被隔离开来的 presenter来交互。Presenter里封装了业务逻辑。 在View的组织上,开发人员可以选择
View优先,也可以选择 Presenter优先。
Layout (布局)
一个layout 在用户界面上定义了一些命名位置。View将在运行时显示在这些位置上。Layout可以在不影响 module 的情况下将view加入到layout。这样就界面设计人员的更改就不会影响内务业务逻辑。
Shell定义了一个程序最高层次的layout。像下边的例子,Shell将用户界面分成两部分,导航部分和主体显示区:
在shell下我们可以类似的定义更多的layout,所有的UI就这样组织起来。
我们通过在一个control中注册一个位置的名字来定义一个命名位置。这个control将在运行时保存对应的view。这些control扮演者占位符的作用。并且在control内部定义了如何显示view的策略。比如:一个 tab control 会合理的组织和显示很多view在不同的tab里。在module里我们定义了一个view,但是并不知道如何在一个命名位置显示。
View 感知(view Discovery)
View感知是这样实现的:module可以将一个view注册到一个命名位置,这样在程序运行时,对于一个命名位置来说,任何注册的view都会自动创建并且显示出来。
Module通过一个registry来注册一个view。父View会请求这个registry,并查找其中的注册信息,一但发现,就会通过把view加到上边提到的起占位符作用的control中,来填充用户的屏幕显示。
当一个程序启动后,layout上的各个占位符control将被通知,用已经注册的view来显示界面。如图:
CAL定义了一个标准的 registry,RegionViewRegistry,用来将view注册到命名位置。
View注入(view injection)
View注入模式,是由view所在的module通过程序显式的控制一个view在一个命名位置的显示和移除。 它是这样实现的 :应用程序保留了所有的命名位置的注册,每个module可以在注册信息中找到一个命名位置,并且将view注入进去。
为了以一种通用的方法实现view注入,所有的命名位置都继承了一个通用的Interface,用来实现view的注入。
如图:
CAL定义了一个标准的注册:RegionManager 和一个标准的interface:IRegion。
View优先和Presenter优先 的组合
通常View的实现都是把业务逻辑和表现层分离。View负责表现,Presenter负责业务逻辑,当view组合时,View和Presenter连接到一起。
View优先的组合,逻辑上,View先被创建,view依赖的Presenter紧跟着被创建。而Presenter优先的组合,与之相反。不管如何,当view和Presenter被创建和初始化后,view就被显示在指定的位置上了。
View感知的方法自然的使用了View优先的模式,因为view先被创建并注册进去。View注入的方式提供了编程的方式,对于View优先还是presenter优先都是一样的。
命令(commanding)
在一个composite程序中,我们分离了表现层和业务逻辑层。这带来了好处也使得我们必须将View上用户的操作传递到view之外的handler来处理。另外,一个用户控件的可用与否还经常和程序内部的逻辑相关。
WPF引入了Command的概念来解决这个交互的问题。一个用户控件可以绑定一个command。这个command处理用户活动与 handler的执行逻辑。当一个控件接受用户的动作,它就可以执行绑定的command。而不管command的内部逻辑。而且,控件可以通过判断这个command是否可用来决定自己是否可用。
WPF 默认的 RoutedUICommand 机制,要求事件处理的handler必须定义在接受用户操作的控件或者包含它控件中。对于一个composite 的程序,command的处理不能遵循这种树形结构,实际上,一个command实际上将处理代理给他的子command的模式是复杂的。
为了突破这个限制,你可以在WPF中创建自定义的ICommand,这样你就可以直接将command转到处理逻辑哪里,而不要理会控件的树形结构。两种常见的方式是:代理和组合。
命令代理:
命令代理用一个Command来代理一个处理逻辑。不管是用event还是delegate实现,处理逻辑可以在Presenter,Service或者Controller里。没有让处理逻辑出现在view的代码中,这让处理逻辑更加容易测试。一个Command需要两个处理方法,一个是Execute,一个是CanExecute。这两个方法都是通过调用Command来调用的。不管是通过event还是delegate。
命令组合:
命令组合是一个可变的命令代理。这种模式,一个组合命令实际上代理了一系列松耦合的子命令。这个模式在有一个共享的命令,但是各个子窗口实现方法不同的情况下有用,比如SaveAll。组合命令模式需要提供一种让子命令注册进来的机制,执行命令组合就是自行各个子命令。只有当所有的子命令的CanExecute返回为true时,组合命令模式的CanExecute方法才返回true。
CAL提供了DelegateCommand<T>和CompositeCommand两个类来是实现上边提到的两种不同的模式。
事件服务(EventService)
通过事件服务,一个程序特定的服务将产生一个标准的.net事件。为了能添加新的事件,service和service的接口需要改变。这个Service被注册,并且所有的module都可以获得。订阅和查询方都引用这个service的接口,但是彼此不依赖。这样订阅者需要手动来处理线程同步的问题,并且要能够把自己从订阅者列表中取消,这样它才能被垃圾回收。
事件集合(EventAggregation)
事件集合用一种通用的集合服务来存储所有的event 对象。Event object 自己用代理而不是.net的event。这样做的一个好处是这些代理可以在发布时被创建并且马上被释放。这样就可以被垃圾回收。每个event对象包含了一个订阅者列表。新的event可以被添加到系统而不要修改这个service的代码。并且event对象可以自动的调用到正确的线程。
EventAggregator 服务和CompositePresentationEvent<T> 类 已经在CAL里被实现了。