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

ViewModel 1st Child Container PRISM Navigation

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2013年8月21日

CPOL

11分钟阅读

viewsIcon

53082

downloadIcon

1042

展示了如何在支持子容器的 VM 1st 中使用 PRISM 导航 API

这是您可以下载演示应用程序的链接

引言

最近,我很幸运能够开始一个全新的项目(完全从零开始)。我只有一个标准,那就是必须使用 WPF 来完成,因为这是其他可部署 UI 所使用的,也是公司其他开发人员都熟悉的技术。所以 WPF 没问题,我喜欢这东西。那么,如果我要开发一个新的 WPF 应用程序,我会考虑做些什么,以及如何做呢?

这是我的必备清单

  • 可组合 UI(使用 DataTemplate 或某种视图解析技术)
  • 对应用程序的每个方面都提供完整的 IOC 支持
  • 某种形式的模块化
  • 可扩展性

嗯,这听起来非常像 WPF 的复合指导 : PRISM。 是的,你抓到我了,这篇文章实际上是关于 PRISM 的,我将讨论一些关于处理列表中前两点(可组合 UI 和 IOC 支持)的替代方法。现在有一件事你应该知道,这篇文章在很多方面都是一个非常小众的文章,它不是一个万能药,你可以到处应用,但在正确的场景下确实效果非常好。话虽如此,如果你使用 PRISM / 想使用 PRISM,这可能对你有用。

为了完整起见,我在任何新的 WPF 项目上通常会从各地拉取一些零碎的东西,最近看起来像这样

  • PRISM 用于其区域/IOC 支持/模块化(哦,顺便说一句,当我提到 PRISM 时,我指的是 PRISM 4 / 4.1)
  • Cinch 用于我的一些辅助类
  • Rx 用于异步编码/流
  • 来自个人库的一些包含各种零散内容的工具

 

演示应用程序概览

正如我之前所说,我将专注于处理 PRISM 的以下两个方面

  • 可组合 UI(使用 DataTemplate 或某种视图解析技术)
  • 对应用程序的每个方面都提供完整的 IOC 支持

表面上看,演示应用程序是 PRISM 区域功能的简单演示,特别是 TabControl 选择器区域适配器(这是 PRISM 自带的)

演示应用程序看起来是这样的,我承认,不是很令人兴奋。

点击图片可以放大

事实上,这看起来很沉闷,但请继续阅读,这篇文章一定会给我们带来一些价值。

 

ViewModel First With PRISM

在开发基于 XAML 的应用程序时,你不是在 ViewModel First 阵营,就是在 View First 阵营。显然,设计师喜欢使用 View First 和 VisualState 而不是 DataTemplate。问题是 PRISM 在开箱即用的情况下,在某种程度上(或多或少)迫使你走向更偏向 View First 的方法,你必须要么

  • 将 View 添加到区域
  • 导航到区域内的 View

这可能看起来足够合理,**但是**(而且这是一个很大的“但是”),你们中有谁真正与设计师(实际上,你们中有谁真正见过这些传说中的 XAML 设计师,他们存在吗?他们似乎比雪人更稀有,正如我所说,我只遇到过一个)在 XAML 项目上合作过,而且他们非常精通 XAML 和 Expression Blend。

我遇到过(但只遇到过一次),你们猜怎么着……他们**不**使用 View First 的方法,但他们喜欢 VisualState,这实际上可以在任何地方使用。你基本上必须选择 Triggers(Sliverlight 根本不支持这些)或 VisualState。我认为,如果让他们选择,设计师总是会选择 VisualState

他们确实主要使用 DataTemplate。我说的不是普通的设计师,他们很优秀,而且绝对是行家,这是一个复杂的基于 XAML 的产品,代码量高达 50 万行。规模很大。

这让我确信了两件事

  1. 你可以选择 ViewModel First,这是开发人员喜欢并希望使用的
  2. 你仍然可以使用 VisualState,这是设计师喜欢并希望使用的。事实上,这与 VM First 或 View First 的关系不大,我只是想向人们保证,即使你选择了 VM First 的方法,仍然应该能够让你的设计师满意。

有点像鱼与熊掌兼得……嗯……你想多听听?

问题是,在这个部分的开篇我曾说过“PRISM 在开箱即用的情况下,在某种程度上(或多或少)迫使你走向更偏向 View First 的方法”。这是大致正确的,因为你可能找到的关于使用 PRISM 及其区域支持和导航 API 的所有文档都会提到 View First 的方法。

然而,在文档的深处,以及在 PRISM 书籍/文档中一行令人惊叹的代码(字面意义上只有一行,眨眼就错过了)中,有一个机制,你可以使用它通过 PRISM 的导航 API 来实现 ViewModel First 导航。太棒了。

当你在 IOC 容器中注册一个用于导航的“ViewModel”时,它基本上看起来是这样的(我在这里使用的是 Unity,但对于 MEF 来说,这甚至更容易,因为你可以直接使用 ViewModel 的全名作为导出契约值)

Container.RegisterTypeForNavigation<MainContainerDummyViewModel>();

这利用了我编写的一个简单的扩展方法,它有助于使 IOC 注册过程更简单、更不杂乱。

public static void RegisterTypeForNavigation<T>(this IUnityContainer container)
{
    container.RegisterType(typeof(Object), typeof(T), typeof(T).FullName);
}
重要提示

在使用 Unity 容器时,注册到 typeof(Object) 是注册的**关键**部分。如果你不这样做,很可能会只显示 ViewModel 的字符串表示而不是你希望应用 DataTemplate 的实际 ViewModel。

现在我们有了一个在 Unity IOC 容器中注册 ViewModel 进行导航的机制,我们可以像这样在一个区域内显示一个 ViewModel:

private void NavigateToMainContainerViewModel(CreateMainContainerViewModelMessage message)
{
    UriQuery parameters = new UriQuery();
    parameters.Add("ID", mainContainerCounter++.ToString());


    var uri = new Uri(typeof(MainContainerDummyViewModel).FullName + parameters, UriKind.RelativeOrAbsolute);
    regionManager.RequestNavigate("MainRegion", uri, HandleNavigationCallback);
}

这实际上工作得很好。正如我所说,PRISM 支持 ViewModel First,只是不太为人所知或宣传。关键是,一旦你知道了,它就很容易做到。

 

我们如何更好地组织结构

好了,我们现在知道,使用 PRISM 确实可以支持 ViewModel First 的方法,这让我们开发人员很满意。正如我所说,当我实际上与一位精通 XAML/Blend(而不是 Photoshop)的神秘设计师合作时,他们也很乐意使用 DataTemplate 和他们喜欢的东西,即 VisualState

问题是,如果你能想象一个中到大型项目,有成百上千的 ViewModel,可能还有成千上万的 UI 服务(在我工作过的一个地方,他们就有这种情况),那么拥有一个单一的应用程序 IOC 容器,并仔细考虑在容器中注册的所有组件的生命周期,很快就会变得

  • **相当令人不知所措**:因为你必须真正理解每个组件是如何以及何时被使用的。你真的能确切地知道使用 IOC 注册组件的东西何时会被使用和释放吗?我认为这在某种程度上取决于用户如何使用该系统。
  • **结构不良**:由于只有一个容器共享所有组件注册(好吧,这可能会在 PRISM 的几个模块中完成,但根本问题是一样的,只有一个容器)。
  • **维护噩梦**:显而易见的原因。
  • **可能会相当脆弱**:因为你可能不完全理解你的 IOC 组件的正确生命周期,因为它们可能被来来往往的 ViewModel 共享。你会使用什么生命周期管理?单例?共享?

我个人的感觉是,很多这些问题可以通过使用子容器来解决。现在 PRISM 支持 Unity 和 MEF,但社区的一些工作允许你将其他 IOC 容器插入 PRISM。在这篇文章中,我使用的是 Unity,因为它做得足够好,而且我喜欢它对子容器的支持。我还没有真正使用过 MEF 的子容器方法,所以无法真正评论。当然,我即将讨论的方法与 Castle Windsor 等也会很好地配合。

现在,我并不是说拥有一个子容器是你能遇到的每一个 WPF 问题的答案,正如我已经说过的,它非常小众,我完全理解。它会非常有效的地方是例如

  • 某种标签式界面,其中每个标签都可以是一个 ViewModel,并且可以关闭标签。
  • 某种工作区界面,其中 UI 的小部分可能由可关闭的 ViewModel 表示。

基本上,任何你可以控制在响应用户操作(如关闭标签,或关闭/删除 ViewModel)时调用 ViewModel 的 Dispose() 的地方,都可以从使用子容器中受益。

当我以这种方式使用 PRISM 时,我通常会采用类似以下的结构:

这就是大致的想法,现在让我们来看一些代码。

确保我们可以创建一个子容器注册供 PRISM 使用。和以前一样,我创建了一个简单的扩展方法来让这项工作尽可能容易。

public static void RegisterTypeForNavigationWithChildContainer<T>(this IUnityContainer container)
{
    container.RegisterType(typeof(Object), typeof(T), typeof(T).FullName, 
        new InjectionMethod("AddDisposable", new object[] { container }));
}

可以看到,这段代码与之前的 Unity 注册代码基本相同,但有一个新的值得注意的地方。那就是子容器被传递给了 AddDisposable 方法,让我们来看看。其思想是,当 ViewModel 被以编程方式或通过 GC 释放时,子容器及其所有注册的组件也将被释放。正如我所说,它不会对所有事情都有效,但对于可关闭的 ViewModel(想想标签式 UI)/工作区类型的 UI 设计来说,它效果非常好。

**注意:**我在这里使用了 Reactive Extensions,但这只是因为我决定使用 Rx 来构建我的 Event Aggregator,但下面看到的 CompositeDisposable 可以很容易地替换为 List<IDisposable>,如果你不想使用 Rx。

public abstract class DisposableViewModel : INPCBase, IDisposable
{
    CompositeDisposable disposables = new CompositeDisposable();


    public void AddDisposable(IDisposable disposable)
    {
        disposables.Add(disposable);
    }

    public void Dispose()
    {
        foreach (var disposable in disposables)
        {
            disposable.Dispose(); 
        } 
    }
}

 

所以,现在让我们来看看如何处理创建一个新的 ViewModel,使用 PRISM 来显示它,以及如何将子容器的生命周期与 ViewModel 绑定。正如我所说,这并不适合所有人,这是一个非常小众的需求,但对我个人来说,我发现它非常有用。

/// <summary>
/// Creates a child container for a viewmodel and registers its dependencies and then shows the ViewModel. It
/// also ties the lifetime of the child container to that of the newly instantatied ViewModel.
/// </summary>
/// <param name="message">The message that has been seen to create a new ViewModel to show</param>
private void NavigateToChildContainerViewModel(CreateChildContainerViewModelMessage message)
{
    var childcontainer = mainAppContainer.CreateChildContainer();

    //note if you want to have services that are disposable that you want disposed when child container is disposed
    //they need to be registered using the "HierarchicalLifetimeManager"
    childcontainer.RegisterType<ISomeDummyDisposableService, SomeDummyDisposableService>(new HierarchicalLifetimeManager());

    //this is how to use ViewModel 1st Unity registration
    childcontainer.RegisterTypeForNavigationWithChildContainer<ChildContainerDummyViewModel>();

    UriQuery parameters = new UriQuery();
    parameters.Add("ID", childContainerCounter++.ToString());

    var uri = new Uri(typeof(ChildContainerDummyViewModel).FullName + parameters, UriKind.RelativeOrAbsolute);

    //use the custom extension methods to specify our own container to use
    regionManager.RequestNavigateUsingSpecificContainer("MainRegion", uri, HandleNavigationCallback, childcontainer);
}

 

支持我们的 PRISM 区域的子容器

为了让 PRISM 完全支持子容器,直到创建实际要在区域中显示的项(导航 API 正在处理的),我需要稍微调整一些东西。这是我非常喜欢 PRISM 的一点,那就是它完全可扩展。

我从这个扩展方法开始

public static class RegionExtensions
{
    public static void RequestNavigateUsingSpecificContainer(this IRegion region, 
        Uri target, Action<NavigationResult> navigationCallback, 
        IUnityContainer containerToUse)
    {
        CustomRegionNavigationService moneycorpRegionNavigationService = region.NavigationService as CustomRegionNavigationService;
        if (moneycorpRegionNavigationService == null)
            throw new InvalidOperationException(
                "RequestNavigate that takes a container may only be used with a CustomRegionNavigationService");

        ((CustomRegionNavigationService)region.NavigationService).RequestNavigate(
            target, navigationCallback, containerToUse);
    }
}

然后我修改了以下 PRISM 类(它们太大了,无法在此列出,但我将尽可能展示最相关的部分)。

  • IRegionNavigationService (代码太多无法显示,请参见演示代码)。这个类是 PRISM 导航 API 的主要入口点。我只是修改它,增加了额外的方法,允许提供一个子容器,用于导航/依赖解析。
  • IRegionNavigationContentLoader 这个类是实际加载导航项的类(所以它可以是视图或 ViewModel,因此对我来说很清楚,我需要和这个家伙打交道才能让它使用子容器)。

多亏了以下启动器代码,这些自定义实现覆盖了默认的 PRISM 实现:

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

    //custom region stuff to support child container navigation
    Container.RegisterType<IRegionNavigationContentLoader, CustomRegionNavigationContentLoader>(new ContainerControlledLifetimeManager());
    Container.RegisterType<IRegionNavigationService, CustomRegionNavigationService>(new ContainerControlledLifetimeManager());
}

这是修改后的 IRegionNavigationContentLoader 类中最相关的方法(不过请务必查看代码的其余部分)。

/// <summary>
/// Provides a new item for the region based on the supplied candidate target contract name.
/// </summary>
/// <param name="candidateTargetContract">The target contract to build.</param>
/// <returns>An instance of an item to put into the <see cref="IRegion"/>.</returns>
protected virtual object CreateNewRegionItem(string candidateTargetContract, IUnityContainer containerToUse)
{
    object newRegionItem;
    try
    {
        if (containerToUse == null)
        {
            newRegionItem = this.serviceLocator.GetInstance<object>(candidateTargetContract);
        }
        else
        {
            newRegionItem = containerToUse.Resolve<object>(candidateTargetContract);
        }
    }
    catch (ActivationException e)
    {
        throw new InvalidOperationException(
            string.Format(CultureInfo.CurrentCulture, "Can not create navigation target {0}", candidateTargetContract),
            e);
    }
    return newRegionItem;
}

 

有了这一切,我们现在有了以下两点:

  1. 你可以选择 ViewModel First,这是开发人员喜欢并希望使用的
  2. 你仍然可以使用 VisualState,这是设计师喜欢并想要使用的。

好日子,我对这一切的成功感到高兴。

 

就这些

尽管这不是一篇鸿篇巨著,而且它关注的是 WPF / Silverlight 开发中一个相当小众的领域,但我确实认为,这种方法在开发 Windows 8 的导航系统时同样有价值,该系统可能需要你在离开某种导航框架时处置视图。

事实上,我接下来的系列文章将是关于使用 Windows 8 上两个最好的 MVVM 框架,届时我将能够在一个不完全由复合视图组成的框架(PRISM 当然支持)中检验这个想法。

在那之后,我将暂时从 UI 方面休息一下,然后专注于一些 Azure 和 Powershell,因为这两件事我已经考虑了很久。

一如既往,如果你认为这篇文章很有用,或者你喜欢它,任何投票/评论都将非常受欢迎。

© . All rights reserved.