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

展示 Cinch MVVM 框架 / Prism 4 互操作性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (38投票s)

2011年1月11日

CPOL

22分钟阅读

viewsIcon

191723

downloadIcon

3601

向您展示如何轻松地将 CinchV2 与 Prism 4 结合使用。

引言

我知道你们中有些人会知道我是一个WPF爱好者,并且我有一个名为 Cinch 的MVVM框架,而且我不久前发布了一系列关于 Cinch V2 的文章,你们可能已经听腻了,好吧,有时我也是,但有人问我让 CinchPrism 协同工作有多容易,我不得不尝试一下。

因此,本文将演示如何轻松地将我自己的 MVVM 框架 Cinch V2 与 Microsoft 复合 WPF/SL 应用程序块(又名:Prism)中您所钟爱的所有优秀组件结合使用。

哦,我应该提一下,Cinch 还依赖于另一位 WPF 弟子 Marlon Grech 的 MEF View/ViewModel 解析框架,名为 MEFedMVVM。这并不是什么新鲜事,那些读过所有以前的 Cinch 文章的人都会知道。我知道我的库依赖 Marlon 的库似乎很奇怪,但我对 MEFedMVVM 只有赞美之词,并且从未遇到任何问题,除了有一次我更新了 Cinch 正在使用的 MEFedMVVM 版本,当时我恰好遇到了 Marlon 正在“实验”的那个版本,天佑。话虽如此,从那以后,MEFedMVVM 从未出现过问题;我一点也不担心这种依赖关系,我想您也不应该担心。

Prism V4

我不知道你们中有多少人了解 Prism 是什么,或者是否关注它的活跃版本,但 Prism 是 Microsoft 的复合 WPF/SL 应用程序块。如果你使用过 WinForms 的 Smart Client Software Factory (SCSF),它可能看起来有点熟悉,但 Prism 与 SCSF 完全不同,它是从头开始编写的,旨在与 WPF 和后来的 Silverlight 协同工作。

它现在是第四个版本,提供以下功能:

  • 模块(这些以独立项目的形式出现,其中包含创建离散工作单元所需的一切,因此,如果您正在进行 MVVM,这可能是一个视图/视图模块和数据访问层助手)
  • 区域,是 UI 中被标记为占位符的部分(类似于 ASP.NET 占位符),可以在运行时容纳其他内容
  • Shell(单一启动窗口)
  • 引导程序,用于将所有组件连接在一起

现在,有很多人认为 Prism 是一个 MVVM 框架,但它不是。它可能有助于实现 MVVM,但事实是,即使经过四个版本,它仍然缺少 MVVM 难题的一些核心部分。因此,人们编写了许多 MVVM 库;这不是因为我们都很迟钝,而是出于真正的需求。Prism 没有您需要的所有部分,所以人们(包括我自己)编写了 MVVM 框架来尝试填补这个空白。

这并不是说 Prism 不好,它一点也不差,而且其区域支持本身就非常有吸引力,但它仍然缺少一些东西,例如您期望 MVVM 框架提供的核心服务,例如 MessageBox、模态窗口等。为什么会这样?嗯,很简单:Prism 不是一个 MVVM 框架,它是一个复合 UI 块,确实有一些花哨的功能使其可以用于 MVVM 相关的事情。在我看来,Prism 应该与您最喜欢的 MVVM 框架一起使用。

正如我所说,Prism 现在是第四个版本。以前的版本都使用 Unity 应用程序块进行依赖注入/IOC 容器。版本 4 不同,它也可以使用 MEF。巧合的是,Cinch V2 也使用 MEF,所以本文将向您展示,您可以毫无问题地将 Cinch V2Prism (v4) 结合使用。

事实上,我甚至可以说它们是相得益彰的优秀框架组合。

演示 1:架构概述

演示 1 可在本文章顶部找到,文件名为 CinchV2AndPrismRegions.zip

此演示不使用 Prism 模块,但它展示了如何毫无困难地将 Prism 区域与 Cinch V2 结合使用。它还向用户展示了如何创建自定义 Prism 区域适配器。

有一个单独的 shell 窗口,它有一个单独的 TabControl 区域(使用标准的 Prism TabControl 区域适配器),该区域将在运行时加载两个视图。

有一个欢迎视图,它只是显示一些简单的富文本,并利用 Cinch V2 MEF 注入的 ViewModel。

有一个图像视图,它显示与关键词驱动的 Google 搜索匹配的图像列表,并允许用户单击其中一个检索到的图像,并在自定义区域适配器中将其显示为更大的图像。图像视图还利用 Cinch V2 MEF 注入的 ViewModel。

演示 1:它长什么样子?

这个演示比第二个演示更精致一些,但话说回来,区域/模块的整体概念也更复杂,所以我把第二个演示应用程序保持得很简单。如前所述,第一个演示有一个欢迎视图,它在启动时预加载到 Shell 中,在 Shell 中的样子如下图所示。

该演示还允许使用横幅(左上角)下方的按钮加载另一个随机 Google 图像搜索视图的多个实例,但 Shell 在启动时预加载了一个随机图像视图,设置为在 Google 上搜索“鲜花”,其外观如下:

注意:这是从一个元素过渡到另一个元素的过程中,稍后会详细介绍。

我添加了一个小型线程助手控件,所以当它正在从 Google 获取数据时,它会是这个样子:

要加载此视图的新实例并加载更多随机 Google 图片,您可以使用以下按钮:

演示 1:它是如何工作的?

接下来的几节将引导您了解其工作原理。

演示 1:Shell

构建此演示时,我执行的第一步是创建 Shell,这显然意味着获取所有相关的 DLL 引用,但您可以从实际的附加演示代码中看到这一点。Shell 是一个非常简单的 Window,它具有以下 XAML,它实际上只是一个用于加载其他视图的区域容器。

这是完整的 Shell XAML:

<Window x:Class="CinchV2AndPrismRegions.Shell"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cal="clr-namespace:Microsoft.Practices.Prism.Regions;
               assembly=Microsoft.Practices.Prism"  
    xmlns:regions="clr-namespace:CinchV2AndPrismRegions.Regions"
    xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
    xmlns:meffed="clr-namespace:MEFedMVVM.ViewModelLocator;assembly=MEFedMVVM.WPF"
    xmlns:i="clr-namespace:System.Windows.Interactivity;
             assembly=System.Windows.Interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
    Icon="/CinchV2AndPrismRegions;component/Images/CinchIcon.png"
    Title="Shell" 
    WindowState="Maximized"
    WindowStyle="ThreeDBorderWindow"
    WindowStartupLocation="CenterScreen"
    Width="800"
    Height="600"
    meffed:ViewModelLocator.ViewModel="ShellViewModel">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="87"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0"  Height="87" 
               HorizontalAlignment="Stretch">

            <Image Height="86" Width="462" 
                   Source="/CinchV2AndPrismRegions;component/Images/BannerLeft.png" 
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"/>

            <Image Height="86" Width="244" 
                   Source="/CinchV2AndPrismRegions;component/Images/BannerRight.png" 
                   HorizontalAlignment="Right"
                   VerticalAlignment="Top"/>

            <Rectangle Fill="Black" VerticalAlignment="Bottom" 
               Height="7" HorizontalAlignment="Stretch"/>

        </Grid>

        <DockPanel LastChildFill="True" Grid.Row="1" 
                   Background="{StaticResource verticalTabHeaderBackground}">


            <Image Height="32" Width="32" 
                   Margin="10,2,2,2" DockPanel.Dock="Top"
                   Source="/CinchV2AndPrismRegions;component/Images/google.png" 
                   HorizontalAlignment="left"
                   VerticalAlignment="Center" 
                   ToolTip="Add New Google Image Search View">

                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseLeftButtonUp">
                        <CinchV2:EventToCommandTrigger 
                                 Command="{Binding AddNewGoogleCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>

                <Image.Effect>
                    <DropShadowEffect ShadowDepth="0" 
                      Color="White" BlurRadius="10" />
                </Image.Effect>
            </Image>


            <TabControl Margin="0"  
                Style="{StaticResource TabControlStyleVerticalTabs}"
                ItemContainerStyle="{StaticResource TabItemStyleVerticalTabs}"
                cal:RegionManager.RegionName="{x:Static regions:RegionNames.MainRegion}"/>
        </DockPanel>
    </Grid>
</Window>

这是它的代码隐藏;请注意,它被标记为 MEF ExportAttribute,这允许 MEF CompositionContainer(我们接下来会看到)能够解析 Shell 类型以及 Shell 可能需要的任何 MEF Import。应该注意的是,在此演示中,Shell 不需要满足任何其他 Import,但在实际代码中,它很可能会需要,因此 Export Shell 是一个好习惯;这也是 Prism 在 V4 中工作的实际方式。

[Export("CinchV2AndPrismRegions.Shell", typeof(Shell))]
public partial class Shell : Window
{
    public Shell()
    {
        InitializeComponent();
    }
}

还应注意的是,Shell 正在使用 MEFedMVVM 来解析其 ViewModel,其类型为 ShellViewModelShellViewModel 基本上启动并立即使用单个 WelcomeView 和单个 GoogleImageSearchView 填充 TabControl 区域“MainRegion”;如下图所示。ShellViewModel 还允许创建 GoogleImageSearchView 的新实例,其中 GoogleImageSearchView 将具有由 ShellViewModel 设置的一些上下文数据,这些数据将决定新创建的 GoogleImageSearchView 中区域的名称。需要注意确保区域名称在同一父范围内是唯一的。更多内容见下文。

目前,这是 ShellViewModel 的完整列表:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using System.ComponentModel.Composition;
using Cinch;
using System.ComponentModel;
using Microsoft.Practices.Prism.Regions;
using System.Windows;
using CinchV2AndPrismRegions.Regions;
using CinchV2AndPrismRegions.Views;
using System.ComponentModel.Composition.Primitives;

namespace CinchV2AndPrismRegions.ViewModels
{
    [ExportViewModel("ShellViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class ShellViewModel : Cinch.ViewModelBase
    {
        #region Data

        private int searchViewsInstanceCounter = 0;
        private int searchViewsCounter = 0;

        private String[] randomSearchTerms = new string[]
        {
            "alien","robots","shoebill",
            "girls","lazer","martians","manga",
            "anime","lizard","elf","lizard",
            "dog","gun","gangsta","soldier",
            "monsters","Zombie","goat"
        };
        private Random rand = new Random();
        private IViewAwareStatus viewAwareStatus;
        private IMessageBoxService messageBoxService;
        #endregion

        [ImportingConstructor]
        public ShellViewModel(IViewAwareStatus viewAwareStatus,
            IMessageBoxService messageBoxService)
        {
            this.viewAwareStatus = viewAwareStatus;
            this.messageBoxService = messageBoxService;
            this.viewAwareStatus.ViewLoaded += ViewAwareStatus_ViewLoaded;

            //Commands
            AddNewGoogleCommand = 
              new SimpleCommand<Object, Object>(ExecuteAddNewGoogleCommand);


            Mediator.Instance.Register(this);
        }

        public SimpleCommand<Object, Object> AddNewGoogleCommand { get; private set; }

        #region Private Methods

        private void ViewAwareStatus_ViewLoaded()
        {
            IRegionManager regionManager = 
                RegionManager.GetRegionManager((DependencyObject)viewAwareStatus.View);
            IRegion region = regionManager.Regions[RegionNames.MainRegion];
            
            WelcomeView welcomeView = ViewModelRepository.Instance.Resolver
                .Container.GetExport<WelcomeView>().Value;
            region.Add(welcomeView, "preloadedWelcomeView");

            GoogleImageSearchView googleImageSearchView = 
                ViewModelRepository.Instance.Resolver
                .Container.GetExport<GoogleImageSearchView>().Value;
            googleImageSearchView.ContextualData = 
                new Model.GoogleImageSearchInfo("Flower", 
                    string.Format("Imageregion_{0}", searchViewsCounter++));

            region.Add(googleImageSearchView, "preloadedGoogleImageSearchView");
            searchViewsInstanceCounter++;
            region.Activate(welcomeView);
        }

        [MediatorMessageSinkAttribute("DecrementSearchCount")]
        public void OnDecrementSearchCount(bool dummy) 
        {
            searchViewsInstanceCounter--;
        }


        private void ExecuteAddNewGoogleCommand(Object args)
        {
            if (searchViewsInstanceCounter >= 5)
            {
                messageBoxService.ShowError(
                    "This demo only supports 5 search views to be " + 
                    "open at once\r\nPlease close one or more instances");
                return;
            }

            IRegionManager regionManager = RegionManager.GetRegionManager(
                (DependencyObject)viewAwareStatus.View);
            IRegion region = regionManager.Regions[RegionNames.MainRegion];
            GoogleImageSearchView googleImageSearchView = 
                ViewModelRepository.Instance.Resolver.Container.
                GetExport<GoogleImageSearchView>().Value;
            googleImageSearchView.ContextualData =
                new Model.GoogleImageSearchInfo(
                    randomSearchTerms[rand.Next(randomSearchTerms.Length)], 
                    string.Format("Imageregion_{0}", searchViewsCounter++));
            region.Add(googleImageSearchView);
            region.Activate(googleImageSearchView);
            searchViewsInstanceCounter++;
        }

        #endregion
    }
}

演示 1:自定义区域适配器

Prism 允许您做的其中一件很棒的事情是创建一个自定义区域适配器,以便与可能还没有 Prism 区域适配器的控件一起工作。

正如我所说,这个演示使用了自定义区域适配器。我创建了一个特殊的区域适配器,它与出色的 Transitionals CodePlex 项目中的精美 TransitionElement 协同工作。

实际的自定义区域适配器声明如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.Prism.Regions;
using System.Windows.Controls;
using System.Windows;
using System.ComponentModel.Composition;

using Transitionals.Controls;

namespace CinchV2AndPrismRegions.Regions
{
    [Export("CinchV2AndPrismRegions.Regions.TransitionElementAdaptor", 
        typeof(TransitionElementAdaptor))]
    public class TransitionElementAdaptor : RegionAdapterBase<TransitionElement>
    {
        [ImportingConstructor]
        public TransitionElementAdaptor(IRegionBehaviorFactory behaviorFactory) :
            base(behaviorFactory)
        {
        }

        protected override void Adapt(IRegion region, TransitionElement regionTarget)
        {
            region.Views.CollectionChanged += (s, e) =>
            {
                //Add
                if (e.Action == 
                    System.Collections.Specialized.NotifyCollectionChangedAction.Add)
                    foreach (FrameworkElement element in e.NewItems)
                        regionTarget.Content = element;

                //Removal
                if (e.Action == 
                    System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
                    foreach (FrameworkElement element in e.OldItems)
                    {
                        regionTarget.Content = null;
                        GC.Collect();
                    }

            };
        }

        protected override IRegion CreateRegion()
        {
            return new AllActiveRegion();
        }
    }
}

我们会在 GoogleImageSearchView 中使用这个自定义区域适配器,如下所示;我们使用 XAML:

<transitionalsControls:TransitionElement Margin="10,20,20,20" 
      x:Name="transitionElement" Transition="{Binding TransitionToUse}">
</transitionalsControls:TransitionElement>

我们还有以下代码隐藏,它将区域名称设置为动态变化的字符串,以确保应用程序中永远不会有两个具有相同名称的区域。使用 Prism,如果您想确保内容被放置到正确的区域中,该名称需要是唯一的,并且由于演示允许打开相同 GoogleImageSearchView 的多个实例,我们需要确保区域名称是在运行时生成和分配的。代码隐藏就是这样做的,以及 ShellViewModel 中命令处理程序中的一些代码,该代码在每次请求显示新的 GoogleImageSearchView 时运行。

这是 GoogleImageSearchView 的代码隐藏,用于设置 TransitionElement 区域名称:

public GoogleImageSearchInfo ContextualData
{
    get
    {
        return contextualData;
    }
    set
    {
        contextualData = value;
        transitionElement.SetValue(RegionManager.RegionNameProperty, 
            contextualData.RegionName);
    }
}

我们可以在下面显示的 ShellViewModel 命令处理程序代码中看到它的设置。

private void ExecuteAddNewGoogleCommand(Object args)
{
    if (searchViewsInstanceCounter >= 5)
    {
        messageBoxService.ShowError("This demo only supports 5 search views " + 
               "to be open at once\r\nPlease close one or more instances");
        return;
    }


    IRegionManager regionManager = 
      RegionManager.GetRegionManager((DependencyObject)viewAwareStatus.View);
    IRegion region = regionManager.Regions[RegionNames.MainRegion];
    GoogleImageSearchView googleImageSearchView = 
      ViewModelRepository.Instance.Resolver.
      Container.GetExport<GoogleImageSearchView>().Value;
    googleImageSearchView.ContextualData =
        new Model.GoogleImageSearchInfo(
            randomSearchTerms[rand.Next(randomSearchTerms.Length)], 
            string.Format("Imageregion_{0}", searchViewsCounter++));
    region.Add(googleImageSearchView);
    region.Activate(googleImageSearchView);
    searchViewsInstanceCounter++;
}

GoogleImageSearchView 中的一张小图片被点击时,就会使用这个区域,稍后会详细介绍。

这就是基于 TransitionElement 的区域在过渡过程中的样子:

演示 1:引导程序

创建 Prism V4 应用程序时必须做的下一件事是创建一个继承自 MefBootstrapper 的引导程序;这是创建 Shell 和进行其他 Prism 相关覆盖的地方。

对于演示应用程序,引导程序如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Text;
using System.Windows;
using Microsoft.Practices.Prism.MefExtensions;
using Microsoft.Practices.Prism.Regions;
using CinchV2AndPrismRegions.Regions;
using Cinch;
using System.Reflection;
using MEFedMVVM.ViewModelLocator;
using CinchV2AndPrismRegions.Views;
using System.Windows.Controls;
using Transitionals.Controls;
using System.ComponentModel.Composition.Primitives;

namespace CinchV2AndPrismRegions
{
    public class CinchV2AndPrismRegionsBootstrapper : MefBootstrapper, IComposer, IContainerProvider
    {

        public override void Run(bool runWithDefaultConfiguration)
        {
            base.Run(runWithDefaultConfiguration);
        }

        #region Overrides of Bootstrapper
        protected override void ConfigureAggregateCatalog()
        {
            this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(App).Assembly));
            this.AggregateCatalog.Catalogs.Add(
                 new AssemblyCatalog(typeof(Cinch.WPFMessageBoxService).Assembly));
        }

        protected override void InitializeShell()
        {
            base.InitializeShell();

            MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);
            
            CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });

            Application.Current.MainWindow = (Shell)this.Shell;
            Application.Current.MainWindow.Show();
        }



        protected override CompositionContainer CreateContainer()
        {
            // The Prism call to create a container
            var exportProvider = new           MEFedMVVMExportProvider(MEFedMVVMCatalog.CreateCatalog(AggregateCatalog));
            _compositionContainer = new CompositionContainer(exportProvider);
            exportProvider.SourceProvider = _compositionContainer;

            return _compositionContainer;
        }


        protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
        {
            RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings();
            mappings.RegisterMapping(typeof(TransitionElement), 
                     Container.GetExportedValue<TransitionElementAdaptor>());
            return mappings;
        }

 

        protected override DependencyObject CreateShell()
        {
            return this.Container.GetExportedValue<Shell>();
        }
        #endregion

        #region Implementation of IComposer (For MEFedMVVM)

        public ComposablePartCatalog InitializeContainer()
        {
            //return the same catalog as the PRISM one
            return this.AggregateCatalog;
        }

        public IEnumerable<ExportProvider> GetCustomExportProviders()
        {
            //In case you want some custom export providers
            return null;
        }

        #endregion


        #region Implementation of IContainerProvider(For MEFedMVVM)
        CompositionContainer IContainerProvider.CreateContainer()
        {
            // The MEFedMVVM call to create a container
            return _compositionContainer;
        }
        #endregion

    }
}

其中有一些关于让 Cinch V2Prism 很好地协同工作的注意事项;这些注意事项是:

  1. 实现 MEFedMVVM(因此是 Cinch V2IComposer 接口,以便我们可以指示 MEFedMVVM 使用与 Prism 相同的 CompositionContainer 和 Parts (Exports/Imports)。您可以直接遵循此演示中的示例;您只需要这样做。
  2. 我们在 ConfigureAggregateCatalog() 覆盖中添加了相关的目录,以使 Cinch V2/MEFedMVVM 正常工作。
  3. CreateShell 覆盖中运行以下行:MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);
  4. 我还运行了 Cinch V2 引导程序,以使其解析任何带有相关 Cinch V2 属性(例如 PopupNameToViewLookupKeyMetadata / ViewnameToViewLookupKeyMetadata)的弹出窗口/工作区视图。
  5. 我们还覆盖了 ConfigureRegionAdapterMappings() 方法来添加我们的自定义区域适配器。

这就是让 Cinch V2(请记住 Cinch V2 利用 MEFedMVVM 进行 ViewModel 解析)/ Prism 协同工作所需的一切。很简单,不是吗?

现在我们有了引导程序,我们需要确保它被调用,这通常在 Prism 应用程序的 App.xaml.cs 代码中完成,如下所示:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        CinchV2AndPrismRegionsBootstrapper bootstrapper = 
               new CinchV2AndPrismRegionsBootstrapper();
        bootstrapper.Run();
        this.ShutdownMode = ShutdownMode.OnMainWindowClose;
    }
}

演示 1:欢迎视图

欢迎视图非常简单,看起来就像这样:

事实上,WelcomeView 的 XAML 也非常简单;它在这里:

<UserControl x:Class="CinchV2AndPrismRegions.Views.WelcomeView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:meffed="clr-namespace:MEFedMVVM.ViewModelLocator;assembly=MEFedMVVM.WPF"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300"
         meffed:ViewModelLocator.ViewModel="WelcomeViewModel">

    <Grid Background="White" Margin="5,0,0,0">

        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <Border Height="30" Margin="0,10,10,0" 
                VerticalAlignment="Bottom" CornerRadius="5">
            <Border.Background>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="Black" Offset="1"/>
                    <GradientStop Color="#FF7C7C7C"/>
                    <GradientStop Color="#FF3D3D3D" Offset="0.5"/>
                </LinearGradientBrush>
            </Border.Background>
        </Border>
        <StackPanel Orientation="Horizontal" 
              Grid.Row="0" Grid.Column="0" 
              Grid.ColumnSpan="2">
            <Image HorizontalAlignment="Center" Margin="10,-3,0,-17" 
                   Source="/CinchV2AndPrismRegions;component/Images/blackInfo.png" 
                   Stretch="Fill" Width="60" 
                   Height="60" VerticalAlignment="Center"/>

            <Label Content="Information About This App" FontSize="15" 
                   HorizontalContentAlignment="Center"
                VerticalContentAlignment="Center" Foreground="White" 
                   FontFamily="Verdana" FontWeight="Bold" 
                   Padding="0" Margin="10,10,0,0" 
                   HorizontalAlignment="Left" 
                   d:LayoutOverrides="Height, GridBox"/>
        </StackPanel>

        <TextBlock Margin="5,30,5,5" Grid.Row="1" 
                TextWrapping="Wrap" 
                FontFamily="Verdana"><Run Language="en-gb" 
                Text="This small "/>
            <Run Foreground="#FF020202" FontWeight="Bold" 
                Language="en-gb" Text="Cinch"/>
            <Run Foreground="#FFF15C23" FontWeight="Bold" 
                Language="en-gb" Text=" V2 "/>
            <Run Language="en-gb" Text="demo shows just how you easy it is to use "/>
            <Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
            <Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2"/>
            <Run Foreground="#FFF15C23" Language="en-gb" Text=" "/>
            <Run Language="en-gb" 
                 Text="along side Microsofts Composite WPF block 
                       (Aka  Patterns and Practices "/>
            <Run FontWeight="Bold" Language="en-gb" Text="PRISM"/>
            <Run Language="en-gb" Text="). "/><LineBreak/>
            <Run Language="en-gb"/><LineBreak/>
            <Run Language="en-gb" Text="This is largely down to the fact that "/>
            <Run FontWeight="Bold" Language="en-gb" Text="PRISM "/>
            <Run Language="en-gb" Text="and "/>
            <Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
            <Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2 "/>
            <Run Language="en-gb" 
               Text="both use MEF to assemble their UI's, 
                     so it really could not be easier to use "/>
            <Run FontWeight="Bold" Language="en-gb" Text="PRISM"/>
            <Run Language="en-gb" Text="s excellent region support alongside "/>
            <Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
            <Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2 "/>
            <Run Language="en-gb" Text="other classes, should you wish to do so."/>
            <LineBreak/>
            <Run Language="en-gb"/><LineBreak/>
            <Run Language="en-gb" 
                Text="This demo is WPF based, but the same 
                      rules will apply when working with "/>
            <Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
            <Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2"/>
            <Run Foreground="#FFF15C23" Language="en-gb" 
                Text=" "/><Run Language="en-gb" Text="for "/>
            <Run Foreground="#FF46B7E7" FontWeight="Bold" 
                Language="en-gb" Text="Silverlight "/>
            <Run Language="en-gb" Text="and making use of "/>
            <Run FontWeight="Bold" Language="en-gb" 
                Text="PRISM "/><Run Language="en-gb" 
                Text="for "/>
            <Run Foreground="#FF46B7E7" FontWeight="Bold" 
                Language="en-gb" Text="Silverlight"/></TextBlock>
    </Grid>
</UserControl>

这里唯一值得注意的是 WelcomeView 使用 MEFedMVVM 解析其 ViewModel,其中 WelcomeViewModel 看起来像这样:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using System.ComponentModel.Composition;

namespace CinchV2AndPrismRegions.ViewModels
{
    [ExportViewModel("WelcomeViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class WelcomeViewModel : Cinch.ViewModelBase
    {
        public WelcomeViewModel()
        {
            base.IsCloseable = false;
        }

        public string ViewName
        {
            get { return "Welcome"; }
        }
    }
}

可以看出,它继承自 Cinch.ViewModelBase 类,因此能够设置 IsCloseable 属性,该属性用于为 Shell TabControl 区域设置 TabItem 的样式(请参阅演示的 AppStyle.xaml 资源)。

演示 1:图像视图

第一个演示中的第二个视图稍微复杂一些,它使用一个免费的 Google 图像搜索(您可以在附加的代码中找到),该搜索使用随机关键词搜索 Google 图片。它将这些图像呈现在 ItemsControl 中,并允许用户使用标准的 Cinch V2 事件到命令动作,在用户在 ItemsControl 中的一个图像上鼠标按下时触发 GoogleImageSearchViewModel 中的命令。Google 搜索可能需要一些时间,因此此调用被包装在一个新的 UI 服务中以获取图像 URL,该服务使用任务并行库 Task 来执行此工作。

它还承载了我们之前讨论过的特殊 TransitionElement 区域适配器。

当命令在 GoogleImageSearchViewModel 中响应 MouseDown 运行时,会请求将一个新的视图放入 TransitionElement 区域适配器中。正如您所期望的,这个新视图会使用 GoogleImageSearchViewModel 中当前选择的过渡类型进行过渡。

让我们从 GoogleImageSearchView 开始;以下是其 XAML 中最相关的部分:

<UserControl x:Class="CinchV2AndPrismRegions.Views.GoogleImageSearchView"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
     xmlns:meffed="clr-namespace:MEFedMVVM.ViewModelLocator;assembly=MEFedMVVM.WPF"
     xmlns:i="clr-namespace:System.Windows.Interactivity;
             assembly=System.Windows.Interactivity"
     xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"        
     xmlns:model="clr-namespace:CinchV2AndPrismRegions.Model"
     xmlns:transitions="clr-namespace:Transitionals.Transitions;assembly=Transitionals"
      xmlns:transitionals="clr-namespace:Transitionals;assembly=Transitionals"
     xmlns:transitionalsControls="clr-namespace:Transitionals.Controls;
        assembly=Transitionals"
     xmlns:controls="clr-namespace:CinchV2AndPrismRegions.Controls"
     mc:Ignorable="d" 
     x:Name="theView"
     d:DesignHeight="300" d:DesignWidth="300"
     meffed:ViewModelLocator.ViewModel="GoogleImageSearchViewModel">

    <UserControl.Resources>

        <DataTemplate x:Key="googleImageTemplate" DataType="{x:Type model:ImageInfo}">
            <Image Margin="5" HorizontalAlignment="Center" 
                       VerticalAlignment="Center"
                       Source="{Binding ImageUrl}" 
                       ToolTip="{Binding Title}"
                       Width="80" Height="80" 
                       Stretch="UniformToFill">

                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseLeftButtonUp">
                        <CinchV2:EventToCommandTrigger 
                                 Command="{Binding ElementName=theView, 
                                   Path=DataContext.SelectImageCommand}"
                                 CommandParameter="{Binding}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>

            </Image>
        </DataTemplate>

    </UserControl.Resources>

    <controls:AsyncHost AsyncState="{Binding Path=AsyncState, Mode=OneWay}">
        <Grid controls:AsyncHost.AsyncContentType="Content"
            Background="White">

            <ItemsControl Margin="10,20,10,10" VerticalAlignment="Top"
                 ItemsSource="{Binding GoogleImageResults}"
                 ItemTemplate="{StaticResource googleImageTemplate}"
                 Grid.Row="1" 
                 BorderBrush="{x:Null}" 
                 MinHeight="350" MaxHeight="350">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>

            <DockPanel Grid.Row="1" Grid.Column="1" Margin="10"
                   LastChildFill="True">

                <transitionalsControls:TransitionElement Margin="10,20,20,20" 
                    x:Name="transitionElement"
                    Transition="{Binding TransitionToUse}">
                </transitionalsControls:TransitionElement>

            </DockPanel>

        </Grid>

        <controls:AsyncBusyUserControl 
                controls:AsyncHost.AsyncContentType="Busy" 
                AsyncWaitText="{Binding Path=WaitText, Mode=OneWay}" 
                Visibility="Hidden" />
        <controls:AsyncFailedUserControl 
                controls:AsyncHost.AsyncContentType="Error" 
                Error="{Binding Path=ErrorText, Mode=OneWay}" 
                Visibility="Hidden" />

    </controls:AsyncHost>
</UserControl>

除了两件事,其他都是相当标准的东西,一件是来自 Transitionals.dllTransitionElement 控件,另一件是我开发的一个特殊线程控件,它有三个项目,每次只显示一个。它可以显示内容、繁忙控件或失败控件;这种显示/隐藏由 GoogleImageSearchViewModel 处理。

这是它的代码隐藏。请注意 IViewContext 的使用,我们之前在查看 ShellViewModel 时看到过它,其中 ShellViewModelGoogleImageSearchView 设置了随机关键字和区域名称。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;

using CinchV2AndPrismRegions.Regions;
using Microsoft.Practices.Prism.Regions;
using CinchV2AndPrismRegions.Model;

namespace CinchV2AndPrismRegions.Views
{
    [Export("CinchV2AndPrismRegions.Views.GoogleImageSearchView", 
            typeof(GoogleImageSearchView))]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public partial class GoogleImageSearchView : 
        UserControl, IViewContext<GoogleImageSearchInfo>
    {

        private GoogleImageSearchInfo contextualData;

        public GoogleImageSearchView()
        {
            InitializeComponent();
        }

        #region IViewContext<GoogleImageSearchInfo> Members

        public GoogleImageSearchInfo ContextualData
        {
            get
            {
                return contextualData;
            }
            set
            {
                contextualData = value;
                transitionElement.SetValue(RegionManager.RegionNameProperty, 
                    contextualData.RegionName);
            }
        }
        #endregion

      
    }
}

现在让我们看看 GoogleImageSearchViewModel 中最相关的部分,如下图所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using System.ComponentModel.Composition;
using System.ComponentModel;
using Cinch;
using CinchV2AndPrismRegions.Views;
using MEFedMVVM.Common;
using Microsoft.Practices.Prism.Regions;
using System.Windows;
using CinchV2AndPrismRegions.Regions;
using CinchV2AndPrismRegions.Services.Contracts;
using CinchV2AndPrismRegions.Model;
using System.Windows.Data;
using Transitionals;
using Transitionals.Transitions;
using System.Windows.Controls;
using CinchV2AndPrismRegions.Enums;

namespace CinchV2AndPrismRegions.ViewModels
{

    [ExportViewModel("GoogleImageSearchViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class GoogleImageSearchViewModel : Cinch.ViewModelBase
    {

        private string contentTitle = "GoogleImageSearch";
        private IMessageBoxService messageBoxService;
        private IViewAwareStatus viewAwareStatus;
        private IGoogleSearchProvider googleSearchProvider;
        private IEnumerable<ImageInfo> googleImageResults=null;
        private IRegion imageRegion = null;
        private Transition transitionToUse = new FadeAndBlurTransition();
        private TransitionType selectedTransitonType = TransitionType.FadeAndBlur;
        private Dictionary<TransitionType, Transition> 
        transitionsLookup = new Dictionary<TransitionType, Transition>();
        private string uniqueRegionNameForThisInstance;

        private string waitText;
        private string errorMessage;
        private AsyncType asyncState = AsyncType.Content;

        [ImportingConstructor]
        public GoogleImageSearchViewModel(
            IMessageBoxService messageBoxService,
            IGoogleSearchProvider googleSearchProvider,
            IViewAwareStatus viewAwareStatus)
        {
            base.IsCloseable = true;
            this.messageBoxService = messageBoxService;
            this.googleSearchProvider = googleSearchProvider;
            this.viewAwareStatus = viewAwareStatus;
            this.viewAwareStatus.ViewLoaded += ViewAwareStatus_ViewLoaded;

            //add transition lookups
            transitionsLookup.Add(TransitionType.FadeAndBlur, 
                                  new FadeAndBlurTransition());
            ......
            ......
            ......
            ......
            ......
            transitionsLookup.Add(TransitionType.VerticalWipeTransition, 
                                  new VerticalWipeTransition());

            //Commands
            CloseViewCommand = 
              new SimpleCommand<Object, Object>(ExecuteCloseViewCommand);
            SelectImageCommand = 
              new SimpleCommand<Object, Object>(ExecuteSelectImageCommand);

            Mediator.Instance.Register(this);

        }

        private void ViewAwareStatus_ViewLoaded()
        {
            string keyword = "";
            if (!Designer.IsInDesignMode)
            {
                IViewContext<GoogleImageSearchInfo> view = 
            (IViewContext<GoogleImageSearchInfo>)viewAwareStatus.View;
                if (view.ContextualData != null && googleImageResults == null)
                {
                    ContentTitle = string.Format("Searching using keyword : {0}",
                                                 view.ContextualData.KeyWord);

                    uniqueRegionNameForThisInstance = view.ContextualData.RegionName;
                    keyword = view.ContextualData.KeyWord;

                }
            }

            AsyncState = AsyncType.Busy;
            WaitText = string.Format("Fetching random Google " + 
                       "images for keyword : {0}", keyword);

            googleSearchProvider.GetAll(keyword, ShowGoogleResults, 
                                        ShowGoogleException);

        }

        private void  ShowGoogleResults(IEnumerable<ImageInfo> results)
        {
            googleImageResults = results;
            NotifyPropertyChanged(googleImageResultsArgs);

            AsyncState = AsyncType.Content;
        }

        private void ShowGoogleException(Exception ex)
        {
            ErrorMessage = ex.Message;
            AsyncState = AsyncType.Error;
        }

        public SimpleCommand<Object, Object> CloseViewCommand { get; private set; }
        public SimpleCommand<Object, Object> SelectImageCommand { get; private set; }

        /// <summary>
        /// TransitionTypes
        /// </summary>
        public Array TransitionTypes
        {
            get
            {
                return Enum.GetValues(typeof(TransitionType));
            }
        }

        /// <summary>
        /// AsyncState
        /// </summary>
        static PropertyChangedEventArgs asyncStateArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(x => x.AsyncState);

        public AsyncType AsyncState
        {
            get { return asyncState; }
            private set
            {
                asyncState = value;
                NotifyPropertyChanged(asyncStateArgs);
            }
        }

        /// <summary>
        /// WaitText
        /// </summary>
        static PropertyChangedEventArgs waitTextArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(x => x.WaitText);
       
        public string WaitText
        {
            get { return waitText; }
            private set
            {
                waitText = value;
                NotifyPropertyChanged(waitTextArgs);
            }
        }

        /// <summary>
        /// ErrorMessage
        /// </summary>
        static PropertyChangedEventArgs errorMessageArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.ErrorMessage);

        public string ErrorMessage
        {
            get { return errorMessage; }
            private set
            {
                errorMessage = value;
                NotifyPropertyChanged(errorMessageArgs);
            }
        }

        /// <summary>
        /// SelectedTransitonType
        /// </summary>
        static PropertyChangedEventArgs selectedTransitonTypeArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.SelectedTransitonType);

        public TransitionType SelectedTransitonType
        {
            get { return selectedTransitonType; }
            set
            {
                selectedTransitonType = value;
                NotifyPropertyChanged(selectedTransitonTypeArgs);
                TransitionToUse = transitionsLookup[value];
            }
        }

        /// <summary>
        /// TransitionToUse
        /// </summary>
        static PropertyChangedEventArgs transitionToUseArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.TransitionToUse);

        public Transition TransitionToUse
        {
            get { return transitionToUse; }
            private set
            {
                transitionToUse = value;
                NotifyPropertyChanged(transitionToUseArgs);
            }
        }

        /// <summary>
        /// GoogleImageResults
        /// </summary>
        static PropertyChangedEventArgs googleImageResultsArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.GoogleImageResults);

        public IEnumerable<ImageInfo> GoogleImageResults
        {
            get { return googleImageResults; }
        }

        /// <summary>
        /// ViewName
        /// </summary>
        public string ViewName
        {
            get { return "GoogleImageSearch"; }
        }

        /// <summary>
        /// ShowContextMenu
        /// </summary>
        static PropertyChangedEventArgs contentTitleArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.ContentTitle);

        public string ContentTitle
        {
            get { return contentTitle; }
            private set
            {
                contentTitle = value;
                NotifyPropertyChanged(contentTitleArgs);
            }
        }

        #region Comand Handlers

        private void ExecuteCloseViewCommand(Object args)
        {
            CustomDialogResults result =
                messageBoxService.ShowYesNo("Are you sure you want to close this tab?",
                    CustomDialogIcons.Question);

            //if user did not want to cancel, keep workspace open
            if (result == CustomDialogResults.Yes)
            {
                IRegionManager regionManager = 
                  RegionManager.GetRegionManager((DependencyObject)viewAwareStatus.View);
                IRegion region = regionManager.Regions[RegionNames.MainRegion];
                region.Remove(args);

                Mediator.Instance.NotifyColleagues<bool>("DecrementSearchCount", true);
            }
        }

        private void ExecuteSelectImageCommand(Object args)
        {
            //NOTE : I am breaking one of my own cardinal MVVM
            //rules here, and using UI element in my ViewModel
            //but this is just for the sake of the transitions
            //in the demo, it make use of the internet downloading 
            //images, load using the already loaded images.
            //I could have used the CommandParameter, and downloaded
            //image again, but decided against it for sake of demo.
            //If I was doing this for production code
            //I would have threaded eacj google searched image,
            //downloaded it locally and then used that downloaded file
            //location as ImageSource. But that would have distracted
            //from the demo too much. So I cheated

            Image selectedImageInfo = (Image)((EventToCommandArgs)args).Sender;

            if (imageRegion == null)
            {
                IRegionManager regionManager = 
                  RegionManager.GetRegionManager(
                  (DependencyObject)viewAwareStatus.View);
                imageRegion = 
                  regionManager.Regions[uniqueRegionNameForThisInstance];
            }

            ImageView imageView = ViewModelRepository.Instance.Resolver.
                      Container.GetExport<ImageView>().Value;

            ((IViewContext<ImageInfo>)imageView).ContextualData =
                new ImageInfo(selectedImageInfo.ToolTip.ToString(), 
                              selectedImageInfo.Source);
            var view = imageRegion.GetView("ImageView");
            if (view != null)
            {
                imageRegion.Remove(view);
            }
            imageRegion.Add(imageView, "ImageView");

        }

        #endregion

    }
}

我认为大部分代码都是不言自明的;其中大部分都使用了 Cinch V2 核心 UI 服务,即 IMessageBoxServiceIViewAwareStatus

但是 GoogleImageSearchViewModel 还使用了专门用于 GoogleImageSearchViewModel 的特殊服务(类型为 ItemRepository)。此服务正在使用任务并行库 Task;因此,当我们使用这个可能长时间运行的服务时,我们可以指示视图中的线程控件在执行工作时显示繁忙指示器,并在获取结果或捕获 Exception 时显示内容或失败控件。这应该从 GoogleImageSearchViewModel 中的代码中足够清楚。

其中服务契约如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CinchV2AndPrismRegions.Model;

namespace CinchV2AndPrismRegions.Services.Contracts
{

    public class SearchResult<T>
    {
        readonly T package;
        readonly Exception error;

        public T Package { get { return package; } }
        public Exception Error { get { return error; } }

        public SearchResult(T package, Exception error)
        {
            this.package = package;
            this.error = error;
        }
    }

    public interface IGoogleSearchProvider
    {
        void GetAll(
            string keyword, 
            Action<IEnumerable<ImageInfo>> resultCallback, 
            Action<Exception> errorCallback);
    }
}

该服务的运行时版本如下所示,我们使用任务并行库通过 Task 进行此获取(正如我所说,这根本不需要时间,但它确实向您展示了如何使用 Task 调用可能需要很长时间的东西):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;
using CinchV2AndPrismRegions.Services.Contracts;
using CinchV2AndPrismRegions.Model;
using System.Threading;
using System.Threading.Tasks;
using Gapi.Search;

namespace CinchV2AndPrismRegions.Services.Runtime
{
    [PartCreationPolicy(CreationPolicy.NonShared)]
    [ExportService(ServiceType.Runtime, typeof(IGoogleSearchProvider))]
    public class ItemRepository : IGoogleSearchProvider
    {

        void IGoogleSearchProvider.GetAll(string keyword, 
             Action<IEnumerable<ImageInfo>> resultCallback, 
             Action<Exception> errorCallback)
        {
            Task<SearchResult<IEnumerable<ImageInfo>>> task =
                Task.Factory.StartNew(() =>
                {
                    try
                    {
                        List<ImageInfo> items = new List<ImageInfo>();
                        SearchResults searchResults = 
                           Searcher.Search(SearchType.Image, keyword);
                        
                        if (searchResults.Items.Count() > 0)
                        {
                            foreach (var searchResult in searchResults.Items)
                            {
                                items.Add(new ImageInfo(
                                   searchResult.Title, searchResult.Url));
                            }
                        }
                        return new SearchResult<IEnumerable<ImageInfo>>(items, null);
                    }
                    catch (Exception ex)
                    {
                        return new SearchResult<IEnumerable<ImageInfo>>(null, ex);
                    }
                });

            task.ContinueWith(r =>
            {
                if (r.Result.Error != null)
                {
                    errorCallback(r.Result.Error);
                }
                else
                {
                    resultCallback(r.Result.Package);
                }
            }, CancellationToken.None, TaskContinuationOptions.None,
                TaskScheduler.FromCurrentSynchronizationContext());
        }
    }
}

这是一个设计时服务,它实际上在完全不同的项目中,甚至不需要被任何其他项目引用。基本上,只要持有设计时服务的项目引用了 MEFedMVVM.WPF,它就应该在设计时被解析并在 Blend 中显示。

一个重要的注意事项是,我正在使用 Windows 7,所以下面显示的设计时服务使用了在 Windows 7 上找到的示例图像路径。如果您不使用 Windows 7,您可能需要修改这些路径。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;
using CinchV2AndPrismRegions.Services.Contracts;
using CinchV2AndPrismRegions.Model;

namespace DesignTimeServices
{
    [PartCreationPolicy(CreationPolicy.NonShared)]
    [ExportService(ServiceType.DesignTime, typeof(IGoogleSearchProvider))]
    public class DesignTimeItemRepository : IGoogleSearchProvider
    {

        void IGoogleSearchProvider.GetAll(string keyword,
                Action<IEnumerable<ImageInfo>> resultCallback, 
        Action<Exception> errorCallback)
        {
            List<ImageInfo> results = new List<ImageInfo>();
            results.Add(new ImageInfo("robot1",
                @"C:\Users\Public\Pictures\Sample Pictures\Chrysanthemum.jpg"));
            results.Add(new ImageInfo("robot2",
                @"C:\Users\Public\Pictures\Sample Pictures\Tulips.jpg"));
            resultCallback(results);
        }
    }
}

为了证明这一切都正常工作,让我们回到 Blend 4 中 Cinch V2Prism 解决方案的原始截图。

看,一切都很好。我更喜欢 MEFedMVVM 处理设计时数据的方式,而不是 Blend d: 设计时标签的工作方式,因为它们假定您的 ViewModel 有一个默认构造函数。我不得不说,在我的生产代码中,我很少发现我的 ViewModel 有默认构造函数;它们通常**总是**依赖于某些上下文或服务。此外,使用 d: Blend 设计标签意味着您模拟整个 ViewModel。提供数据的应该是服务,所以它们应该被模拟,而不是整个 ViewModel。此外,Blend 要求所有 get/set 属性都进行这种模拟,这我认为会鼓励糟糕的设计。

但是,嘿,这只是我的个人看法。

演示 2:架构概述

演示 2 包含在本文顶部的 CinchV2AndPrismModulesRegions.zip 中。

本文中包含的第二个演示文章以更传统的 Prism 风格设计,包含单独的模块(项目),这些模块都在运行时被整合到一个应用程序中。

就我个人而言,我并不是模块的忠实拥护者,我更喜欢我的解决方案按以下方式组织:

  • Shell 项目
  • 视图项目
  • 控件项目
  • ViewModels 项目

我认为,对我来说,这比采用 Prism 模块范例显示出更好的关注点分离 (SOC)。我的意思是,如果您遵循单独的 ViewModels 项目,并且它不了解任何 WPF/SL 特定 DLL(例如 PresentationCore),如果有人要添加类似的东西,他们更有可能想到,等等,我为什么要将这个 DLL 添加到 ViewModels 项目中?但我知道我在这里很可能是少数,所以正如我所说,这个演示将向您展示如何将 Cinch V2 与所有标准 Prism 好东西(如模块/区域等)结合使用。

有一个单独的 shell 窗口,它有一个单独的 TabControl 区域(使用标准的 Prism TabControl 区域适配器),该区域将在运行时加载两个视图。这两个视图位于两个独立的模块中。

有一个欢迎视图/模块,它只显示一个简单的文本项,并利用了 Cinch V2 MEF 注入的 ViewModel,并且它还使用了多个 Cinch V2 核心 UI 服务,即 IMessageBoxServiceIViewAwareStatus

有一个列表视图/模块,它只显示一个项目列表,并利用 Cinch V2 MEF 注入的 ViewModel,并且它还使用了多个 Cinch V2 核心 UI 服务,即 IMessageBoxServiceIViewAwareStatus,并且还展示了多线程自定义服务的使用,其中还提供了设计时服务。

演示 2:它长什么样子?

好吧,回想一下我说过有一个简单的 Shell 窗口,在 TabControl 区域内有两个 Tab,其中有两个模块,一个欢迎视图和一个列表视图。演示 2 的屏幕截图如下所示。诚然,它不会在选美比赛中获奖,但它确实表明 Prism/Cinch V2 协同工作没有问题。

这是 Blend 4 中的截图,在演示 2 应用程序中,我为列表模块提供了一个默认的设计时数据访问服务。

演示 2:它是如何工作的?

接下来的几节将引导您了解其工作原理。

演示 2:Shell

构建此演示时,我执行的第一步是创建 Shell,这显然意味着获取所有相关的 DLL 引用,但您可以从实际的附加演示代码中看到这一点。Shell 是一个非常简单的 Window,它具有以下 XAML,它实际上只是一个区域容器,用于加载其他两个模块视图。正如我所说,在生产系统中,您的 Shell 显然会做更多的事情,并且看起来比这好得多;这只是一个演示。

这是整个 Shell 的 XAML:

<Window x:Class="CinchV2AndPrismModulesRegions.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Microsoft.Practices.Prism.Regions;
                   assembly=Microsoft.Practices.Prism"
        xmlns:ApplicationCommon="clr-namespace:ApplicationCommon;
                                 assembly=ApplicationCommon" 
        Title="MainWindow" Height="350" Width="525">
    <TabControl 
      cal:RegionManager.RegionName=
        "{x:Static ApplicationCommon:Regions.MainRegion}" 
      Grid.Column="1" Margin="0,0,5,0"   />

</Window>

这是它的代码隐藏;请注意,它被标记为 MEF ExportAttribute,这允许 MEF CompositionContainer(我们接下来会看到)能够解析 Shell 类型以及 Shell 可能需要的任何 MEF Import。应该注意的是,在此演示中,Shell 不需要满足任何其他 Import,但在实际代码中,它很可能会需要,因此 Export Shell 是一个好习惯;这也是 Prism 在 V4 中工作的实际方式。

namespace CinchV2AndPrismModulesRegions
{
    [Export]
    public partial class Shell : Window
    {
        public Shell()
        {
            InitializeComponent();
        }
    }
}

演示 2:引导程序

创建 Prism V4 应用程序时,您必须做的下一件事是创建一个继承自 MefBootstrapper 的引导程序。这是创建 Shell 和进行其他 Prism 相关覆盖的地方。

对于演示应用程序,引导程序如下所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Diagnostics;
using System.Windows;
using Microsoft.Practices.Prism.Logging;
using Microsoft.Practices.Prism.MefExtensions;

using MEFedMVVM.ViewModelLocator;
using Cinch;

namespace CinchV2AndPrismModulesRegions
{
    public class Bootstrapper : MefBootstrapper, IComposer, IContainerProvider
    {
        private CompositionContainer _compositionContainer;

        protected override void ConfigureAggregateCatalog()
        {
            this.AggregateCatalog.Catalogs.Add(
                new AssemblyCatalog(typeof(Bootstrapper).Assembly));
            this.AggregateCatalog.Catalogs.Add(
                new DirectoryCatalog("Modules"));
                // add all assemblies in the modules
            this.AggregateCatalog.Catalogs.Add(
                new AssemblyCatalog(typeof(ViewModelBase).Assembly));
        
            //add a reference to the <a href="mefedmvvm.codeplex.com">MEFedMVVM services
            this.AggregateCatalog.Catalogs.Add(
                new AssemblyCatalog(typeof(ViewModelLocator).Assembly));
                // reference the xml data access
        }

        protected override void InitializeShell()
        {
            base.InitializeShell();

            Application.Current.MainWindow = (Shell)this.Shell;
            Application.Current.MainWindow.Show();
        }

        #region Overrides of Bootstrapper

        protected override DependencyObject CreateShell()
        {
            //init <a href="mefedmvvm.codeplex.com">MEFedMVVM composed
            MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);

            return this.Container.GetExportedValue<Shell>();
        }


        protected override CompositionContainer CreateContainer()
        {
            // The Prism call to create a container
            var exportProvider = new  MEFedMVVMExportProvider(MEFedMVVMCatalog.CreateCatalog(AggregateCatalog));
            _compositionContainer = new CompositionContainer(exportProvider);
            exportProvider.SourceProvider = _compositionContainer;

            return _compositionContainer;
        }

        #endregion

        #region Implementation of IComposer (For MEFedMVVM)

        public ComposablePartCatalog InitializeContainer()
        {
            //return the same catalog as the PRISM one
            return this.AggregateCatalog;
        }

        public IEnumerable<ExportProvider> GetCustomExportProviders()
        {
            //In case you want some custom export providers
            return null;
        }

        #endregion

        #region Implementation of IContainerProvider(For MEFedMVVM)

        CompositionContainer IContainerProvider.CreateContainer()
        {
            // The MEFedMVVM call to create a container
            return _compositionContainer;
        }
        #endregion
    }
}

其中有一些关于让 Cinch V2Prism 很好地协同工作的注意事项;这些注意事项是:

  1. 实现 MEFedMVVM(因此是 Cinch V2IComposer 接口,以便我们可以指示 MEFedMVVM 使用与 Prism 相同的 CompositionContainer 和 Parts (Exports/Imports)。您可以直接遵循此演示中的示例,这就是您需要做的一切。
  2. 我们在 ConfigureAggregateCatalog() 覆盖中添加了相关的目录,以使 Cinch V2/MEFedMVVM 正常工作。
  3. CreateShell 覆盖中运行以下行:MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);

这就是让 Cinch V2(请记住 Cinch V2 利用 MEFedMVVM 进行 ViewModel 解析)/ Prism 协同工作所需的一切。很简单,不是吗?

现在我们有了引导程序,我们需要确保它被调用,这通常在 Prism 应用程序的 App.xaml.cs 代码中完成,如下所示:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        Bootstrapper b = new Bootstrapper();
        b.Run();
    }
}

演示 2:欢迎模块

WelcomeModule 是一个非常简单的模块,它只有一个视图/Cinch V2 基础的 ViewModel,因此,它必须引用 MEFedMVVM.WPF DLL 和 Cinch.WPF DLL,以及 Prism DLL,如下所示。

当引用 Cinch.WPF 和 MEFedMVVM.WPF DLL 时,您**必须**遵守一个特殊事项。它们**必须**将 Copy Local 设置为 false。这与 Bootstrapper 的 MEF AggregateCatalog 构建 Imports/Exports 的方式有关。如果您不将“Copy Local”设置为 false,那么对于特定的 MEF 部分契约名称,可能会找到多个 Import/Export,这会导致 MEFedMVVM 出现问题。

只需确保您创建的任何自定义模块的 Cinch.WPF 和 MEFedMVVM.WPF 这两个引用都将“Copy Local = False”设置为 false,一切都会正常。

现在您已经解决了引用问题,您需要做的就是创建一个模块,如下所示:

using System.ComponentModel.Composition;
using ApplicationCommon;
using Microsoft.Practices.Prism.MefExtensions.Modularity;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;

namespace Modules.WelcomeModule
{
    [ModuleExport(typeof(WelcomeModule))]
    public class WelcomeModule : IModule
    {
        readonly IRegionManager _regionManager;

        [ImportingConstructor]
        public WelcomeModule(IRegionManager regionManager)
        {
            _regionManager = regionManager;
        }

        #region Implementation of IModule

        public void Initialize()
        {
            var view = new WelcomeView();

            // Add it to the region
            IRegion region = _regionManager.Regions[Regions.MainRegion];
            region.Add(view, "WelcomeView");
            region.Activate(view);
        }

        #endregion
    }
}

您必须确保每个包含的模块都输出到一个特殊文件夹,该文件夹包含在 Bootstrapper 代码中,用于扫描任何自定义模块。下面显示了 WelcomModule 的情况,但适用于您编写的任何自定义模块。

可以看出,这个 WelcomeModule 只是在一个在 Shell 中声明的区域中显示一个 WelcomeView 的新实例。那么让我们看看 WelcomeView

它在这里,并不是那么令人兴奋:

<UserControl x:Class="Modules.WelcomeModule.WelcomeView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             xmlns:mefed="http:\\www.codeplex.com\MEFedMVVM"
             mefed:ViewModelLocator.ViewModel="WelcomeViewModel">
    <Grid>
        <Label Content="{Binding WelcomeText}"/>
    </Grid>
</UserControl>

这里值得注意的重要一点是 MEFedMVVM ViewModel DependencyProperty 的使用,它使用 MEFedMVVM 来定位 WelcomeViewModel

WelcomeViewModel 是一个非常简单的 Cinch V2 ViewModel,它允许我们使用标准的 Cinch V2 UI 服务/ViewModel 基类。WelcomeViewModel 如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using ApplicationCommon;
using System.ComponentModel.Composition;
using Cinch;

namespace Modules.WelcomeModule.ViewModels
{
    [ExportViewModel("WelcomeViewModel")]
    public class WelcomeViewModel : ViewModelBase
    {
        private IMessageBoxService messageBoxService;
        private IViewAwareStatus viewAwareStatus;
        private bool initialised = false;

        [ImportingConstructor]
        public WelcomeViewModel(
            IMessageBoxService messageBoxService,
            IViewAwareStatus viewAwareStatus)
        {
            WelcomeText = "hello";
            
            this.messageBoxService = messageBoxService;
            this.viewAwareStatus = viewAwareStatus;
            this.viewAwareStatus.ViewLoaded += ViewAwareStatus_ViewLoaded;
        }

        void ViewAwareStatus_ViewLoaded()
        {
            if (!initialised)
            {
                initialised = true;
                messageshow BoxService.ShowInformation(
                    string.Format("WelcomeViewModel says {0}", WelcomeText));
            }
        }

        public string WelcomeText { get; set; }
    }
}

正如我所说,WelcomeModule 并不复杂。因此,它的 WelcomeViewModel 所做的只是使用一个标准的 Cinch V2 服务,在本例中是 IMessageBoxService/IViewAwareStatus,以便在视图加载时显示一条消息(它通过 IViewAwareStatus 告知 ViewModel 视图已加载)。

演示 2:列表模块

此演示中的第二个模块使用了一个运行时多线程服务,该服务模拟从某个需要很长时间的地方获取数据列表(好的,在演示中,它不需要很长时间,但它向您展示了如何操作),并且它还展示了如何使用设计时服务来提供设计时数据。

将 MEFedMVVM.WPF 和 Cinch.WPF 引用设置为“Copy Local=False”的方式也适用于此处,以及将构建输出路径更改为整个解决方案的“Modules”文件夹。

模块本身很简单,看起来像这样:

using System.ComponentModel.Composition;
using ApplicationCommon;
using Microsoft.Practices.Prism.MefExtensions.Modularity;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;

namespace Modules.DisciplesListModule
{
    [ModuleExport(typeof(DisciplesListModule))]
    public class DisciplesListModule : IModule
    {
        readonly IRegionManager _regionManager;

        [ImportingConstructor]
        public DisciplesListModule(IRegionManager regionManager)
        {
            _regionManager = regionManager;
        }

        #region Implementation of IModule

        public void Initialize()
        {
            var view = new DisciplesListView();

            // Add it to the region
            IRegion region = _regionManager.Regions[Regions.MainRegion];
            region.Add(view, "DisciplesListView");
            region.Activate(view);
        }

        #endregion
    }
}

DisciplesListView 仅比 WelcomeView 复杂一点,因为这次我们显示的是一个项目列表,并且有一个简单的 DataTemplate 来设置列表中数据项的样式。这是 DisciplesListView 的完整 XAML;再次注意,DisciplesListViewModel 是使用 MEFedMVVM 连接起来的。

<UserControl x:Class="Modules.DisciplesListModule.DisciplesListView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:models="clr-namespace:ApplicationCommon.Models;
                           assembly=ApplicationCommon"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             xmlns:mefed="http:\\www.codeplex.com\MEFedMVVM"
             mefed:ViewModelLocator.ViewModel="DisciplesListViewModel">
    
    <UserControl.Resources>
        <DataTemplate x:Key="discTemplate" DataType="{x:Type models:DiscipleInfo}">
                <StackPanel Orientation="Horizontal" Background="WhiteSmoke">
                    <Label Content="FirstName: "/>
                    <Label Content="{Binding FirstName}"/>
                    <Label Margin="10,0,0,0" Content="LastName: "/>
                    <Label Content="{Binding LastName}"/>
                </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    
    
    <Grid Background="Cyan">
        <ItemsControl Margin="10" 
                      Background="CornflowerBlue"
                      BorderBrush="Black" BorderThickness="2"
                      ItemsSource="{Binding DisciplesResults}"
                      ItemTemplate="{StaticResource discTemplate}"/>
    </Grid>
</UserControl>

DisciplesListViewModel 如下所示,可以看出它不仅使用了一些核心 Cinch V2 服务,还使用了一个专门用于 DisciplesListViewModel 的特殊服务(类型为 IDisciplesListProvider)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using ApplicationCommon;
using System.ComponentModel.Composition;
using Cinch;

using ApplicationCommon.Models;
using System.ComponentModel;
using UIServiceContracts;

namespace Modules.DisciplesListModule.ViewModels
{
    [ExportViewModel("DisciplesListViewModel")]
    public class DisciplesListViewModel : ViewModelBase
    {
        private IMessageBoxService messageBoxService;
        private IViewAwareStatus viewAwareStatus;
        private IDisciplesListProvider disciplesListProvider;
        private IEnumerable<DiscipleInfo> disciplesResults = null;

        [ImportingConstructor]
        public DisciplesListViewModel(
            IMessageBoxService messageBoxService,
            IViewAwareStatus viewAwareStatus, 
            IDisciplesListProvider disciplesListProvider)
        {

            this.messageBoxService = messageBoxService;
            this.disciplesListProvider = disciplesListProvider;
            this.viewAwareStatus = viewAwareStatus;
            this.viewAwareStatus.ViewLoaded += new Action(viewAwareStatus_ViewLoaded);

        }

        private void viewAwareStatus_ViewLoaded()
        {
            if (disciplesResults == null)
                disciplesListProvider.GetAll(ShowResults, ShowException);
        }

        private void ShowResults(IEnumerable<DiscipleInfo> results)
        {
            disciplesResults = results;
            NotifyPropertyChanged(disciplesResultsArgs);

            messageBoxService.ShowInformation("got results");
        }


        private void ShowException(Exception ex)
        {
            messageBoxService.ShowError(
                string.Format("there was a problem fetching the " + 
                       "Disciples list\r\nThis is the exception{0}",
                       ex.ToString()));
        }

        /// <summary>
        /// DisciplesResults
        /// </summary>
        static PropertyChangedEventArgs disciplesResultsArgs =
            ObservableHelper.CreateArgs<DisciplesListViewModel>(x => x.DisciplesResults);


        public IEnumerable<DiscipleInfo> DisciplesResults
        {
            get { return disciplesResults; }
        }
    }
}

其中服务契约如下所示:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using ApplicationCommon.Models;

namespace UIServiceContracts
{

    public class SearchResult<T>
    {
        readonly T package;
        readonly Exception error;

        public T Package { get { return package; } }
        public Exception Error { get { return error; } }

        public SearchResult(T package, Exception error)
        {
            this.package = package;
            this.error = error;
        }
    }

    public interface IDisciplesListProvider
    {
        void GetAll(
            Action<IEnumerable<DiscipleInfo>> resultCallback, 
            Action<Exception> errorCallback);
    }
}

其中该服务的运行时版本如下所示,我们使用任务并行库通过 Task 进行此获取(正如我所说,这根本不需要时间,但它确实向您展示了如何使用 Task 调用可能需要很长时间的东西):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;

using MEFedMVVM.ViewModelLocator;
using ApplicationCommon.Models;
using UIServiceContracts;


namespace DisciplesListModule.Services.Runtime
{
    [PartCreationPolicy(CreationPolicy.NonShared)]
    [ExportService(ServiceType.Runtime, typeof(IDisciplesListProvider))]
    public class DisciplesListProvider : IDisciplesListProvider
    {
        void IDisciplesListProvider.GetAll(
        Action<IEnumerable<DiscipleInfo>> resultCallback, 
            Action<Exception> errorCallback)
        {
            Task<SearchResult<IEnumerable<DiscipleInfo>>> task =
                Task.Factory.StartNew(() =>
                {
                    try
                    {
                        List<DiscipleInfo> items = new List<DiscipleInfo>();
                        items.Add(new DiscipleInfo("marlon", "grech"));
                        items.Add(new DiscipleInfo("sacha", "barber"));
                        items.Add(new DiscipleInfo("josh", "smith"));
                        items.Add(new DiscipleInfo("karl", "shifflett"));
                        items.Add(new DiscipleInfo("daniel", "vaughan"));
                        items.Add(new DiscipleInfo("jeremiah", "morrill"));



                        return new SearchResult<IEnumerable<DiscipleInfo>>(items, null);
                    }
                    catch (Exception ex)
                    {
                        return new SearchResult<IEnumerable<DiscipleInfo>>(null, ex);
                    }
                });

            task.ContinueWith(r =>
            {
                if (r.Result.Error != null)
                {
                    errorCallback(r.Result.Error);
                }
                else
                {
                    resultCallback(r.Result.Package);
                }
            }, CancellationToken.None, TaskContinuationOptions.None,
                TaskScheduler.FromCurrentSynchronizationContext());
        }
    }
}

这是一个设计时服务,它实际上在完全不同的项目中,甚至不需要被任何其他项目引用。基本上,只要持有设计时服务的项目引用了 MEFedMVVM.WPF,它就应该在设计时被解析并在 Blend 中显示

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using MEFedMVVM.ViewModelLocator;
using UIServiceContracts;
using ApplicationCommon.Models;

namespace DesignTimeServices
{
    [PartCreationPolicy(CreationPolicy.NonShared)]
    [ExportService(ServiceType.DesignTime, typeof(IDisciplesListProvider))]
    public class DisciplesListProvider : IDisciplesListProvider
    {
        void IDisciplesListProvider.GetAll(
            Action<IEnumerable<DiscipleInfo>> resultCallback, 
            Action<Exception> errorCallback)
        {

            List<DiscipleInfo> items = new List<DiscipleInfo>();
            items.Add(new DiscipleInfo("lorem", "ipsum1"));
            items.Add(new DiscipleInfo("slovvy", "toads"));
            items.Add(new DiscipleInfo("jabber", "wocky"));
            resultCallback(items);
        }
    }
}

为了证明这一切都正常工作,让我们回到 Blend 4 中 Cinch V2Prism 解决方案的原始截图。

看,一切都好吗?我更喜欢 MEFedMVVM 处理设计时数据的方式,而不是 Blend d: 设计时标签的工作方式,因为它们假定您的 ViewModel 有一个默认构造函数。我不得不说,在我的生产代码中,我很少发现我的 ViewModel 有默认构造函数;它们通常**总是**依赖于某些上下文或服务。此外,使用 d: Blend 设计标签意味着您模拟整个 ViewModel。提供数据的应该是服务,所以它们应该被模拟,而不是整个 ViewModel。此外,Blend 要求所有 get/set 属性都进行这种模拟,这我认为会鼓励糟糕的设计。

但是,嘿,这只是我的个人看法。

各位,就这些了

我希望通过本文向您展示,您确实可以非常轻松地将 Cinch V2Prism 结合使用,并充分发挥这两个框架的优势。要知道,如果您喜欢 Prism 的区域/模块,您可以使用它们,同时仍然利用 Cinch V2 的服务、ViewModel 基类和额外的实用程序。它们只是无缝地协同工作,这主要归功于 MEF

一如既往,如果您喜欢您所看到的内容,请抽出时间添加评论和投票,它们总是受欢迎的。

© . All rights reserved.