WPF:展示如何以一种非常不像 PRISM 的方式使用 PRISM






4.90/5 (27投票s)
展示如何在 PRISM 之外使用 PRISM 的区域。
引言
我有一段时间没有写关于框架,尤其是 MVVM 模式的文章了。我发现自己渴望能做一个大型项目;因此,我正准备更新我的 Cinch MVVM 框架,使其与即将发布的 .NET 4.0 / VS2010 版本保持一致。然而,在我开始这项工作之前,我想展示一下扩展 Cinch 是多么容易,以包含 PRISM/CAL 的某些区域,特别是 PRISM/CAL 的区域支持,因为这是 PRISM/CAL 中 Cinch 所不具备的一个方面。
PRISM 大杂烩 (我从未想过会用到这个词)
现在,我不知道你们有多少人使用过 PRISM/CAL,或者使用过类似的软件,比如 Smart Client Software Factory,尽管它与 PRISM/CAL 有很大的不同,但仍然有一些相似之处。
好了,这些都暂且不提;让我们专注于 PRISM/CAL 能为我们做什么。
PRISM/CAL 被宣传为“Composite WPF and Silverlight”。因此,它为开发人员提供了创建复合 UI 的能力,这些 UI 会被构建成一个 UI 应用程序。那么 PRISM/CAL 是如何做到的呢?嗯,它拥有一套相当不错的武器库来帮助实现这一点,例如:
Shell
通常会有一个单一的窗口作为应用程序的外壳。这实际上是 WPF 应用的一种相当普遍的做法,而且已经持续了一段时间。一些最好的 WPF 应用都是单窗口应用;看看 Expression Blend 就知道了。
这是一个单窗口应用。总之,在 PRISM/CAL 中,外壳只负责创建区域。
BootStrapper
引导程序负责加载所有分散的(看似无关的)模块,以便它们可以在单个外壳窗口中使用。这通常也包括创建外壳窗口并运行它。
模块
PRISM/CAL 提供了称为模块的功能,以帮助实现,嗯,构建复合系统的模块化。每个模块可以包含各种类,例如视图(通常是 UserControl),可能还有一个 ViewModel。这些模块被理解为代码单元,PRISM/CAL 的核心代码知道如何处理它们,以便将它们编译成可以在主应用程序外壳中使用的状态。
事件聚合器
这是一个断开连接的消息传递系统,它使用 WeakReference
对象,以便发送方/接收方可以自由地进行垃圾回收。事件聚合器还提供了将已发送消息调用到 UI 线程的能力。你可以将其视为一个通信通道,发送方和接收方彼此不了解,而是通过某个中间人进行通信。这在概念上与使用 Mediator 模式非常相似,这也是我的 Cinch MVVM 框架所使用的。
依赖注入
PRISM/CAL 大量使用 DI/IOC,它通过使用 Microsoft 自带的 IOC 容器 Unity application block 来实现。它的工作方式与大多数其他 IOC 容器大致相同;好的,有些容器有更少的语法需要掌握,但从概念上讲,它们的工作方式大致相同,它们都能够存储类型并注入所需的类型到构造函数/属性级别来解析类型依赖关系,并且它们都可以管理对象实例的生命周期(单例、新实例等)。
基本上,我们真正关心的是 PRISM/CAL 正在使用 DI/IOC 来解析各种类型。
区域支持
这是 PRISM/CAL 拼图中的最后一块,就是区域支持。有些人可能知道这是什么,而有些人可能不知道;好吧,简单来说,区域是指一个控件或屏幕上的区域,它能够接受内容添加到其中。 PRISM/CAL 提供了一种非常方便的方式来处理区域;你所需要做的就是在一个支持的区域控件上使用一个附加属性,据我所知,支持的控件如下:
ContentControl
Grid
StackPanel
TabControl
可能还有一些我遗漏了,抱歉。
这是一个在 XAML 中为外壳控件设置区域的示例
<TabControl cal:RegionManager.RegionName="MainRegion"/>
现在,你可以使用一个非常易于使用的语法动态地将新内容添加到 TabControl
中;你可以这样做:
View1 view = new View1();
regionManager.RegisterViewWithRegion("Someregion", () => view);
regionManager.Regions["Someregion"].Activate(view);
RegionAdaptors
这里稍微偏离主题,但仅供参考,如果你的 UI 需求需要使用更复杂的控件作为区域,那么不用担心,你也可以通过自定义 RegionAdaptor
来实现。
你可以在各种地方找到示例,但你真正需要做的就是继承自 RegionAdapterBase<T>
,并重写 Adapt()
和 CreateRegion()
方法,并在 bootStrapper
类中重写 ConfigureRegionAdapterMappings()
方法。
John Papa 有一个关于如何创建自定义 RegionAdaptor
的好例子,你可以在以下网址找到:Fill My region Please,或者对于一个稍微更具挑战性的 RegionAdaptor
,可以看看这个:WindowRegionAdapter for CompositeWPF。
按需取用
现在,有些人可能会想,是的,那又怎样 Sacha,你没告诉我多少,甚至还没展示任何代码。好吧,这是一篇很小的文章,但我不想在能快速了解 PRISM/CAL 之前就展示任何代码,至少这样一来,那些没有使用过它的人就能非常、非常快速地了解它是如何工作的。
我必须说,我不是 PRISM/CAL 的狂热粉丝,但我非常非常喜欢它的区域支持。我不知道你们有多少人读过我的 Cinch MVVM 框架文章;嘿,有些人甚至可能在使用我的 Cinch MVVM 框架。好吧,在过去的几个月里(自从我发布了这个框架以来),我收到了各种人的联系,他们谈论 PRISM/CAL 出色的区域支持,我告诉你,我同意,我爱 PRISM/CAL 的区域功能,只是我对其他部分不太感冒;别误会,P&P 的伙计们都很聪明,只是有时候对我来说有点太拘束了;你必须以某种方式做事。或者呢?)
嗯,实际上,不,你不用。你看,PRISM/CAL 的另一个优点是你可以只使用你想要的部分,而留下其余的部分。我喜欢区域,但不太关心其他部分,所以本文的其余部分将专门展示如何将 PRISM/CAL 与另一个 MVVM 框架一起使用。当然,我将使用我自己的 Cinch MVVM 框架,但在合适的时候,如果你们不想使用我的 Cinch MVVM 框架,我会说明另一种实现方式。
好了,说够了;让我们继续看看我如何让我的 Cinch MVVM 框架能够与 RISM/CAL 出色的区域支持一起工作。
那么如何在另一个 MVVM 框架中使用区域呢?
在 PRISM 之外实现区域支持并不难。至少,对我来说,让它与我的 Cinch MVVM 框架一起工作并不难。我只需要遵循以下简单步骤:
步骤 1:确保我有了所有必需的引用
我只需要确保附带的演示项目有正确的程序集引用,对我来说,这相当于:
正如我所说,演示应用程序是为与我的 Cinch MVVM 框架一起使用而编写的,所以如果你不使用它,你将 **不** 需要为 Cinch 文件夹中的程序集添加任何引用。
步骤 2:项目结构
如我上面所述,PRISM/CAL 使用 bootStrapper
,并且通常与模块一起工作。bootStrapper
是 **可选的**,因为它设置了 Unity IOC 容器。然而,模块实际上是可选的;我们根本不必使用模块,但我们必须重写 GetModuleCatalog()
方法来声明实际上没有模块可以加载到外壳中。
我应该提的一件事是,本文附带的演示应用程序显然假定所有文件都属于同一个项目。附加演示项目的项目结构如下:
如果你的项目需要与此略有不同,例如将 ViewModels 放在单独的程序集中,只需确保你自己的所有项目都有正确的引用,如前一小节中所述。
步骤 3:引导程序
正如我之前所说,PRISM/CAL 使用引导程序文件来确保各种事情(如外壳)得到设置。如果我们希望在自己的应用程序中使用 PRISM/CAL 的任何部分,我们也 **必须** 这样做。这是修改后的 bootStrapper
文件的完整代码:
using System.Windows;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.UnityExtensions;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
namespace CinchAndPrismRegions
{
class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
#region Setup Cinch with PRISM/CAL supporting service
//********************************************************************
//METHOD 1 :
//
//Add ViewManager to Cinch directly, to allow it to use
//PRISM/CAL regions. This is prefferred method
//********************************************************************
IRegionManager regionManager = base.Container.Resolve<IRegionManager>();
Cinch.ViewModelBase.ServiceProvider.Add(typeof(IViewManager),
new ViewManager(regionManager));
//********************************************************************
//METHOD 2 :
//
//Add ViewManager to Cinch via Unity, to allow it to use
//PRISM/CAL regions. This is strictly not really required, as all it does
//is make the Unity IOC container aware of the IViewManager service. However
//any interaction with the IViewManager and Cinch will be via the
//Cinch ViewModelBase.ServiceProvider and Cinch ViewModelBase.Resolve<T>()
//methods.
//
//I just thought it good practice to add it to Unity, on the off chance
//that it may be used elsewhere. Though as I say with Cinch this is very
//unlikely
//********************************************************************
//base.Container.RegisterType<IViewManager,ViewManager>(
// new InjectionConstructor(base.Container.Resolve<IRegionManager>()));
//var viewManager = base.Container.Resolve<IViewManager>();
//Cinch.ViewModelBase.ServiceProvider.Add(typeof(IViewManager), viewManager);
#endregion
//Create CAL shell
//NOTE : Shell is without modules, as I want it to be a
//bulk standard MVVM app just with added
//regionManager support)
Shell shell = new Shell();
shell.Show();
return shell;
}
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog();
return catalog;
}
}
}
从这段代码中,只有两个重要的事情需要注意:
- 我们正在创建一个(或者创建并存储在 Unity IOC 容器中,以防
IViewManager
服务在其他地方需要,严格来说,这是不需要的,但我只是想展示如何向 Unity IOC 容器添加服务)IViewManager
的实例(这是一个我编写的用于处理区域的服务)。这个IViewManager
需要一个IRegionManager
作为构造函数参数。IRegionManager
服务是一个 PRISM/CAL 类型,可以从 Unity IOC 容器中获取,所以你可以在上面的代码中看到IRegionManager
被注入到了IViewManager
中,通过从 Unity 获取IRegionManager
服务的实例。你还会注意到IViewManager
服务随后被存储在 Cinch 的ViewModelBase ServiceProvider
实例中。这允许所有 Cinch ViewModel 使用IViewManager
服务。此步骤特定于 Cinch。所以如果你不使用 Cinch,你可以简单地将IViewManager
服务存储在一个单例中,或者甚至放在一个基本 ViewModel 类的属性上,基本上只是一个你可以轻松使用它的地方。 - 下一步是告诉 PRISM/CAL 我们实际上不想使用模块。我们通过重写
GetModuleCatalog()
方法来实现这一点。
步骤 4:创建支持区域的 UI 服务
所以,如果我们想处理区域,我们应该创建一个可重用的对象,我们可以到处使用。服务通常就是做这个的。所以我创建了一个简单的支持区域的服务,我称之为 ViewManager
,它看起来很简单:
服务定义
/// <summary>
/// A simple region service contract
/// that works with PRISM/CALs regions
/// </summary>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CinchAndPrismRegions
{
public interface IViewManager
{
void CreateAndShowViewInRegion(String regionName, Type viewType);
void ShowViewInRegion(String regionName, Object viewInstance);
}
}
服务实现
这是一个与 PRISM/CAL 区域配合工作的最小服务:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
namespace CinchAndPrismRegions
{
/// <summary>
/// A simple region supporting UI service
/// that works with PRISM/CALs regions
/// </summary>
public sealed class ViewManager : IViewManager
{
private IRegionManager regionManager;
public ViewManager(IRegionManager regionManager)
{
this.regionManager = regionManager;
}
/// <summary>
/// Creates and shows a view in the region specified by the regionName
/// input parameter
/// </summary>
/// <param name="regionName">The region name to put view in</param>
/// <param name="viewType">The type of the view to create</param>
public void CreateAndShowViewInRegion(String regionName, Type viewType)
{
var content = Activator.CreateInstance(viewType);
regionManager.RegisterViewWithRegion(regionName, () => content);
regionManager.Regions[regionName].Activate(content);
}
/// <summary>
/// Shows the view instance in the region specified by the regionName
/// input parameter
/// </summary>
/// <param name="regionName">The region name to put view in</param>
/// <param name="viewInstance">The instance of the view to create</param>
public void ShowViewInRegion(String regionName, Object viewInstance)
{
regionManager.RegisterViewWithRegion(regionName, () => viewInstance);
regionManager.Regions[regionName].Activate(viewInstance);
}
}
}
这就是使用 PRISM/CAL 与你自己的 MVVM 框架所需了解的全部内容;当然,我使用了 Cinch,它已经支持 UI 服务,所以如果你使用自己的 MVVM 框架,这只是你需要注意的一件事。
那么,来个小演示怎么样?
没有小演示的代码有什么用?为此,我创建了一个小型演示应用程序,它允许以下操作:
- 有一个单一的主窗口(外壳),它承载了一个标准的 PRISM/CAL 区域启用控件,一个
TabControl
。外壳窗口使用一个名为ShellViewModel
的小型 ViewModel。 - 外壳中显示了两个非常简单的视图。这些视图在外壳中的显示是由
ShellViewModel
中的代码完成的。
这是外壳窗口的所有 XAML 代码:
<Window x:Class="CinchAndPrismRegions.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="http://www.codeplex.com/CompositeWPF"
Title="Hello World" Height="300" Width="300">
<Window.Resources>
<Style TargetType="{x:Type TabItem}" x:Key="TabItemRegionStyle">
<Setter Property="Header"
Value="{Binding RelativeSource={RelativeSource Self},
Path=Content.DataContext.HeaderText}" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Button Margin="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Content="Show View1" Command="{Binding ShowView1InRegionCommand}"/>
<Button Margin="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Content="Show View2" Command="{Binding ShowView2InRegionCommand}"/>
</StackPanel>
<TabControl Grid.Row="1" Name="MainRegion" cal:RegionManager.RegionName="MainRegion"
ItemContainerStyle="{StaticResource TabItemRegionStyle}"/>
</Grid>
</Window>
这里有两点需要注意:
- 附加的
RegionManager
属性,这意味着使用此附加属性的TabControl
现在可以使用 PRISM/CAL 的区域。 TabControl
使用了ItemContainerStyle
。这个ItemContainerStyle
简单地显示一段文本,该文本是当前视图的名称,从活动视图的 ViewModel 中获取。
好的,这就是外壳的 XAML;那么使用区域的 ViewModel 呢?好吧,这是它的全部代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
namespace CinchAndPrismRegions
{
public class ShellViewModel : Cinch.ViewModelBase
{
private SimpleCommand showView1InRegionCommand;
private SimpleCommand showView2InRegionCommand;
public ShellViewModel()
{
showView1InRegionCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteShowView1InRegionCommand()
};
showView2InRegionCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteShowView2InRegionCommand()
};
}
public SimpleCommand ShowView1InRegionCommand
{
get { return showView1InRegionCommand; }
}
public SimpleCommand ShowView2InRegionCommand
{
get { return showView2InRegionCommand; }
}
private void ExecuteShowView1InRegionCommand()
{
this.Resolve<IViewManager>().CreateAndShowViewInRegion(
"MainRegion", typeof(View1));
}
private void ExecuteShowView2InRegionCommand()
{
this.Resolve<IViewManager>().ShowViewInRegion(
"MainRegion", new View2());
}
}
}
为了完整起见,这是其中一个 ViewModel 的代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
using System.ComponentModel;
namespace CinchAndPrismRegions
{
public class View1ViewModel : Cinch.ViewModelBase
{
private String headerText = String.Empty;
public View1ViewModel()
{
HeaderText = "View1";
}
/// <summary>
/// Bound Type for search
/// </summary>
static PropertyChangedEventArgs headerTextChangeArgs =
ObservableHelper.CreateArgs<View1ViewModel>(x => x.HeaderText);
public String HeaderText
{
get { return headerText; }
set
{
headerText = value;
NotifyPropertyChanged(headerTextChangeArgs);
}
}
}
}
这就是最终结果;我们可以点击其中一个按钮(它会调用 ShellViewModel
中的 ICommand
),然后在 Shell
窗口的“mainRegion”(如果你还记得的话,那就是 TabControl
)中显示一个新的视图。
就这些
总之,这就是我目前想说的;但正如我在引言中提到的,我正准备对我自己的 Cinch MVVM 框架进行一些重大更新,以使其与即将发布的 .NET 4.0 / VS2010 版本保持一致。