使用 MVVM 和 Castle Windsor 的 WPF 模板解决方案
一个使用 Castle Windsor 和常用工具的 WPF 应用程序基础解决方案。
引言
我即将开始一个长期的项目,需要从头开始构建一个 WPF 客户端应用程序。在浏览网络寻找最新的设计模式和代码解决方案时,我看到了几种可以作为我项目基础的方法(Prism、MEF、Unity 等),但没有一篇文章提供一个我可以直接下载并开始构建代码块的完整综合性基础解决方案。
因此,我决定构建一个包含 WPF 客户端应用程序所需一切的基础解决方案,并使其尽可能通用,以便成为我未来构建的任何 WPF 应用程序的良好起点。
换句话说,我希望创建一个基本的、可下载的 WPF 工具包,其中包含所有常用的实用程序,即线程管理器、消息调度器、日志、单元测试、帮助管理器、配置文件管理器以及一个基于主对接面板的屏幕。哦,还有一个好的设计模式来管理所有这些。
因此,当为我的项目(一个 WPF 客户端应用程序)寻找一个好的框架设计时,我的主要目标不仅是基于 MVVM 模式,还要将所有其他程序集完全分离,以实现我解决方案中每个 DLL 的完整独立性。这将使我在应用程序成型时,能够轻松地单独维护和版本控制每个模块。
问题
所以,在实现一个基于模块化组件的应用程序时,主要困难在于如何在不引用其程序集的情况下访问一个类。当然,这可以通过控制反转来解决——但要拥有一个真正纯粹的 IOC 环境,您必须正确实现 IOC,因此,只有一个地方调用 IOC 的 Register
、Resolve
和 Release
方法。
Castle Windsor
为了在我的应用程序中实现这种模块化、基于组件的设计,我决定使用Castle 项目的 Castle Windsor。Castle Windsor 是 .NET 和 Silverlight 中最优秀、最成熟的控制反转容器。简单来说,Castle 迫使您以一种方式设计代码,如果您构建了一个良好的基础设施,那么在开发过程中保持模块的分离将是一项相当容易的任务。
Using the Code
在使用 Castle 构建的 MVVM 应用程序时,您主要需要遵守四个基本规则:
- 接口是您使用的所有东西的基础。
- 智能注册模块。
- 注入控制。
- 分开,不要分散。
让我们以 **Map 库** 为例,指导您如何将库添加到模板解决方案中。Map 库是我添加到基本模板解决方案中的唯一一个代码块库。在添加您的代码块时,请遵循以下步骤来维护 MVVM 设计并通过 Castle 框架进行管理。
接口是基础
由于在应用程序成熟后接口很少更改,因此您的目标是在主项目中只引用 Interfaces 库。如果您发现需要在主项目中引用其他库,请将所需内容重构到 Interfaces 库中的一个接口中。
namespace Interfaces
{
public interface IMapViewModel : IViewModel
{
/// <summary>
/// Gets or sets the model.
/// </summary>
/// <value>
/// The model.
/// </value>
IMapModel TheModel { get; set; }
/// <summary>
/// Handles the help.
/// </summary>
/// <param name="source">The source.</param>
void HandleHelp(object source);
}
}
namespace Interfaces
{
public interface IViewModel
{
IHelpManager Help { get; set; }
}
}
您可以在这里看到我的 Map ViewModel 接口,它继承自通用的 IViewModel
接口。IViewModel
中的 Help
属性将由 Castle 框架注入(即 创建
和设置
)。对于 Map View 的 Model
类 IMapModel
也是如此。
智能注册库
为了能够利用 Castle 的优势,并将您的对象注入到各个类和模块中,您首先需要注册它们。这也被称为安装。
/// <summary>
/// Performs the installation in the <see cref="T:Castle.Windsor.IWindsorContainer">.
/// </see></summary>
/// <param name="container" />The container.
/// <param name="store" />The configuration store.
public void Install(IWindsorContainer container, IConfigurationStore store)
{
try
{
//Adding this code into the installer tells castle that we
//are about to specify a new type factory, we now just need
//to add a new line to identify the factory as shown below
container.AddFacility<typedfactoryfacility>();
container.Register(Component.For<iabstructfactory>().AsFactory());
container.Register(Component.For<ishell<mainwindow>>()
.ImplementedBy<shell>().LifestyleTransient());
container.Register(Classes.FromAssemblyInDirectory(
new AssemblyFilter(AssemblyDirectory))
.BasedOn<iview>()
.Configure(c => c.LifestyleTransient().Named(c.Implementation.Name))
.WithService.Base()
.WithService.FromInterface(typeof(IView)));
container.Register(Classes.FromAssemblyInDirectory(
new AssemblyFilter(AssemblyDirectory))
.BasedOn<iviewmodel>()
.Configure(c => c.LifestyleTransient().Named(c.Implementation.Name))
.WithService.Base()
.WithService.FromInterface(typeof(IViewModel)));
container.Register(Types.FromAssemblyInDirectory(
new AssemblyFilter(AssemblyDirectory + "\\CommonData"))
.Where(type => type.Name.StartsWith("Common"))
.WithService.AllInterfaces());
container.Register(Types.FromAssemblyInDirectory(
new AssemblyFilter(AssemblyDirectory + "\\CommonGui"))
.Where(type => type.Name.StartsWith("Common"))
.WithService.AllInterfaces());
container.Register(Types.FromAssemblyInDirectory(
new AssemblyFilter(AssemblyDirectory + "\\CommonUtilities"))
.Where(type => type.Name.StartsWith("Common"))
.WithService.AllInterfaces());
container.Register(Types.FromAssemblyInDirectory(
new AssemblyFilter(AssemblyDirectory + "\\Map"))
.Where(type => type.Name.StartsWith("Map"))
.WithService.AllInterfaces());
container.Register(Component.For<iviewfactory>()
.AsFactory().LifestyleTransient());
container.Register(Component.For<mainwindow>().LifestyleTransient());
}
catch (Exception ex)
{
MessageBox.Show("Error installing some components: " + ex.Message);
}
}
所以,如果我们关注这个 Install 块中的 Map 库注册部分,您可以看到我正在使用 <FromAssemblyInDirectory>
选项来定位我的 Map 库。这就是我如何避免在主项目中引用 Map 库,并在应用程序启动时动态加载它的方法。
注入控制
现在我已经告诉 Castle 在名为 *Map* 的目录中查找我所有的 Map
类,该目录位于我的 *Assembly* 目录之下,我可以确信 Castle 已经从我的 Map
库中找到的所有类和接口创建了一个链接表,并且知道如何相应地创建和注入它们。
/// <summary>
/// Initializes a new instance of the <see cref="MainWindow"> class.
/// </see></summary>
/// <param name="context" />The context.
public MainWindow(IAbstructFactory context)
{
InitializeComponent();
try
{
#region Creating objects
IMapView theMapView = context.Create<imapview>();
#endregion
#region Releasing objects
context.Release(theMapView);
#endregion
}
catch (Exception ex)
{
MessageBox.Show("Failed to create components: " + ex.Message);
}
}
您可以看到,在我的主窗口中,我只创建了 IMapView
。
Castle 将负责所有其他的初始化,并将执行以下操作:
- 创建并注入
IMapViewModel
,它是MapView
中的一个属性。 - 创建并注入
IViewModel
和IHelpManager
,它们是IMapViewModel
中的属性。
分开,不要分散
重要的是要记住,Castle 和 MVVM 在应用程序发展到拥有数千行代码的成熟阶段时,可以为您作为开发人员简化维护和构建应用程序的工作。要明智地选择应用程序的设计方式——也就是说,过度设计的(OOD)会导致分散、难以管理、难以维护的应用程序。
我的源代码
我的源代码显示了一个带有对接面板的主窗口。其中一个面板包含 MapView
用户控件,显示时间,并有一个按钮,点击后会显示一个 CHM 帮助文件。
时间是通过线程更新的,总的来说,这个解决方案是为多线程应用程序设计的,因此它有一个线程管理器和一个倒计时对象,用于监视线程的初始化和终止。
关注点
关于本文,我想强调以下几点:
- 在此解决方案中,我使用了几个开源库,如 Nog、Avalon Dock、Event Broker 和 Castle Windsor。
- 我在代码中添加了必要的致谢,但如果我遗漏了什么,并非有意,我会进行更正。
- 这是我的第一篇文章,所以投票时请多多包涵 :)