作用域 Prism 区域 (修复 Prism 工作项 8927)






4.86/5 (6投票s)
在视图发现或区域导航期间创建作用域区域。
引言
Prism 中的区域是屏幕上用于渲染内容的命名区域。(有关区域和 UI 组合的更多信息,请在此处查看)。
区域设计用于包含任意数量的任意类型的视图。如果区域只包含一个视图,一切都很简单。当多个视图与同一区域关联时,就会出现复杂情况。当视图类型相同并包含嵌套区域时,就会发生冲突。RegionManager
要求所有区域具有唯一的名称,但当创建多个嵌套区域时,名称不再是唯一的。
为了解决这个问题,RegionManager
可以创建一个作用域并将视图放入该作用域。本质上,会创建一个新的
RegionManager
实例并将其分配给视图。这会将视图与其他父区域内的视图隔离。有关作用域 RegionManager
的更多信息,请在此处查看。
Prism 程序员指南指出,“作用域区域仅在使用视图注入时可用”。换句话说,您可以创建具有作用域区域管理器视图的唯一时间是当您在区域实例上调用 Add(Object view, string viewName, bool createRegionManagerScope )
时,或者手动创建作用域区域管理器并使用对 SetRegionManager( DependencyObject target, IRegionManager value)
的调用将其附加到视图。
本文演示了一种在视图发现期间以及在区域导航期间启用作用域区域创建的方法。
背景
区域是通过使用 RegionManager.RegionName
附加属性来装饰元素来创建的。这会将该元素与负责处理该区域的 RegionManager
实例关联起来。当 RegionManager.RegionName
属性附加到元素时,RegionManager
还会将两个附加属性 RegionManager.RegionManager
和 RegionManager.RegionContext
附加到同一元素。这一点非常重要,您稍后会看到。有关 RegionManager.RegionContext
和 RegionManager.RegionManager
属性的深入说明,请访问此链接。
RegionManager.RegionManager
属性的关键特性是它使用持有视图的 RegionManager
实例的引用进行初始化。因此,如果视图是使用作用域创建的,该属性将持有对该作用域 RegionManager
的引用。
问题
作用域区域支持的实现可以分为两个独立方面
- 在创建视图时,需要告知 PRISM 库 (可能动态地,根据具体情况) 此特定视图需要作用域区域。
- 一旦创建了视图,它应该可以直接访问与视图及其作用域关联的
RegionManager
。
解决方案
需要作用域区域
在视图发现或区域导航期间,Prism 实现只是从 DI 容器解析视图实例并将其添加到区域。为了启用作用域区域支持,我们需要在这两个步骤之间注入代码,以确定是否需要作用域,如果需要,则将视图添加到带有作用域的区域。我们必须有一种标志来指示是否需要作用域区域。此信息可以通过几种不同的方式传递
- 视图类可以实现特定的接口来指示需要作用域。它可能有一个返回布尔值的成员,指示运行时是否需要作用域。
- 可以使用属性来标记类型为需要作用域区域。
还有其他方法,但为了简单起见,我们将使用这两种。
访问正确的区域管理器
第二个问题的解决方案并不那么直接。无法直接访问为视图的作用域创建的 RegionManager
实例。没有返回值,并且在区域树的任何地方都无法获得嵌套作用域区域管理器的列表。为了解决这个障碍,我们可以使用 RegionManager.RegionManager
附加属性。当视图被添加到区域时,它会使用负责该特定区域的 RegionManager
实例的引用来初始化此附加属性。如果请求了作用域区域,它将持有对**作用域** RegionManager
实例的引用,而不是父/根 RegionManager
。要访问正确的 RegionManager
实例,我们只需要将此附加属性绑定到视图的成员参数。毕竟,附加属性就是为此而设计的。
实现
当前实现
PRISM 4 使用 IRegionNavigationContentLoader
来创建新视图并将其添加到区域。RegionNavigationScopedContentLoader
类实现了 IRegionNavigationContentLoader
。所有魔法发生的关键方法称为 LoadContent
。检查 源代码 会发现,在对区域导航进行一些必要的操作后,加载器会执行这三行代码
...
view = this.CreateNewRegionItem(candidateTargetContract);
region.Add(view);
return view;
它创建新视图,将其添加到区域,然后将新视图返回给调用者。因此,要扩展此行为并添加作用域区域支持,我们只需确定是否需要作用域区域,然后添加带有作用域的视图。最合乎逻辑的方法是将标志作为参数传递给 LoadContent
函数。不幸的是,在视图发现或区域导航期间,内容加载器由 PRISM 库内部调用,没有简单的方法可以更改它。
与其重新设计 PRISM,不如让视图类来承载此信息。为此,我们可以使用自定义属性标记类型,或让类实现某个接口。这些方法各有优缺点,因此出于演示目的,我将同时实现两者。
接口
为了将视图添加到带有作用域的区域,应调用 IRegion.Add(view, viewName, createRegionManagerScope)
方法。在此函数中,view 参数是我们刚刚创建的新视图,而 Name 和 Scope 标志是我们需要的缺失信息。我们将实现一个接口来提供缺失的信息片段
public interface IProvideRegionScopeInfo
{
string ViewName { get; }
bool CreateRegionManagerScope { get; }
}
通过实现此接口,视图类可以在运行时提供关于是否需要作用域的信息,并为视图实例提供唯一的名称。
Attribute
有时,只需使用一个属性来标记一个类,表明该视图的每个实例都需要作用域区域就足够了。为此,我们可以使用以下属性
// Attribute used to indicate if scoped region manager needs to be
// instantiated and attached to created view.
public class ScopedRegionManagerAttribute : Attribute, IProvideRegionScopeInfo
{
// By default it requires Scoped RegionManager and no Name;
public ScopedRegionManagerAttribute()
{
CreateRegionManagerScope = true;
}
// Name of the View when added to Region by
// calling <see cref="Add(Object view, string viewName,
// bool createRegionManagerScope"/>
public string ViewName { get; set; }
// Indicates if new scoped RegionManaged should be associated with the View
public bool CreateRegionManagerScope { get; set; }
}
为了统一访问接口和属性,我让该属性也实现了 IProvideRegionScopeInfo
接口。因此,在对代码添加所有必要的检查后,我们应该会得到类似这样的结果
...
view = this.CreateNewRegionItem(candidateTargetContract);
// Check if scoped region is required
IProvideRegionScopeInfo info = (view as IProvideRegionScopeInfo)
?? (ScopedRegionManagerAttribute)view.GetType()
.GetCustomAttributes(typeof(ScopedRegionManagerAttribute), false)
.FirstOrDefault();
if (null == info)
region.Add(view);
else
region.Add(view, info.ViewName, info.CreateRegionManagerScope);
return view;
访问 RegionManager
访问合适的区域管理器需要额外的几行代码。通常我们会使用依赖注入容器来解析 IRegionManager
接口。我们仍然可以这样做来访问根 RegionManager
,但为了访问负责视图实例的 RegionManager
,我们必须使用 RegionManager.RegionManager
附加属性。这样做非常简单,并且在视图的构造函数中完成,代码如下
Binding binding = new Binding("LocalRegionManager")
{ Mode= BindingMode.OneWayToSource, Source = viewModel };
this.SetBinding(Microsoft.Practices.Prism.Regions.RegionManager.RegionManagerProperty, binding);
绑定的模式是 BindingMode.OneWayToSource
,因为我们不希望允许此属性在源内部被更改。源可以是 View
或 ViewModel
,或者如果使用单独的 Binding 对象,则两者都是。
因此,这两个增强功能的组合将允许使用作用域区域进行视图发现以及 PRISM 区域导航。
使用代码
我包含了一个演示此解决方案的示例应用程序。为了在您的项目中在此代码,只需将 RegionNavigationScopedContentLoader.cs 添加到您的解决方案中,并使用 DI 容器注册它,而不是默认实现。有关示例,请参考 Bootstrapper.cs 文件。
致谢
本文的示例基于发布在 Prism Work Item 8927 上的项目。它被修改以实现此修复,但保留了项目的其余部分,以便进行比较。
历史
- 2012 年 1 月 2 日 – 首次发布。
- 2012 年 4 月 1 日 - 添加了致谢。
- 2012 年 4 月 10 日 - 更改了部分段落的语言。
- 2013 年 3 月 4 日 - 更改了标题以包含工作项引用。