Caliburn.Micro 中的视图组件与组合模式详解
在 Caliburn.Micro 框架中,Screen、Conductor 与 Composition 是构建复杂可维护用户界面的核心机制。虽然 Actions、Coroutines 与 Conventions 也极具价值,但深入理解 Screen 与 Conductor 的协作关系,对实现清晰的 UI 架构至关重要。
这些概念源自 Jeremy Miller 在《Presentation Patterns》一书中提出的架构思想,在 CM 框架中通过继承特定基类或实现接口来具体化。它们应被视为"角色"而非普通视图模型(ViewModel),因为一个 Screen 可能对应一个 UserControl、Presenter,甚至是一个独立的业务逻辑单元。
一、核心概念解析
1. Screen(屏幕)
代表应用中具有状态的独立交互单元,如编辑器窗口、设置对话框或页面视图。它不依赖于主外壳(Shell),可独立存在并拥有自己的生命周期。
每个 Screen 支持激活(Activate)与停用(Deactivate)操作。例如,在 Visual Studio 中切换代码标签页时,工具栏图标随之变化——这正是由当前活跃 Screen 控制的。这种行为通常通过自定义激活/停用逻辑实现。
- 关键接口:
IActivate:提供Activate()、IsActive属性及Activated事件。IDeactivate:支持带布尔参数的Deactivate(bool close)方法,以及AttemptingDeactivation与Deactivated事件。IGuardClose:定义异步关闭检查逻辑,接受一个回调函数用于通知是否允许关闭。
此外,还包含辅助接口如:
IHaveDisplayName:支持显示名称绑定。INotifyPropertyChangedEx:增强版属性变更通知,支持线程安全与强类型更新。IViewAware:处理视图关联,包括AttachView、GetView和ViewAttached事件。
2. Conductor(导引者)
负责管理多个 Screen 的激活状态流转。当切换内容时,Conductor 判定目标项是否可激活,并处理前一项的停用或移除。
其核心职责包括:
- 激活指定项目:
ActivateItem() - 停用并可选择关闭项目:
DeactivateItem(close: bool) - 提供当前活动项引用:
ActiveItem - 查询所有跟踪项:
GetChildren()
值得注意的是,Conductor 并不要求其管理的项必须实现 IScreen 接口。只要对象具备所需生命周期接口(如 IActivate, IGuardClose 等),即可被纳入控制范围。
3. Screen Collection(屏幕集合)
用于维护一组打开的 Screen 实例,常见于多文档界面(MDI)或标签页式应用。该集合保持唯一活跃项,其余处于非激活状态。
典型场景下,新文档打开时加入集合并变为活动项;关闭时则从集合中移除。是否真正关闭取决于 CanClose 逻辑的返回结果。同时,关闭后需决定下一个激活项。
二、实际实现方式
1. 基于框架的基类与接口
Caliburn.Micro 提供了以下便捷基类与接口:
PropertyChangedBase:自动处理属性变更通知,支持 lambda 式更新,且确保事件在线程上下文中触发。BindableCollection<T>:继承自ObservableCollection<T>,实现IObservableCollection<T>,并保证所有变更事件均在主线程执行。IScreen:聚合了IHaveDisplayName、IActivate、IDeactivate、IGuardClose与INotifyPropertyChangedEx。Screen:继承PropertyChangedBase并实现IScreen,同时支持IChild与IViewAware。
使用建议:若需完整生命周期管理,优先继承 Screen;否则使用 PropertyChangedBase。
重写方法说明:
OnInitialize():仅首次激活时调用,初始化完成后IsInitialized为真。OnActivate():每次激活后执行。OnDeactivate():关闭或停用时调用,返回值决定是否仍保留在内存中。CanClose():默认允许关闭,可覆盖以添加验证逻辑。OnViewLoaded():在视图加载完成后触发,适用于需要访问视图实例的场景。TryClose():尝试关闭当前屏幕,会触发CanClose逻辑,并通知Conductor进行处理。
2. Conductor 实现策略
框架内置三种标准 conductor:
-
Conductor<T>通用导引者,支持强类型操作。激活新项时,旧项将被停用并关闭。适用于单页面导航。 -
Conductor<T>.Collection.OneActive维护一个集合,允许多个项存在,但仅一个处于激活状态。关闭某项需显式调用CloseItem()。 -
Conductor<T>.Collection.AllActive允许同时激活多个项。关闭某项仅使其失效并从集合中移除。
⚠️ 注意:若子项在未激活的 Conductor 内被激活,实际上不会立即生效,直到 Conductor 自身被激活为止。这是常见误解来源。
此外,所有内置 conductor 均继承自 Screen,这意味着它们自身也具备生命周期,且会级联影响所管理的子项。例如,当一个 conductor 被停用时,其所有子项也将自动停用;若尝试关闭 conductor,所有可关闭的子项也会被一并处理。
3. 非传统导引者:伪导引者(Quasi-Conductors)
并非所有生命周期管理都来自显式 Conductor。以下情况也扮演类似角色:
- Bootstrapper:引导程序虽非 Conductor,但会主动调用根 ViewModel 的激活逻辑。
- WindowManager:负责模态窗口的生命周期管理,类似于轻量级 Conductor。
- NavigationService(WP7/Silverlight):CM 提供适配器(如
PhoneBootstrapper)将其桥接到 ViewModel 层,使导航过程可响应IGuardClose等接口。
因此,任何具备小生命周期接口的对象,均可被框架识别并参与管理。
三、实战示例:简单导航壳层
以下演示如何使用 Conductor<object> 构建基础导航结构。
public class ShellViewModel : Conductor<object>
{
public ShellViewModel()
{
ShowPageOne();
}
public void ShowPageOne()
{
ActivateItem(new PageOneViewModel());
}
public void ShowPageTwo()
{
ActivateItem(new PageTwoViewModel());
}
}
对应的视图定义如下:
<UserControl x:Class="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="跳转到第一页" />
<Button x:Name="ShowPageTwo" Content="跳转到第二页" />
</StackPanel>
<ContentControl x:Name="ActiveItem" />
</tc:DockPanel>
</UserControl>
此设计实现了典型的"点击按钮 → 切换视图"的导航模式,其中 ActiveItem 自动绑定至当前激活的 ViewModel。
四、总结
- Screen 是可独立存在的有状态单元,拥有完整的生命周期。
- Conductor 是状态协调者,负责管理多个 Screen 之间的激活/停用流程。
- Composition 是通过集合与嵌套结构构建复杂界面的方式,支持多层级、多模式布局。
- 使用
Screen作为基类可获得开箱即用的生命周期支持。 - 合理利用
Conductor<T>.Collection.OneActive可轻松实现标签页或多文档界面。 - 所有生命周期元素必须由某个顶层容器(如 Bootstrapper、WindowManager、Conductor)启动,否则无法正常工作。
掌握这些机制,便能在 Caliburn.Micro 中构建出结构清晰、易于扩展的现代桌面或移动应用。