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。
<ContentControl prism:RegionManager.RegionName="MainRegion" />
并不是所有的控件都可以作为Region的,需要为需要定义为Region的控件添加RegionAdapter。RegionAdapter的作用是为特定的控件创建相应的Region,并将控件与Region进行绑定,然后为Region添加一些行为。一个RegionAdapter需要实现IRegionAdapter接口,如果你需要自定义一个RegionAdapter,可以通过继承RegionAdapterBase类来省去一些工作。Prism为Silverlight提供了几个RegionAdapter:
- ContentControlRegionAdapter: 创建一个SingleActiveRegion并将其与ContentControl绑定
- ItemsControlRegionAdapter: 创建一个AllActiveRegion并将其与ItemsControl绑定
- SelectorRegionAdapter: 创建一个Region并将其与Selector绑定
- TabControlRegionAdapter: 创建一个Region并将其与TabControl绑定
从图中可以看到,导航区对应的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添加了更多的功能。
- AddToRegion: 将一个视图添加到一个Region中。
- RegisterViewWithRegion: 将一个视图和一个Region进行关联。当Region显示的时候,关联的视图才会显示,也就是说,在这个Region显示之前,关联的视图是不会被创建的。
- RequestNavigate: 进行页面切换,将指定的Region中显示的视图切换为指定的视图。
本文开头说过,需要在运行时将分散在各个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方法中完成这个过程。
public void Initialize() { logger.Log("初始化Navigation模块", Category.Debug, Priority.Low); _regionManager.RegisterViewWithRegion(RegionNames.NavRegion, typeof(NavigationItem)); _regionManager.RegisterViewWithRegion(RegionNames.MainRegion, // 两种方式都可以 () => _container.Resolve<NavigationContainer>() ); _regionManager.RegisterViewWithRegion(RegionNames.NavDemoActionRegion, typeof(ActionController)); }
如果一个区域需要频繁地切换页面的话,如主内容区,可以使用View Injection的方式。
IRegionManager regionManager = ...; IRegion mainRegion = regionManager.Regions["MainRegion"]; InboxView view = this.container.Resolve<InboxView>(); mainRegion.Add(view);
可以看到,这时候已经生成了视图的实例。之前提到过,一个Region可以包含多个视图,这些视图会处于不同的状态,对于ItemsControl类型的Region来说,里面会显示很多个Item,所以添加进去就可以了;但是对于ContentControl这种Region,同一时刻只能显示一个视图,所以在添加进去之后还需要有一个Activate的过程。
使用URI来进行导航只需要提供需要切换的视图的名称就可以,并不需要了解视图的类型,从而达到解耦的目的,并且可以通过URI来进行参数传递。
public void Initialize() { // 因为Prism无法确定每个视图都是什么类型,所以就使用了Object,因此在根据ViewName获取实例时,会使用IServiceLocator.GetInstance<Object>(ViewName)
_container.RegisterType<object, ViewA>(ViewNames.ViewA); _container.RegisterType<object, ViewB>(ViewNames.ViewB); _container.RegisterType<object, ViewC>(ViewNames.ViewC); }
首先注册一下视图的类型,其实就是将视图的名称与视图类型进行一下关联。在导航的时候调用RequestNavigate方法就可以了。
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类来帮助我们在导航的时候传递参数。
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,来实现。
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接口提供的。
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方法进行前进和后退。
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。