65.9K
CodeProject 正在变化。 阅读更多。
Home

复合 WPF 按需显示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (9投票s)

2009年8月30日

CPOL

6分钟阅读

viewsIcon

48059

downloadIcon

1353

Composite WPF 按需显示。

引言

注意: 本文已被 Prism 2.1 for WPF 入门 所取代,后者更详细地涵盖了相同的内容。本文保留给仍在使用 Prism 1.0 的用户。

2008 年 6 月,Microsoft 发布了Composite Client Application Guidance 的 1.0 版本,该版本提供了用于在 WPF 中开发复合应用程序的框架。2009 年 2 月,Microsoft 发布了 2.0 版本,该版本将框架扩展到 Silverlight。这两个框架通常被称为Prism,这是它们在开发期间的代码名称。

Prism 附带的大多数快速入门和示例应用程序都隐式地假定所有模块都在启动时加载,并立即持续显示。但这通常并非如此。例如,请考虑 Microsoft Outlook,它会根据用户在电子邮件、日程安排和任务管理之间切换任务来动态替换 UI 中的工作区面板。

Prism 构建于松耦合模块的思想之上,似乎非常适合此类 UI。但奇怪的是,关于如何使用 Prism 创建此类 UI 的文档并不多。本文介绍了一种实现此类界面的方法。它包括一个非常简单的演示项目,展示了如何做到这一点。

本文假设您对 Prism 和“控制反转容器”有基本了解,Prism 依赖于它们。如果您不熟悉这些内容,MSDN 提供了一个很好的起点。演示项目使用 Prism 1.0,并包含必要的程序集。该演示在进行少量修改后,同样适用于 Prism 2.0。演示应用程序是用 C# 编写的,但可以轻松地转换为 VB.NET。

问题

本文将要解决的问题是:如何在 C# 代码中显示和隐藏模块视图?这与另一个相关但不同的问题略有不同:如何按需加载模块?MSDN 有一个很好的操作指南来解决这个问题。基本上,按需加载会将模块加载推迟到代码中请求模块时。我们将要做的是在启动时加载所有模块,但将模块的显示推迟到代码中请求时。这就是为什么我们将问题称为按需显示,而不是按需加载。本文提出的解决方案可以通过将 IRegion.Activate()IRegion.Deactivate() 的调用更改为 IRegion.Add() IRegion.Remove() 方法,并进行其他微小更改,来适应按需加载和卸载模块。 Matias Bonaventura 的博客有一篇文章和一个演示应用程序,可能对此有所帮助。

演示应用程序

本文附带的演示应用程序演示了 Composite WPF 应用程序中的按需显示。该应用程序非常简单;实际上,它只不过是一个“Hello World”应用程序。但是,它包含了实现按需显示的完整代码。

关于演示应用程序,有一点特别值得注意。该应用程序使用了 Model-View-ViewModel 模式的宽松形式。我之所以说是“宽松”,是因为 Shell 视图模型同时服务于应用程序中的 Shell 和两个模块。在生产应用程序中,每个模块可能都应该有自己的视图模型,以最大限度地减少与 Shell 的耦合。

应用程序的工作原理如下:Shell 窗口中的 ContentControl 在 XAML 中被设置为唯一的 Composite WPF 区域。Shell 窗口中的按钮绑定到ShellViewModel.cs 中的命令属性。这种方法将代码排除在代码隐藏之外,从而提高了可测试性。

视图模型命令属性实现为 ICommands。我的 ICommands 通常与其他地方的实现不同。我将每个 ICommand 创建为一个单独的对象,并将执行命令的代码放在其 Execute() 方法中。常用函数,如 ClearRegion() 方法,被放入 static 服务类中。在生产应用程序中,我会以略微不同的方式构建事物以降低耦合度。在演示应用程序中,我们保持事物相对简单。

演练

当应用程序启动时,Bootstrapper 以通常的 Composite WPF 方式初始化两个模块:Module A 和 Module B。每个模块的 Initialize() 方法都会加载模块,但不会激活它。

public void Initialize()
{
    // Get main region
    var mainRegion = m_RegionManager.Regions["MainRegion"];
 
    // Load Module A
    var view = new ModuleAView();
    mainRegion.Add(view, "ModuleA.ModuleAView");
}

请注意,我们在 mainRegion.Add() 的第二个参数中为视图指定的名称。我们为模块指定的名称等于其完全限定类名。这是调用视图实例的 ToString() 时返回的值。正如我们在下面将看到的,这种方法是无需了解视图的任何信息即可检索视图的关键。

由于视图已加载但未初始化,因此当演示应用程序加载时,它会显示一个空的 Shell。

当您单击按钮时,其绑定会调用ShellViewModel.cs 中的命令属性,该属性又会调用相应的 ICommand。例如,单击“Load Module A”按钮最终会调用 LoadModuleACommand.Execute() 方法。

public void Execute(object parameter)
{
    // Get main region
    var mainRegion = m_ViewModel.RegionManager.Regions["MainRegion"];
 
    // Clear region
    ModuleServices.ClearRegion(mainRegion);
 
    // Activate Module A view
    var moduleAView = mainRegion.GetView("ModuleA.ModuleAView");
    mainRegion.Activate(moduleAView);
}

该方法首先从 Prism 区域管理器获取主区域,该区域管理器从 Shell 视图模型中获取。然后,该命令调用 ModuleServices.ClearRegion() 方法以停用任何当前活动的视图。正如我上面提到的,此方法包含在一个 static 服务类中。

public static void ClearRegion(IRegion region)
{
    // Get existing view names
    var oldViewNames = new List<string>();
    foreach (var v in region.Views)
    {
        var s = v.ToString();
        oldViewNames.Add(s);
    }
 
    // Remove existing views
    foreach (var oldViewName in oldViewNames)
    {
        var oldView = region.GetView(oldViewName);
        region.Deactivate(oldView);
    }
}

该方法首先编译视图名称列表。我们无法获取每个视图的 Name 属性,因为在 IRegion.Views 集合中,视图被转换为 object 类型。但我们可以调用视图的 ToString() ,它将返回视图的完全限定名。由于我们之前已将视图的名称设置为其完全限定名,因此我们从 ToString() 返回的值实际上就是视图的名称。

为什么我们要使用 foreach 迭代器,而实际上我们只想停用一个视图?我在演示应用程序中采用的方法是停用所有视图,这样调用代码就不需要知道哪个视图是活动的。但其他方法也应该有效,例如在视图激活时将当前视图的名称存储在视图模型中,然后在加载不同视图时回忆它以进行停用。由于我的 ICommands 包含对视图模型的引用,因此这很容易做到。

按钮单击的结果非常简单。请求模块的视图已加载并显示如下:

Shell 中的 Unload 按钮的工作方式基本相同。由于我们知道要停用哪个视图,因此只需将名称传递给主区域即可。

结论

真的没有什么可多说的了。这是我第一次解决这个问题,我确信解决方案会随着时间的推移而演变。发布本文的一个原因是将其提交给 CodeProject 的同行评审。如果您发现任何错误,或者可以为解决方案提出改进建议,非常欢迎您的评论。

历史

  • 2009 年 8 月 29 日:初始版本
© . All rights reserved.