原文地址(项目说明文档):[Documentation Screens, Conductors and Composition]
http://caliburnmicro.codeplex.com/wikipage?title=Screens%2c%20Conductors%20and%20Composition&referringTitle=Documentation
Actions, Coroutines 和 Conventions 是在 Caliburn.Micro 中很值得关注的, 但是 Screens 和 Conductors 更需要深入了解,如果你的 UI 需要进行良好的设计. 他是非常重要的一个利器。Screen, Screen Conductor 和 Screen Collection 这三个术语,Jeremy Miller 先生也多次在 "Presentation Patterns" for Addison Wesley 书籍中的到过。 而这些模式在CM中是通过从特定的基类继承ViewModel实现的,最好理解它们为“角色(Roles)”,而不是普通的 View-Models。实际上根据你不同的架构,一个 Screen 可能是一个 UserControl, Presenter 或 ViewModel。有点儿跑题了, 我们先看一下它们是什么吧。
一、概述:
1.Screen
最简单易懂的说。你可能会认为它是一个在应用程序中有状态的表示层单元。它独立于应用程序shell。这种shell 可以显示许多不同的screens,一些事件在某个时间可能会有不同的几个screens,它可以显示大量的小部件,但不是screens的某部分。一些 screen 实例还可能是用于程序设置的“模态对话框”。Visual Studio 中的 代码编辑器窗口,或一个浏览器页。读到这里,你可能稍微有些理解。
通常一个Screen有一个与之关联的生命周期,允许执行自定义的屏幕 激活/失效( activation/deactivation) 的逻辑。Jeremy 先生称之为ScreenActivator。 以Visual Studio代码编辑器窗口举例。我在其中一个编辑器窗口写代码,之后我切换到了另一个选项卡编辑xml文件,此时要注意工具栏图标会发生改变。
每一个Screen都有自定义的 激活/失效 的逻辑,使它能够 设置/卸载 应用程序工具栏等。这样就能基于活动的screen为程序提供图标。普通形况下,ScreenActivator 就是一个 Screen 类。 但是要记住它们是不同的角色。如果一个特定的 screen 有复杂的激活逻辑,为了减少复杂度,ScreenActivator 就是这个类中必要的因素了。如果你的程序有不同的Screen但有相同的 激活/失效 逻辑,这是很重要的一点。
2.Screen Conductor
只要你的应用程序引入了 Screen 激活生命周期的概念。就需要执行某种方法,他就是ScreenConductor角色。 当显示一个 screen时,conductor 会确认它的激活状态。如果要离开一个 screen,它就可以实现停用功能。
同样,Screen 的 激活/失效 可能要实现一个接口或多个接口,允许 Conductor 问它“你能关闭吗?”。这就引出了一个重要的观点:在某些情况下关闭 Screen 和别的Screen 可能是相同的,也可能是不同的,这里就不一样了。比如:在Visual Studio中,从一个选项卡切换到另一个选项卡中,只是 激活/非激活 这两个情况,还要显示定义关闭选项卡逻辑,来完成触发器逻辑。
然而,在基于导航(navigation)的应用程序中,跳转导航肯定会非激活页面,但也可能导致页面关闭。 这一切都取决于您的特定应用程序的体系结构,你应该仔细考虑这些情况。
3.Screen Collection
像Visual Studio这样的应用程序,不仅会有一个ScreenConductor管理 激活/非激活 等等。也会有 ScreenCollection 维持当前打开的 Screens 或 Documents 的列表。 就像堆积木一样,我们也可以解决 非激活/关闭 的问题。 所有ScreenCollection都是开放的,但只有其中一个项目是活跃的。 MDI-style风格的应用程序(如VS,,Conductor 会管理 ScreenCollection 成员中的 Screen 的切换。 打开一个新 Document 会将它添加到 ScreenCollection 并切换到活跃的 Screen。 关闭文档不仅会关闭它,还会从 ScreenCollection 中删除它。 这一切取决于是否去积极响应“要关闭它么?”事件。 当然,文件关闭后,Conductor 还需要决定ScreenCollection中的那些物件会成为下一个活动项。
二、实现:
有很多不同的实现方式。 你可以继承一个TabControl,实现一个IScreenConductor接口直接构建所有的逻辑的控件。 然后添加到您的IoC容器中,完成并运行。 也可以让你的 UserControl(自定控件) 实现一个 IScreen 接口或者你可以实现它作为POCO让 “监视控制器” 继承。
ScreenCollection可以是包含维护 Screen 活动状态,一些特殊逻辑的自定义集合,或者它可能只是一个简单的 IList < IScreen >。
1.Caliburn.Micro 实现
通过CM中各种接口和基类实现,可以使用 mostly1to 建立 ViewModels。 让我们看一看: 见注释1
1.1 Screens
在Caliburn.Micro。 我们将 screen 激活的概念分解为几个接口:
- IActivate – 表明实现者需要激活。 这个接口提供了一个 Activate 激活方法,一个 IsActive 属性和一个 Activated 激活事件在发生激活时提交通知。
- IDeactivate –表明实现者需要非激活。 这个接口有一个传递 bool 类型参数的 Deactivate 停用方法,可以定义是要关闭还是只是停用它。它还有两个事件:AttemptingDeactivation停用前通知事件。Deactivated停用后通知事件。
- IGuardClose –表明实现者可能需要取消关闭操作。 它有一个 CanClose 方法。 该方法设计了一种异步模式,允许复杂的逻辑,如异步用户交互时发生的关闭动作。 调用者将通过传递 Action<bool> 给CanClose方法。 实现者应该在监视到逻辑完成后,调用这个方法。 通过返回的结果,来决定是否可以关闭。
除了这些核心生命周期接口,我们有一些其他辅助创建类似的表示层的类:
- IHaveDisplayName – 有一个 DisplayName 属性
- INotifyPropertyChangedEx – 从标准 INotifyPropertyChanged 接口继承而来,并增加额外的行为。 增加了一个 IsNotifying 属性可以用来关闭/更改通知,增加 NotifyOfPropertyChange 方法用于手动调用所有属性更新通知方法,用来刷新所有绑定的对象。
- IObservableCollection<T> – 由以下几个接口组成: IList<T>, INotifyPropertyChangedEx, INotifyCollectionChanged
- IChild<T> – 实现元素层次结构的一部分,或者需要一个引用一个所有者。 它有一个 Parent 属性。
- IViewAware – 这个类一定会注意的视图的实现。 它有一个 AttachView 方法称为框架绑定到视图实例。 它有一个 GetView 方法在框架调用之前创建一个视图实例。 它能缓存复杂的视图,甚至复杂的视图解析逻辑。 最后,它有一个 ViewAttached 事件,当视图连接到实例时会自动通知。
因为某些组合非常普遍,我们提供了一些方便的接口和基类:
- PropertyChangedBase——实现INotifyPropertyChangedEx(也就是INotifyPropertyChanged)。 它提供了一个 lambda-based 的 NotifyOfPropertyChange方法,除了标准的字符串机制,还支持强类型的更改通知 。 同时,所有属性改变事件自动整理到UI线程2。(见注释2)
- BindableCollection——实现 IObservableCollection < T > 的标准继承 ObservableCollection < T > 还添加了 INotifyPropertyChangedEx 规定的行为。 同时此类还确保发生在UI线程集合所有属性变化和更改事件。(见注释2)
- IScreen——这个接口由其他几个接口组成:IHaveDisplayName, IActivate, IDeactivate,IGuardClose, INotifyPropertyChangedEx
- Screen——从 PropertyChangedBase 继承和实现 IScreen 接口。 此外还有 IChild 和 IViewAware 实现。
这意味着,你可能会从PropertyChangedBase或Screen继承你自己的视图模型。 一般来说,如果你需要任何激活特性和PropertyChangedBase的一切,您将使用Screen。CM默认的 Screen 实现一些附加功能,很容易hook到相应部分的生命周期:
- OnInitialize - 可重写,仅在第一次激活 Screen 时执行的逻辑。 初始化完成后,IsInitialized 将为 true。
- OnActivate—— 可重写,每次 Screen 被激活后要执行的逻辑。 激活完成后, IsActive 将为 true。
- OnDeactivate—— 可重写,当屏幕以 “deactivated”或“closed”关闭时,应该执行的自定义逻辑。bool 类型的返回值指示该Screen如果还为alive状态,则会返回true
- CanClose——可重写,默认实现总是允许关闭。 重写这个方法来添加自定义逻辑。
- OnViewLoaded——因为 Screen 实现 IViewAware,他提供了一个机会让你知道你的视图的 Loaded 加载事件被触发了。 如果你是 SupervisingController 或 PassiveView 风格,需要使用 View 视图,就需要它。 这也是一个可能依赖于一个 view 的,放置 view model 逻辑的地方,即使你可能没有直接使用view。
- TryClose——调用这个方法关闭Screen。 如果Screen是由 Conductor 控制的,它要求Conductor 启动Screen关闭过程。 如果Screen不是由Conductor 控制,但是独立的(也许它通过 WindowManager 展示),该方法会试图关闭视图。 在这两种场景 CanClose 逻辑将被调用,如果通过,OnDeactivate 将被设为true。
要重申:如果你需要一个生命周期,就继承 Screen,不然就继承 PropertyChangedBase。
1.2 Conductors
如上所述,只要引用生命周期,就一定会实施一些东西。在Caliburn.Micro中,这一角色由 IConductor 接口表示,具有以下成员:
- ActivateItem – 激活一个特定的项目。如果 conductor 使用了 “screen collection”,他也会被加载到当前控制集合中。
- DeactivateItem – 禁用特定项目。第二个参数表明项目是否会被关闭。定义为关闭时,如果 conductor 使用了 “screen collection”,会把它从当前控制集合中删除。
- ActivationProcessed – 通知 conductor已经完成激活一个项目,不论是否激活成功3 。
- GetChildren– 返回 conductor 跟踪的所有列表项。如果 conductor 使用了 “screen collection”, 会返回所有的 “screens”, 其它方式只会返回 ActiveItem. (来自 IParent 接口)
- INotifyPropertyChangedEx – 此接口组成 IConductor.
我们也有一个接口称为 IConductActiveItem 构成 IConductor 和 IHaveActiveItem ,有以下成员:
- ActiveItem – 该属性表明 conductor 正在跟踪的当前活动项目。
it checks them individually for the following fine-grained interfaces: IActivate, IDeactive, IGuardClose and IChild. In practice, I usually inherit conducted items from Screen, but this gives you the flexibility to use your own base class, or to only implement the interfaces for the lifecycle events you care about on a per-class basis. You can even have a conductor tracking heterogeneous items, some of which inherit from Screen and others that implement specific interfaces or none at all.
您可能已经注意到,CM的 IConductor 接口使用术语 “item” ,而不是 “screen” ,我给 “screen collection” 这个词加上了引号。 原因是 CM 的 conductor 的实现不需要栏目项(item)一定要实现 IScreen 或其它特别的的接口。 他的栏目项(item)可以是 POCOs(注:可能是 Plain Old CLR Object 的 缩写,简单的普通对象,),不强制使用 IScreen。每个 conductor 实现是通用的,没有限制的类型。
当 conductor 要求为每一个每个栏目项(item)进行 激活/禁用/关闭/等等……,它会分别的单独检查以下小的接口:IActivate,IDeactive,IGuardClose,IChild。 在实践中,我通常会继承 Screen,但这样您可以灵活地使用自己的基类,或只实施你关心的基础类的 生命周期 接口事件。 你甚至可以用 conductor 跟踪混合 栏目项(item),它们可能继承 Screen 和实现其它特定的接口或根本没有。
CM 有三个 IConductor 开袋即食的的实现,两个使用“screen collection”,另一个没有。 我们从没使用的开始看一下:
Conductor<T>
这个 conductor 通过显式接口机制,简单的实现了 IConductor 的大部分成员,增加了同样的强类公开方法公开版本供使用。 它允许通过接口以强类型方式以及基于他们正在栏目项(item)来使用 conductors。 Conductor<T> 会将 “失效/关闭” 同等对待, 由于 Conductor<T> 不保持 “screen collection”,每个新栏目项(item)的激活,都会导致之前激活的项目的失效和关闭。
由于 IGuardClose 的异步性,确定当前 item 是否可以关闭,可能实际的逻辑会很复杂,但实际上当前进行的 item 也可能不会实现这个接口。因此,conductor 会委托一个 ICloseStrategy<T> 来处理,再通知 conductor 处理的结果。 大部分的时候,你就用自动提供的 DefaultCloseStrategy<T> 就好了,但你需要改变一些东西(也许对你而言 IGuardClose 不够用),你可以在 Conductor<T> 里设置 CloseStrategy 属性,来定义自己的策略。
Conductor<T>.Collection.OneActive
When a new item is activated, the previous active item is deactivated only and it remains in the Items collection. To close an item with this conductor, you must explicitly call its CloseItem method.4
同理,这个也一样实现了 Conductor<T> ,并添加一个概念"screen collection"。主要的区别在于,它不是只激活一个item,而是同时可以激活多个item。关闭一个item会使之失效,并从集合中删除。
There’s one aspect about this that I’ve noticed frequently trips up developers. If you activate an item in a conductor that is itself not active, that item won’t actually be activated until the conductor gets activated. This makes sense when you think about it, but can occasionally cause hair pulling.
还有两个我没有提到的CM的 IConductor 的实现非常重要的细节。首先,他们都是从 Screen 继承的,这是这些实现的一个关键特性。因为它创建了一个 screens 和conductors 之间的组合模式。因此,我们可以说你是在构建一个基本的导航风格的应用程序。你的 shell 可以是 Conductor<IScreen> 的一个实例,因为它只显示一个 Screen ,没有要维护的列表。但是,这些 screens 的每一个都是非常复杂的,还需要有一个多标签界面,每个标签都需要有生命周期事件。
好吧,那就继承 Conductor<T>.Collection.OneActive 吧。shell 不需要关心个别 screen 的复杂性。如果需要,某个 screen 可能是一个实现了 的 UserControl ,而不是一个 ViewModel 。
第二个重要的细节是第一条的推论。所有继承 Screen 实现了 IConductor 接口的 OOTB(开袋即食)的对象,意味着他们也有一个生命周期,这个生命周期会级联到所有他们正在管理的items项。所以,如果一个 conductor 被定义为失效(deactivated),他的 ActiveItem 也会失效停用。如果您尝试关闭一个 conductor,也会关闭他管理的所有可以关闭的 items 。这是一个非常强大的功能。有关于这一点,我已经注意到这是经常绊倒开发者的一个方面。如果你在一个 conductor 中激活一个 item ,而 conductor 本身没有被激活,该 item 实际上不会被激活,直到该 conductor 被激活。,但很可能会让你想破头的。
1.3Quasi-Conductors
在CM中,一个 Conductor 里的顶层并非全都是都是一个 screen 。举例来说,你的根 ViewModel 怎么样?如果它是一个 conductor,谁来激活它?嗯,这是 Bootstrapper 引导程序要干的事儿。Bootstrapper 本身不是一个 conductor,但它可以理解为上面讨论的小生命周期接口,确保你的根 ViewModel 得到应有的支持。WindowManager 的方式5就有点儿像一个 conductor 维护你的模态窗口(或者仅适用于WPF的非模态窗口)的生命周期。因此,生命周期并不神奇。所有的 screens/conductors 必须要么一个 conductor 的首节点,要么是 Bootstrapper 或 WindowManager 的,才能正常工作。不然你还需要自己管理生命周期。
View-First
如果你正在使用WP7或使用Silverlight的导航框架,你可能想知道怎么使用 screens 和 conductors。到目前为止,我一直假设 ViewModel-First 方式为主的 shell 工程。但是WP7平台是执行 View-First 的方法控制页面导航。SL导航框架也是这样。这种情况下,Phone/Nav(电话/导航)框架的行为像是一个 conductor。为了与ViewModels搭的好,CM的wp7版本有一个 它会hooks到 NavigationService。这个转换器是由 PhoneBootstrapper 设置的,了解了和 conductors 相同的小生命周期接口,确保他们在适当的导航点,调取你的 ViewModels 。你甚至可以通过在 ViewModel 上实现 IGuardClose 取消手机的gage导航。而 FrameAdapter 只是CM的wp7版本的一块,如果你结合Silverlight导航框架使用它,移植也很容易。
Simple Navigation(最简单的导航)
Previously, we discussed the theory and basic APIs for Screens and Conductors in Caliburn.Micro. Now I would like to walk through the first of several samples. This particular sample demonstrates how to set up a simple navigation-style shell using Conductor<T> and two “Page” view models. As you can see from the project structure, we have the typical pattern of Bootstrapper and ShellViewModel. In order to keep this sample as simple as possible, I’m not even using an IoC container with the Bootstrapper. Let’s look at the ShellViewModel first. It inherits from Conductor<object> and is implemented as follows:
public class ShellViewModel : Conductor<object> {
public ShellViewModel() {
ShowPageOne();
} public void ShowPageOne() {
ActivateItem(new PageOneViewModel());
} public void ShowPageTwo() {
ActivateItem(new PageTwoViewModel());
}
}
Here is the corresponding ShellView:
<UserControl x:Class="Caliburn.Micro.SimpleNavigation.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tc="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit">
<tc:DockPanel>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
tc:DockPanel.Dock="Top">
<Button x:Name="ShowPageOne"
Content="Show Page One" />
<Button x:Name="ShowPageTwo"
Content="Show Page Two" />
</StackPanel> <ContentControl x:Name="ActiveItem" />
</tc:DockPanel>
</UserControl>
未完待续……
已完成50%