Prism可以帮助我们开发模块化程序,将程序分割成一个个独立的Module,分别进行开发。然后在程序运行的时候,将各个Module组合到一起,为程序提供各种各样的功能。通常来说,Module是一些视图和功能的集合,那么就需要一种办法来将这些视图以某种形式,在特定的时间展现出来。Prism通过Shell + Region来组织视图的布局,完成视图间的转换等。
如上图所示,Shell相当于ASP.NET中的母版页,它定义了页面的布局、主题等。其中的导航区和内容区是预留出来的需要进行填充内容的部分,也就是Region,起到占位符的作用,程序会在运行时动态地向Region中填充内容。
那么如何将一个区域定义为Region呢?
首先在引入Prism的命名空间
xmlns:prism="http://www.codeplex.com/prism" 如果IDE无法找到这个命名空间的话,需要先注册Prism。
然后在需要定义为Region的控件上加上Attached Property。
并不是所有的控件都可以作为Region的,需要为需要定义为Region的控件添加RegionAdapter。RegionAdapter的作用是为特定的控件创建相应的Region,并将控件与Region进行绑定,然后为Region添加一些行为。一个RegionAdapter需要实现IRegionAdapter接口,如果你需要自定义一个RegionAdapter,可以通过继承RegionAdapterBase类来省去一些工作。Prism为Silverlight提供了几个RegionAdapter:
从图中可以看到,导航区对应的NavigationRegion中四个视图都是亮着的,而内容区对应的ContentRegion中四个视图只有一个是亮着的(橘黄色代表显示在页面中)。ItemsControl本来就是由许多个Item组成的,因此ItemsControlRegionAdapter会创建AllActiveRegion,这种类型的Region中所有Active的视图都会显示在ItemsControl中;而ContentControl只能容纳一个Content,所以ContentControlRegionAdapter创建了一个SingleActiveRegion,其中的视图只有一个是处于Active状态的,会显示在ContentControl中,其它的都是不可见的,需要将它们激活(Active),才能使其显示。
通常我们并不直接和Region打交道,而是通过RegionManager,它实现了IRegionManager接口。IRegionManager接口包含一个只读属性Regions,是Region的集合,还有一个CreateRegionManager方法。Prism通过RegionManagerExtensions类使用扩展方法为IRegionManager添加了更多的功能。
本文开头说过,需要在运行时将分散在各个Module的视图显示在页面特定的位置上。那么首先就需要定义页面显示的地方,即Region。然后就是要定义创建视图的时机和方式。在Prism中有两种方式来定义视图与Region之间的映射关系——View Discovery和View Injection。
View Discovery是以声明式的方式来建立Region和视图之间的关系。如上图中的导航区,需要在导航区显示的时候就将各个导航视图填充到其中。而内容区中也需要一个默认显示的内容视图。因此也可以这样理解View Discovery,就是指定一个Region的默认视图。我们可以使用IRegionManager.RegisterViewWithRegion方法来声明某个Region默认应该显示哪个视图。注意这里是Register,是注册,也就是说不会马上创建该视图。当Region显示在页面中的时候,它会去寻找与自己相关联的视图,并对其进行初始化。
这样做的好处是我们不必关注在什么时候创建视图,一切都会自动完成。缺点就是默认视图是确定的,当需要进行视图转换的时候,这种方式就行不通了。这时候就需要View Injection。
View Injection可以让我们对于Region中显示的视图有更精确的控制。通常可以通过调用IRegionManager.AddToRegion方法或者是IRegionManager.Regions[“RegionName”].Add方法来向一个Region中添加一个视图的实例。对于SingleActiveRegion(ContentControlRegionAdapter会创建这种类型的Region),可以通过IRegion.Activate方法将一个已经添加到Region中的视图显示出来。当然也可以通过IRegion.Deactivate方法来将视图状态置为非激活或者干脆调用IRegion.Remove方法将视图移除。可以看到,因为要添加的是视图的实例,所以需要仔细地设计在什么时候使用View Injection,以免造成不必要的开销。
在Prism 4.0中新添加了一些导航API,这套API大大地简化了View Injection的流程,它使用URI来进行Region中视图的导航,然后会根据URI来创建视图,并将其添加到Region中,然后激活该视图。导航API的出现不只是为了简化View Injection的过程,它还提供了前进、后退的功能,并且对MVVM模式下的导航有良好的支持,还能够在进行导航的时候传递参数等等。所以推荐的方式是使用新的导航API,也就是使用IRegionManager.RequestNavigate方法。
如果一个页面相对来说不大变化,如导航区,在程序初始化的过程完成后就不会轻易地变动,这时候就较适合于使用RegisterViewWithRegion方法,通常可以在Module的Initialize方法中完成这个过程。
1
2
3
4
5
6
7
8
|
public
void
Initialize()
{
logger.Log(
"初始化Navigation模块"
, Category.Debug, Priority.Low);
_regionManager.RegisterViewWithRegion(RegionNames.NavRegion,
typeof
(NavigationItem));
_regionManager.RegisterViewWithRegion(RegionNames.MainRegion,
// 两种方式都可以
() => _container.Resolve
_regionManager.RegisterViewWithRegion(RegionNames.NavDemoActionRegion,
typeof
(ActionController));
}
|
如果一个区域需要频繁地切换页面的话,如主内容区,可以使用View Injection的方式。
1
2
3
4
|
IRegionManager regionManager = ...;
IRegion mainRegion = regionManager.Regions[
"MainRegion"
];
InboxView view =
this
.container.Resolve
mainRegion.Add(view);
|
可以看到,这时候已经生成了视图的实例。之前提到过,一个Region可以包含多个视图,这些视图会处于不同的状态,对于ItemsControl类型的Region来说,里面会显示很多个Item,所以添加进去就可以了;但是对于ContentControl这种Region,同一时刻只能显示一个视图,所以在添加进去之后还需要有一个Activate的过程。
使用URI来进行导航只需要提供需要切换的视图的名称就可以,并不需要了解视图的类型,从而达到解耦的目的,并且可以通过URI来进行参数传递。
1
2
3
|
public
void
Initialize()
{
// 因为Prism无法确定每个视图都是什么类型,所以就使用了Object,因此在根据ViewName获取实例时,会使用IServiceLocator.GetInstance
|
1
2
3
4
|
_container.RegisterType<
object
, ViewA>(ViewNames.ViewA);
_container.RegisterType<
object
, ViewB>(ViewNames.ViewB);
_container.RegisterType<
object
, ViewC>(ViewNames.ViewC);
}
|
首先注册一下视图的类型,其实就是将视图的名称与视图类型进行一下关联。在导航的时候调用RequestNavigate方法就可以了。
1
2
3
4
5
6
|
void
ToSpecifiedView(
string
viewName)
{
Uri uri =
new
Uri(viewName, UriKind.Relative);
_regionManager.RequestNavigate(RegionNames.NavDemoShowRegion, uri);
logger.Log(
"跳转到视图 ["
+ viewName +
"]"
, Category.Info, Priority.Low);
}
|
Prism提供了UriQuery类来帮助我们在导航的时候传递参数。
1
2
3
4
5
6
7
8
9
10
|
void
ToSpecifiedView(
string
viewName)
{
UriQuery query =
new
UriQuery();
if
(viewName == ViewNames.ViewA)
{
query.Add(
"Time"
, DateTime.Now.ToShortTimeString());
}
Uri uri =
new
Uri(viewName + query.ToString(), UriKind.Relative);
_regionManager.RequestNavigate(RegionNames.NavDemoShowRegion, uri, CallbackHandler);
// 回调方法可加可不加
}
|
上面的代码判断当跳转到ViewA时,传递一个叫做Time的参数。那么怎样在视图中获取传递的参数呢?这里就要提一下INavigationAware接口了。这个接口使视图或者其对应的ViewModel也可以参与到页面导航的过程中来。所以这个接口既可以由视图来实现,也可以由视图的DataContext——通常指的就是ViewModel,来实现。
1
2
3
4
5
6
|
public
interface
INavigationAware
{
bool
IsNavigationTarget(NavigationContext navigationContext);
void
OnNavigatedTo(NavigationContext navigationContext);
void
OnNavigatedFrom(NavigationContext navigationContext);
}
|
当从本页面转到其它页面的时候,会调用OnNavigatedFrom方法,navigationContext会包含目标页面的URI。
当从其它页面导航至本页面的时候,首先会调用IsNavigationTarget,IsNavigationTarget返回一个bool值,简单地说这个方法的作用就是告诉Prism,是重复使用这个视图的实例还是再创建一个。然后调用OnNavigatedTo方法。在导航到本页面的时候,就可以从navigationContext中取出传递过来的参数。
使用导航API的另一个优点就是可以进行页面的前进和后退,一切由Prism完成。这个功能是由IRegionNavigationJournal接口提供的。
1
2
3
4
5
6
7
8
9
10
11
|
public
interface
IRegionNavigationJournal
{
bool
CanGoBack {
get
; }
bool
CanGoForward {
get
; }
IRegionNavigationJournalEntry CurrentEntry {
get
; }
INavigateAsync NavigationTarget {
get
;
set
; }
void
Clear();
void
GoBack();
void
GoForward();
void
RecordNavigation(IRegionNavigationJournalEntry entry);
}
|
其中CanGoBack和CanGoForward属性表示当前是否可以后退或前进。如果可以的话,可以使用GoBack和GoForward方法进行前进和后退。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public
class
ActionControllerViewModel : NotificationObject
{
private
IRegion _demoShowRegion;
public
bool
CanGoBack
{
get
{
return
_demoShowRegion.NavigationService.Journal.CanGoBack;
}
}
public
bool
CanGoForward
{
get
{
return
_demoShowRegion.NavigationService.Journal.CanGoForward;
}
}
void
ToPrevious()
{
_demoShowRegion.NavigationService.Journal.GoBack();
ResetNavigationButtonState();
}
void
ToNext()
{
_demoShowRegion.NavigationService.Journal.GoForward();
ResetNavigationButtonState();
}
void
ResetNavigationButtonState()
{
RaisePropertyChanged(() =>
this
.CanGoBack);
RaisePropertyChanged(() =>
this
.CanGoForward);
}
}
|
导航API还可以控制视图的生命周期,在页面跳转时进行确认拦截(Confirming or Cancelling Navigation)以及其它功能,可以参考 Developer’s Guide to Microsoft Prism。