Prism之Region(2)
在Prism之Region(1)中,介绍了一些Prism中页面组织以及切换的方式。这一篇将以一个很简单的示例程序来实践一下。
下面是效果图:
/201102101320047122.png)
先说Log,Prism内置了ILogFacade接口,在Prism提供的QuickStart项目里的Modularity中,有一个CallbackLogger,这里我们直接拿过来使用。然后在PrismRegionShell中放一个TextBox,将log的内容显示在这个TextBox中。值得一提的是,为了让输出新log的直接显示出来,需要将TextBox的滚动条滚动到最下面。这里采用的是如下方法:
1 | public void Log(string message, Category category, Priority priority) |
然后说一下左边的导航区。这里放置了一个ItemsControl,并将其设为Region。里面的两个按钮并不是直接写死到xaml里的,而是在两个Module初始化时动态添加进来的。也就是说,这个ItemsControl并不知道自己将要包含哪些项。这里我们使用IRegionManager.RegisterViewWithRegion(RegionNames.NavRegion, typeof(EmptyNavigationItem));这种方式来将视图注册到Region中。这样当该Region显示的时候两个视图才会被初始化。这里需要注意的是,一个Region里需要同时显示多个视图时,视图的顺序问题。比如ItemsControl,哪个先被注册就哪个显示在上面,但是由于Module的加载速度等原因,所以这时两个视图不一定谁在上面。现在我需要指定[导航示例]这个按钮在上,那么Prism为我们提供了ViewSortHintAttribute来解决这个问题。在需要进行排序的视图上添加上相应的attribute就可以了。
1 | [ViewSortHint("01")] |
在初始化导航实例的Module时,将导航示例的视图注册到内容区的Region,这时[上一个]按钮依然处于灰色状态,因为通过RegisterViewWithRegion方法显示的页面是不被记录的。当点击[ViewA][ViewB][ViewC]这三个按钮时,会采用RequestNavigate方法来进行页面的跳转,这时页面跳转的过程会被记录下来,此时就可以通过[上一个]和[下一个]按钮进行页面的前进和后退。
1 | void ToSpecifiedView(string viewName) |
注意这三个视图已经在初始化Module的时候使用IUnityContainer.RegisterType<object, ViewA>(ViewNames.ViewA)方法注册过了。
1 | public void Initialize() |
ViewA和ViewB都实现了INavigationAware接口,不同之处在于ViewA是在其对应的ViewModel ViewAViewModel类中实现的,而ViewB则直接在Code Behind中实现的。Prism对MVVM提供了良好的支持,因此既可以选择在视图中实现该接口也可以在对应的ViewModel中实现。
1 | public bool IsNavigationTarget(NavigationContext navigationContext) |
在ViewB中,IsNavigationTarget方法返回了false,而ViewA中则返回了true。可以通过点击三个按钮进行页面跳转,观察log可以发现,ViewA只创建了一次,而ViewB则每次都要重新创建。还有就是在跳转到ViewA的时候传递了参数,可以在OnNavigatedTo方法中取出参数。
1 | public void OnNavigatedTo(NavigationContext navigationContext) |
Prism之Region(1)
Prism可以帮助我们开发模块化程序,将程序分割成一个个独立的Module,分别进行开发。然后在程序运行的时候,将各个Module组合到一起,为程序提供各种各样的功能。通常来说,Module是一些视图和功能的集合,那么就需要一种办法来将这些视图以某种形式,在特定的时间展现出来。Prism通过Shell + Region来组织视图的布局,完成视图间的转换等。
/201101281728038372.png)
如上图所示,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显示在页面中的时候,它会去寻找与自己相关联的视图,并对其进行初始化。
/201101301728424507.png)
/201101301728498789.png)
这样做的好处是我们不必关注在什么时候创建视图,一切都会自动完成。缺点就是默认视图是确定的,当需要进行视图转换的时候,这种方式就行不通了。这时候就需要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 | public void Initialize() |
如果一个区域需要频繁地切换页面的话,如主内容区,可以使用View Injection的方式。
1 | IRegionManager regionManager = ...; |
可以看到,这时候已经生成了视图的实例。之前提到过,一个Region可以包含多个视图,这些视图会处于不同的状态,对于ItemsControl类型的Region来说,里面会显示很多个Item,所以添加进去就可以了;但是对于ContentControl这种Region,同一时刻只能显示一个视图,所以在添加进去之后还需要有一个Activate的过程。
使用URI来进行导航只需要提供需要切换的视图的名称就可以,并不需要了解视图的类型,从而达到解耦的目的,并且可以通过URI来进行参数传递。
1 | public void Initialize() |
首先注册一下视图的类型,其实就是将视图的名称与视图类型进行一下关联。在导航的时候调用RequestNavigate方法就可以了。
1 | void ToSpecifiedView(string viewName) |
Prism提供了UriQuery类来帮助我们在导航的时候传递参数。
1 | void ToSpecifiedView(string viewName) |
上面的代码判断当跳转到ViewA时,传递一个叫做Time的参数。那么怎样在视图中获取传递的参数呢?这里就要提一下INavigationAware接口了。这个接口使视图或者其对应的ViewModel也可以参与到页面导航的过程中来。所以这个接口既可以由视图来实现,也可以由视图的DataContext——通常指的就是ViewModel,来实现。
1 | public interface INavigationAware |
当从本页面转到其它页面的时候,会调用OnNavigatedFrom方法,navigationContext会包含目标页面的URI。
当从其它页面导航至本页面的时候,首先会调用IsNavigationTarget,IsNavigationTarget返回一个bool值,简单地说这个方法的作用就是告诉Prism,是重复使用这个视图的实例还是再创建一个。然后调用OnNavigatedTo方法。在导航到本页面的时候,就可以从navigationContext中取出传递过来的参数。
/201101311116493180.png)
使用导航API的另一个优点就是可以进行页面的前进和后退,一切由Prism完成。这个功能是由IRegionNavigationJournal接口提供的。
1 | public interface IRegionNavigationJournal |
其中CanGoBack和CanGoForward属性表示当前是否可以后退或前进。如果可以的话,可以使用GoBack和GoForward方法进行前进和后退。
1 | public class ActionControllerViewModel : NotificationObject |
/201101311119072583.png)
导航API还可以控制视图的生命周期,在页面跳转时进行确认拦截(Confirming or Cancelling Navigation)以及其它功能,可以参考 Developer’s Guide to Microsoft Prism。
Prism之Module
Prism的核心功能之一就是支持模块化应用程序开发(Modular Application Development),并且在运行时对各个模块进行动态管理。
使用Prism进行模块化开发首先要了解几个概念:
1.Module: Module是一些逻辑上相关的程序集或者资源文件的集合,在Silverlight程序中通常以xap文件为单位存在。而每一个Module中都需要有一个负责进行初始化工作以及与系统进行集成的角色,它需要实现IModule接口。IModule接口中只有一个Initialize方法,一方面这个接口将这个工程标记为一个Module,另一方面你可以在Initialize方法中实现一些逻辑,比如向容器中注册一些Service,或者将视图集成到程序中等等。
2.ModuleInfo: 在创建了一个Module之后,需要通知Prism这个Module的存在,也就是要注册一下。在Prism中,Module是以ModuleInfo的形式存在的。ModuleInfo记录了Module的信息,ModuleName属性是Module的标识符,相当于Module的ID;ModuleType是Module的AssemblyQualifiedName;DependsOn属性是该Module依赖的其它Module的ModuleName的集合,在加载该Module时,如果有依赖项没有加载的话,会先将依赖项加载;InitializationMode,有两种情况——WhenAvailable和OnDemand,当选择了WhenAvailable时,该Module会在程序启动时自动加载,如果选择了OnDemand,则会按需加载,默认情况下是WhenAvailable;Ref,存储该Module的位置,如XXX.xap;State,定义了Module从注册到加载到初始化的整个过程中的状态。
3.ModuleCatalog: ModuleCatalog实现了IModuleCatalog接口,它是ModuleInfo的容器,保存着系统中所有Module的信息,不仅会管理哪些Module需要加载,什么时候加载以什么顺序加载等问题,还要检查各个Module之间是否存在着循环依赖、是否有重复的Module等等。ModuleCatalog提供了含参构造方法和AddModule方法,可以通过代码将Module注册进去,同时也可以在xaml文件中配置好Module,然后通过ModuleCatalog.CreateFromXaml方法来加载。
4.ModuleManager: ModuleManager实现了IModuleManager接口。顾名思义就是管理Module的类。IModuleManager中含有两个方法和两个事件:Run方法会将所有InitializationMode为WhenAvailable的Module加载,然后进行初始化,初始化的工作委托给了IModuleInitializer来完成,它会获取到Module类(上面提到的实现了IModule接口的类)的实例,然后调用其Initialize方法。LoadModule方法用来加载InitializationMode为OnDemand的Module。两个事件分别用来通知下载Module的进度变化以及Module加载完成。
下面用一个示例程序来说明如何在Prism中进行模块化程序开发。
1.创建一个Silverlight Application,叫做PrismModule。
2.在Solution中添加三个Silverlight Application,分别叫做ModuleA, ModuleB, ModuleC。然后删除这三个工程中的App文件和MainPage文件。
3.在ModuleA工程下添加一个UserControl,叫做ViewA,然后再添加一个类,叫做ModuleA。并添加Microsoft.Practices.Prism和Microsoft.Practices.ServiceLocation引用。下面是ViewA和ModuleA的代码:
1 | <UserControl x:Class="ModuleA.ViewA" |
1 | public class ModuleA : IModule |
4.对ModuleB和ModuleC重复做步骤3的操作,只是将文本改成相应模块。
5.在PrismModule中添加对ModuleA、ModuleB、ModuleC、Prism、UnityExtensions还有Unity for Silverlight的引用,然后创建Shell和Bootstrapper。添加一个UserControl,叫做Shell;再添加一个类,叫做Bootstrapper。
Shell代码如下:
1 | <UserControl x:Class="PrismModule.Shell" |
Shell.xaml.cs代码如下:
1 | <UserControl x:Class="PrismModule.Shell" |
Bootstrapper代码如下:
1 | public class Bootstrapper : UnityBootstrapper |
将App.xaml.cs中的Application_Startup方法改为
1 | private void Application_Startup(object sender, StartupEventArgs e) |
6.现在已经有了Region,需要将各个Module中的View填充到Region中。修改ModuleA,ModuleB和ModuleC的Initialize方法。
1 | public void Initialize() |
将其中的A改为相应的字母。运行程序,结果如下:

我们点击按钮来加载ModuleC,因为ModuleC依赖于ModuleB,所以ModuleB也一块儿加载出来了。但是这与我们预期的效果不太一致。因为一共只load了一个xap文件,用WinRAR打开看一下,发现三个Module的程序集都在其中。

在Silverlight程序中,模块化程序开发应该不仅仅体现在开发时的模块化,运行时也应该是模块化的。比如ModuleA在程序加载时就load出来,但是ModuleB和ModuleC则是在点击了按钮后才load出来的,换句话说,在没点按钮前就不应该将ModuleB和ModuleC的程序集加载进来。现在由于PrismModule项目引用了三个Module,所以程序集会被一块打包进xap文件中。我们修改一下,将对ModuleB和ModuleC的引用的Copy Local属性设置为false:


重新编译一下,再次查看xap文件,发现已经没有了ModuleB和ModuleC。

运行程序,报错。很简单,因为我们在Bootstrapper中用到了ModuleB和ModuleC,缺少了这两个dll,程序没法运行。为了解决这个问题,我们把初始化ModuleCatalog的过程改一下,不使用代码,而是使用配置文件。在Silverlight中,Prism支持使用xaml文件作为配置文件。下面在PrismModule工程下新建一个资源文件,ModuleCatalog.xaml。内容如下:
1 | <Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
这里大体和用代码写一致,只不过Ref属性里要写明该Module对应的是哪个xap包。Prism在Silverlight程序中使用一个叫做XapModuleTypeLoader的类来加载Module,在将Module下载之后会获取AppManifest.xaml文件,也就是说如果你的Module是个类库工程的话,会在加载时产生错误。可以将几个类库的程序集文件包装在一个xap文件中作为一个Module来使用,或者自定义一个ModuleTypeLoader。
定义完Module的配置文件后,要改写Bootstrapper。首先删除用代码配置Module的方法ConfigureModuleCatalog,然后在CreateModuleCatalog方法中替换成一下内容:
1 | protected override IModuleCatalog CreateModuleCatalog() |
再次运行程序,正常运行。

这样就达到了按需加载的目的。节约带宽是一个好处,如果产品是分模块往外卖的时候,可以由客户按需定制。
不过再打开ModuleB和ModuleC的xap文件看一下,发现里面不仅有Module本身的程序集,还包括了引用的Prism的程序集等。而这些程序集其实已经在PrismModule.xap中包含了。完全没有必要重复下载。所以可以将多余的程序集的引用的Copy Local属性设置为false,这样就瘦身成功了。(想要避免重复加载相同的文件,也可以通过在项目的Properties面板中勾选Reduce XAP size by using application library caching选项)
如果你对Module的加载到执行的整个过程感兴趣,那么Prism本身提供了一个QuickStart,既有Unity版本也有Mef版本,不要错过。
Prism之Bootstrapper
在程序中使用框架必然要有一个切入点,框架会在这里进行初始化,处理相关配置信息等。在Prism中扮演这一角色的就是Bootstrapper。
Prism提供了一个抽象基类Bootstrapper,这个类里面包含了包含了许多空的虚方法,可以重写它们添加自己的逻辑。这个基类与任何容器无关,所以可以通过继承它来实现基于特定容器的Bootstrapper,不过通常我们大可不必这样做,因为Prism默认提供了两个基于特定容器的Bootstrapper——UnityBootstrapper和MefBootstrapper,分别使用Unity和Mef来实现依赖注入。而我们需要做的工作就是在这两个类之间选择一个适合自己的,稍微配置一下就可以了。当然如果你不喜欢这两个容器或者已有的程序使用了其它容器(如Spring.Net, Castle等),也可以通过继承Boostrapper抽象基类来实现自己的SpringBootstrapper和CastleBootstrapper。虽然UnityBootstrapper的代码看起来挺简单的,但是如果仿照这个来实现CastleBootstrapper却并不是那么容易的一件事(不信你可以试试),所以更好的办法是用现成的。
那么Bootstrapper都做了些什么呢?
创建Logger:
执行CreateLogger方法,默认创建一个EmptyLogger,不会在任何地方输出log。当然是可以扩展的,比如你可以使用Clog来做一个适配器。
创建并配置ModuleCatalog
执行CreateModuleCatalog方法,默认创建一个空的ModuleCatalog。然后执行ConfigureModuleCatalog方法,默认情况下这个方法是空的。可以重写这两个方法,加入自定义的获取ModuleCatalog的逻辑,比如在CreateModuleCatalog中可以从一个xaml文件中读取Module信息。
1 | protected override IModuleCatalog CreateModuleCatalog() |
- 创建并配置依赖注入容器
Prism中使用依赖注入来管理各个组件,你可以使用任何你熟悉的容器,比如Castle, Unity等。Prism中内置了对Unity以及Mef的支持,所以有两种预定义好的Bootstrapper: UnityBootstrapper和MefBootstrapper,其中分别采用UnityContainer和CompositionContainer作为依赖注入容器。以UnityBootstrapper为例,在这一步中会先调用CreateContainer方法,返回一个UnityContainer;然后调用ConfigureContainer方法,在这个方法中主要是将一些常用的类注册到容器中。
- 配置默认的Region适配器映射
为了使xaml中的UI控件可以使用Region,需要先注册一下。Prism默认支持Region的控件类型有:TabControl, Selector, ItemsControl, ContentControl。当然你也可以通过实现IRegionAdapter接口或者直接继承RegionAdapterBase
- 配置默认的Region 行为(Behavior)
为RegionBehaviorFactory添加一些默认的行为。这样可以扩展Region的行为。可以通过实现IRegionBehavior接口或继承RegionBehavior来自定义Region的行为,并重写ConfigureDefaultRegionBehaviors方法添加到Region。
- 注册框架异常类型
Prism提供了ExceptionExtensions类来帮助开发人员定位异常发生的根异常。在这一步通过调用RegisterFrameworkExceptionTypes方法向ExceptionExtensions中添加新的Root Exception。
- 创建并初始化Shell
首先调用CreateShell方法来创建一个Shell,这是一个抽象方法,通常这个方法中就是返回作为整个网站容器的页面。之后会将RegionManager attach到Shell上,然后更新定义的Regions,最后调用InitializeShell方法来初始化Shell。默认情况下这是个空方法,可以通过重写这个方法加入自定义的逻辑,可以在这个方法中将Shell作为Silverlight程序的根容器页面显示出来。
1 | protected override void InitializeShell()` |
- 初始化Modules
调用InitializeModules方法,实际上就是调用ModuleManager.Run方法,会调用ModuleCatalog中的所有InitializationMode为WhenAvailable的Module的Initialize方法。
至此,整个容器的初始化过程就完毕了。
值得一提的还有CommonServiceLocator,这同样是Patterns & Practices小组的产品。它的作用很简单,就是统一依赖注入容器的接口,使程序不必依赖于特定的容器,只需要使用ServiceLocator,然后去间接地使用其它各种各样的容器。在Prism的内部就是使用ServiceLocator来进行管理的。所以不管使用什么样的容器,都需要提供一个实现了IServiceLocator接口的适配器,如使用Unity要提供UnityServiceLocatorAdapter,使用Mef要提供MefServiceLocatorAdapter。这样不管外部使用什么容器,内部都不需要改变。所以如果要使用Prism重头开始构架一个程序,那么在整个程序中不依赖于特定的依赖注入容器接口,而是使用ServiceLocator是一个不错的选择,这样可以在需要的情况下很容易地更换容器,只需要重写一个Bootstrapper和一个ServiceLocatorAdapter就可以了。