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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (27投票s)

2010年4月22日

CPOL

11分钟阅读

viewsIcon

142985

downloadIcon

2088

展示如何在 PRISM 之外使用 PRISM 的区域。

引言

我有一段时间没有写关于框架,尤其是 MVVM 模式的文章了。我发现自己渴望能做一个大型项目;因此,我正准备更新我的 Cinch MVVM 框架,使其与即将发布的 .NET 4.0 / VS2010 版本保持一致。然而,在我开始这项工作之前,我想展示一下扩展 Cinch 是多么容易,以包含 PRISM/CAL 的某些区域,特别是 PRISM/CAL 的区域支持,因为这是 PRISM/CALCinch 所不具备的一个方面。

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;
        }
    }
}

从这段代码中,只有两个重要的事情需要注意:

  1. 我们正在创建一个(或者创建并存储在 Unity IOC 容器中,以防 IViewManager 服务在其他地方需要,严格来说,这是不需要的,但我只是想展示如何向 Unity IOC 容器添加服务)IViewManager 的实例(这是一个我编写的用于处理区域的服务)。这个 IViewManager 需要一个 IRegionManager 作为构造函数参数。IRegionManager 服务是一个 PRISM/CAL 类型,可以从 Unity IOC 容器中获取,所以你可以在上面的代码中看到 IRegionManager 被注入到了 IViewManager 中,通过从 Unity 获取 IRegionManager 服务的实例。你还会注意到 IViewManager 服务随后被存储在 CinchViewModelBase ServiceProvider 实例中。这允许所有 Cinch ViewModel 使用 IViewManager 服务。此步骤特定于 Cinch。所以如果你不使用 Cinch,你可以简单地将 IViewManager 服务存储在一个单例中,或者甚至放在一个基本 ViewModel 类的属性上,基本上只是一个你可以轻松使用它的地方。
  2. 下一步是告诉 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>

这里有两点需要注意:

  1. 附加的 RegionManager 属性,这意味着使用此附加属性的 TabControl 现在可以使用 PRISM/CAL 的区域。
  2. 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 版本保持一致。

© . All rights reserved.