Caliburn - 模块、窗口和操作
本文演示了如何通过模块开发打破 Shell 的限制,并使用 Caliburn 操作。
引言
Caliburn 是一个 CodePlex 项目,它在一个集成的框架中为 WPF 和 Silverlight 开发提供了 UI 模式。这些模式包括 MVVM、MVC、MVP、Commands 等。它鼓励测试驱动开发 (TDD),并提供了一个易于使用的依赖注入容器。该项目由 Rob Eisenburg 协调,他非常积极地解决问题和改进框架。他激励了许多其他有才华的开发者协助添加补丁并促进产品的增长。本文假定您了解依赖注入和控制反转。我将简要介绍每个部分,但会提供链接以供更深入地了解该主题。
背景
目前,我正在处理一个大型后台系统,该系统在 WPF 上下文中使用了Prism。我对依赖注入和松散耦合的架构很感兴趣,并开发了一些使用 Prism 的独立项目。不久之后,我接触到了Ninject,整个基础设施很快就切换到使用 Ninject 模式。随着应用程序的迭代,一些场景使用 Ninject 处理得不好。一些单例作用域对象保留了私有字段值(即使它们每次都被构造),并且框架(与 Prism 类似)是基于单个 Shell 的。我需要模块拥有自己的窗口(与其他模块同时显示)以及从多个上下文中启动的模态对话框。
通过研究其他 WPF UI 框架,我发现了Caliburn。使用 Caliburn,单例状态持久性不再是问题,它还提供了一个可自定义的 Window Manager。该框架支持的功能比演示的要多得多。包含的项目有一个非常简单的模块场景,主菜单在单独的窗口中启动模块 UI,或将它们嵌入到 Shell 中。此外,还启动了模态项编辑器,并引入了 Caliburn 操作。Caliburn 发行版中的示例中有各种演示项目,并在 CodePlex 的问题跟踪器中添加了其他项目,但我找不到任何演示此组合的项目。
|
|
Shell 项目
|
共享项目
|
|
|
模块 A 项目
|
模块 B 项目
|
Using the Code
代码的组织是为了可读性而不是最佳的面向对象设计实践。解决方案的结构包括一个 Shell、两个模块和一个共享库。将概述通用的开发人员工作流程,并特别关注对此特定场景更核心的流程。以下代码与 RTW V1 分支相关。V2 主干源代码已有所更改。
Shell
App.xaml 继承自CaliburnApplication
,以便进行一些配置和初始化。这也可以通过CaliburnFramework
类手动完成。我们关心的 IsMethod 是CreateRootModel
、SelectAssemblies
和ConfigurePresentationFramework
。
protected override object CreateRootModel()
{
var binder = (DefaultBinder)Container.GetInstance<IBinder>();
binder.EnableMessageConventions();
binder.EnableBindingConventions();
return Container.GetInstance<IShellViewModel>();
}
此方法返回根应用程序模型,即ShellViewModel
(实现了IShellViewModel
)。
protected override System.Reflection.Assembly[] SelectAssemblies()
{
return new System.Reflection.Assembly[] { Assembly.GetExecutingAssembly(),
Assembly.GetAssembly(typeof(emx.tcp.caliburn.loading.modules.moduleA.ModuleAModule)),
Assembly.GetAssembly(typeof(emx.tcp.caliburn.loading.modules.moduleB.ModuleBModule)),
Assembly.GetAssembly(typeof(emx.tcp.caliburn.loading.shared.Actions.DialogResultAction))
};
此方法选择一个Assembly
数组,Caliburn 将能够检查该数组以获取组件、视图等。这包括已声明用于依赖注入的类,以及由DefaultViewStrategy
(通过命名空间约定将 ViewModels 映射到 Views)配置的类等。
protected override void ConfigurePresentationFramework(PresentationFrameworkModule module)
{
module.UsingWindowManager<WindowManager>();
}
此调用使用WindowManager
类中的配置来定制 Window Manager。
//Display a view in a dialog (modal) window
public new bool? ShowDialog(object rootModel, object context,
Action<ISubordinate, Action> handleShutdownModel)
{
var window = base.CreateWindow(rootModel, context, handleShutdownModel);
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
window.WindowStyle = WindowStyle.ToolWindow;
window.ResizeMode = ResizeMode.NoResize;
window.Title = ((IPresenter)rootModel).DisplayName;
return window.ShowDialog();
}
//Display a view in a popup (non-modal) window
public new void Show(object rootModel, object context,
Action<ISubordinate, Action> handleShutdownModel)
{
var window = base.CreateWindow(rootModel, context, handleShutdownModel);
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
window.Title = ((IPresenter)rootModel).DisplayName;
window.ResizeMode = ResizeMode.NoResize;
window.Show();
}
这些方法配置用于ShowDialog
和Show
的窗口。
因此,当应用程序启动时,ShellViewModel
是初始的根模型,并且由于命名空间约定,ShellView
已绑定到它。
ShellView.xaml 具有非常简单的界面,尽管它确实使用了基本的操作。这些操作是高度可定制的,并在此处记录。
<Button
cal:Message.Attach="ShowModuleA(HostComboBox.SelectedIndex)"
Content="Module A - Persons"/>
附加到按钮的属性会将默认事件(Click
)路由到 ViewModel 中的方法。用于将加载的视图托管在 Shell 中的标记是
<Controls:TransitionPresenter x:Name="CurrentPresenter">
<cal:Action.TargetWithoutContext >
<shared.actions:CRUDAction />
</cal:Action.TargetWithoutContext >
</Controls:TransitionPresenter>
此机制类似于在 Prism 中声明一个区域。这里重要的是附加属性cal:Action.TargetWithoutContext
,它将托管视图中的任何操作映射到一个名为CRUDAction
的共享库中的类(不为其分配视图的 datacontext),稍后将讨论该类。
ShellViewModel.cs 是所讨论视图的 ViewModel。它有一个在cal:Message.Attach
属性中映射的方法。
public void ShowModuleA(int index)
{
Host<emx.tcp.caliburn.loading.modules.moduleA.ViewModels.PersonListViewModel>(index);
}
此方法接收组合框的选定索引,并将模块 A ViewModel 和索引路由到一个通用方法,该方法用于嵌入视图或在窗口中显示视图。
模块
模块在功能上非常相似,包含 Views、ViewModels、Models 和 Services。模块 B 包含一些额外的 Controls、Converters 和 Formatters,如果需要,这些都可以打包到另一个库中。
ViewModels 通过构造函数注入服务。
Services 从POCO的序列化(仅用于演示目的)ObservableCollection
中获取数据,这些数据位于Models命名空间中。使用一个简单的Singleton参数服务来传递类实例之间的参数;对于演示以外的任何情况,建议使用更健壮的基于契约的方法。
Views 也将操作映射到共享库中的类;然而,整个 UserControl 都可以进行此声明。另外值得注意的是传递给外部操作的参数
<Button
Content="Add"
cal:Message.Attach="AddAction($datacontext)"
Style="{StaticResource ButtonStyle}"/>
这引入了一个新的特殊参数概念。一些受支持的参数是
$dataContext
- 视图的数据上下文,可能是 ViewModel。$value
- 源元素的值。$source
- 源元素。$eventArgs
- 事件签名中的参数。
共享库
共享库包含模块中使用的一些可重用接口和类。它主要包含在内,以展示如何注入依赖项以及如何从外部库引用操作。Action 类值得特别提及。
CRUDAction.cs 由模块 A 和模块 B 中的“List”视图引用。该类执行视图的操作(添加、编辑、删除、保存)并设置操作源按钮的状态。每个操作都可以使用Preview Filter进行装饰。这与Commands
中的Execute
、CanExecute
具有类似的功能。
[Preview("CanAddAction")]
public void AddAction(IActionViewModel actionViewModel)
{
_actionViewModel = actionViewModel;
actionViewModel.ParameterService.AssignParameter("Action", "Add");
actionViewModel.ParameterService.AssignParameter("CurrentItem",
actionViewModel.EditableCollectionView.AddNew());
if ((bool)ShowDialog(actionViewModel.GetEditorRootModel()))
{
actionViewModel.EditableCollectionView.CommitNew();
RaisePropertyChangedEventImmediately("CanSaveAction");
}
else
{
actionViewModel.EditableCollectionView.CancelNew();
}
}
public bool CanAddAction(IActionViewModel actionViewModel)
{
return actionViewModel.EditableCollectionView.CanAddNew;
}
虽然参数通过$datacontext
从视图传递,但通过键入参数IActionViewModel
(“List” ViewModels 都实现了该接口),操作和预览过滤器可以通用地工作。
DialogResultAction.cs 由模块 A 和模块 B 中的“AddEdit”视图引用。该类执行视图的操作(OK、Cancel)。
public void OKAction(RoutedEventArgs e)
{
Window hostWindow = FindParent((Button)e.OriginalSource);
hostWindow.DialogResult = true;
}
public void CancelAction(RoutedEventArgs e)
{
Window hostWindow = FindParent((Button)e.OriginalSource);
hostWindow.DialogResult = false;
}
传递的参数是$eventArgs
,因此该参数可用于获取对以编程方式生成的窗口的引用,该窗口以模态方式托管了 View。然后,可以设置DialogResult
。
关注点
通过命名空间约定进行的绑定以及使用操作和命令的多种方式使得代码量与替代方法相比非常少。这也有助于分离关注点和松散耦合。根据我的观察,尽管 API 在第二版中进行了重大的重组,但新的命名约定更加贴切。
链接
历史
- 2010-03-10 - HTML 修改。
- 2010-03-04 - 初始提交。