ViewModel 1st Child Container PRISM Navigation





5.00/5 (17投票s)
展示了如何在支持子容器的 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 万行。规模很大。
这让我确信了两件事
- 你可以选择 ViewModel First,这是开发人员喜欢并希望使用的
- 你仍然可以使用
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;
}
有了这一切,我们现在有了以下两点:
- 你可以选择 ViewModel First,这是开发人员喜欢并希望使用的
- 你仍然可以使用
VisualState
,这是设计师喜欢并想要使用的。
好日子,我对这一切的成功感到高兴。
就这些
尽管这不是一篇鸿篇巨著,而且它关注的是 WPF / Silverlight 开发中一个相当小众的领域,但我确实认为,这种方法在开发 Windows 8 的导航系统时同样有价值,该系统可能需要你在离开某种导航框架时处置视图。
事实上,我接下来的系列文章将是关于使用 Windows 8 上两个最好的 MVVM 框架,届时我将能够在一个不完全由复合视图组成的框架(PRISM 当然支持)中检验这个想法。
在那之后,我将暂时从 UI 方面休息一下,然后专注于一些 Azure 和 Powershell,因为这两件事我已经考虑了很久。
一如既往,如果你认为这篇文章很有用,或者你喜欢它,任何投票/评论都将非常受欢迎。