在现代用户界面设计中,区域(Region)的概念是至关重要的。区域可以包含任意数量的视图,并且可以是任意类型。当一个区域只包含一个视图时,一切看起来都很简单。但是,当一个区域关联了多个视图时,复杂性就开始出现了。如果这些视图是相同类型,并且包含嵌套区域,那么冲突就可能发生。
为了解决这个问题,PRISM框架提供了一个名为RegionManager
的工具,它要求所有区域都有一个唯一的名称。但是,当创建多个嵌套区域时,名称就不再唯一了。为了解决这个问题,RegionManager
可能会创建一个作用域,并将视图放入该作用域中。本质上,这是创建了一个新的RegionManager
实例,并将其分配给视图。这将视图与其他父区域中的视图隔离开来。
区域是通过使用RegionManager.RegionName
附加属性来创建的。这将元素与处理该区域的RegionManager
实例关联起来。当RegionManager.RegionName
属性附加到元素上时,RegionManager
还会附加另外两个属性:RegionManager.RegionManager
和RegionManager.RegionContext
。这两个属性非常重要,稍后将会看到。
关键特性是RegionManager.RegionManager
属性,它初始化时会引用持有视图的RegionManager
实例。所以,如果视图是带有作用域创建的,那么这个属性将持有对该作用域RegionManager
的引用。
实现作用域区域支持可以分为两个不同的方面:
1. 在创建视图时,需要告诉PRISM库(可能是动态的,逐个案例)这个特定的视图需要作用域区域。
2. 一旦视图创建,它应该能够直接访问与视图及其作用域相关联的RegionManager
。
为了解决第一个问题,需要在视图发现或区域导航期间,在PRISM实现中注入代码,以确定是否需要作用域,如果需要,就将视图添加到具有作用域的区域中。需要某种标志来指示是否需要作用域区域。这个信息可以通过几种不同的方式传递:
- 视图类可以实现某个接口,以指示需要作用域。它可以有一个成员在运行时返回布尔值,指示是否需要作用域。
- 可以使用属性来标记需要作用域区域的类型。
还有其他方法,但为了简单起见,将使用这两种方法。
为了解决第二个问题,解决方案并不那么直接。没有直接访问为视图的作用域创建的RegionManager
实例。没有返回引用,也没有在区域树中的任何地方提供嵌套作用域区域管理器的列表。为了解决这个障碍,可以使用RegionManager.RegionManager
附加属性。当视图被添加到区域时,它将这个附加属性初始化为处理这个特定区域的RegionManager
实例的引用。如果已经请求了作用域区域,它将持有对作用域RegionManager
实例的引用,而不是父/根RegionManager
。为了获得正确的RegionManager
实例,所要做的就是将这个附加属性绑定到视图的成员参数上。毕竟,附加属性的设计就是为了这个目的。
当前的PRISM4实现使用IRegionNavigationContentLoader
来创建新视图并将其添加到区域中。IRegionNavigationContentLoader
在RegionNavigationScopedContentLoader
类中实现。关键方法是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]
public class ScopedRegionManagerAttribute : Attribute, IProvideRegionScopeInfo
{
public ScopedRegionManagerAttribute()
{
CreateRegionManagerScope = true;
}
public string ViewName { get; set; }
public bool CreateRegionManagerScope { get; set; }
}
为了统一访问接口和属性,让属性也实现了IProvideRegionScopeInfo
接口。因此,在代码中添加了所有必要的检查之后,应该有类似这样的东西:
view = this.CreateNewRegionItem(candidateTargetContract);
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;
获得适当的区域管理器需要一些额外的代码。通常会使用依赖注入容器来解析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
,因为不希望允许这个属性从源内部更改。源可以是一个视图或视图模型,或者两者都是,如果使用单独的绑定对象。
这两种增强的结合将允许在视图发现以及PRISM区域导航中使用作用域区域。
已经包含了一个示例应用程序来演示这个解决方案。为了在项目中使用这段代码,只需将RegionNavigationScopedContentLoader.cs
添加到解决方案中,并将其与默认实现一起注册到DI容器中。有关示例,请参考Bootstrapper.cs
文件。