0%

Prism之Region(1)中,介绍了一些Prism中页面组织以及切换的方式。这一篇将以一个很简单的示例程序来实践一下。

下面是效果图:

image

先说Log,Prism内置了ILogFacade接口,在Prism提供的QuickStart项目里的Modularity中,有一个CallbackLogger,这里我们直接拿过来使用。然后在PrismRegionShell中放一个TextBox,将log的内容显示在这个TextBox中。值得一提的是,为了让输出新log的直接显示出来,需要将TextBox的滚动条滚动到最下面。这里采用的是如下方法:

1
2
3
4
5
6
public void Log(string message, Category category, Priority priority)
{
this.LogContainer.Text += string.Format(CultureInfo.CurrentUICulture, "[{0}][{1}] {2}\r\n", category, priority, message);
// 这段代码的作用是让文本框的滚动条滚动到最底部
LogContainer.Select(LogContainer.Text.Length, LogContainer.Text.Length);
}

然后说一下左边的导航区。这里放置了一个ItemsControl,并将其设为Region。里面的两个按钮并不是直接写死到xaml里的,而是在两个Module初始化时动态添加进来的。也就是说,这个ItemsControl并不知道自己将要包含哪些项。这里我们使用IRegionManager.RegisterViewWithRegion(RegionNames.NavRegion, typeof(EmptyNavigationItem));这种方式来将视图注册到Region中。这样当该Region显示的时候两个视图才会被初始化。这里需要注意的是,一个Region里需要同时显示多个视图时,视图的顺序问题。比如ItemsControl,哪个先被注册就哪个显示在上面,但是由于Module的加载速度等原因,所以这时两个视图不一定谁在上面。现在我需要指定[导航示例]这个按钮在上,那么Prism为我们提供了ViewSortHintAttribute来解决这个问题。在需要进行排序的视图上添加上相应的attribute就可以了。

1
2
3
4
5
[ViewSortHint("01")]
public partial class NavigationItem : UserControl

[ViewSortHint("02")]
public partial class EmptyNavigationItem : UserControl

在初始化导航实例的Module时,将导航示例的视图注册到内容区的Region,这时[上一个]按钮依然处于灰色状态,因为通过RegisterViewWithRegion方法显示的页面是不被记录的。当点击[ViewA][ViewB][ViewC]这三个按钮时,会采用RequestNavigate方法来进行页面的跳转,这时页面跳转的过程会被记录下来,此时就可以通过[上一个]和[下一个]按钮进行页面的前进和后退。

1
2
3
4
5
6
7
8
9
10
11
12
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);
logger.Log("跳转到视图 [" + viewName + "]", Category.Info, Priority.Low);
ResetNavigationButtonState();
}

注意这三个视图已经在初始化Module的时候使用IUnityContainer.RegisterType<object, ViewA>(ViewNames.ViewA)方法注册过了。

1
2
3
4
5
6
7
8
9
10
11
12
13
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));

// 注意注册的类型的必须是object,因为Prism无法确定视图的类型,所以就用了object
_container.RegisterType<object, ViewA>(ViewNames.ViewA);
_container.RegisterType<object, ViewB>(ViewNames.ViewB);
_container.RegisterType<object, ViewC>(ViewNames.ViewC);
}

ViewA和ViewB都实现了INavigationAware接口,不同之处在于ViewA是在其对应的ViewModel ViewAViewModel类中实现的,而ViewB则直接在Code Behind中实现的。Prism对MVVM提供了良好的支持,因此既可以选择在视图中实现该接口也可以在对应的ViewModel中实现。

1
2
3
4
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return false;
}

在ViewB中,IsNavigationTarget方法返回了false,而ViewA中则返回了true。可以通过点击三个按钮进行页面跳转,观察log可以发现,ViewA只创建了一次,而ViewB则每次都要重新创建。还有就是在跳转到ViewA的时候传递了参数,可以在OnNavigatedTo方法中取出参数。

1
2
3
4
5
6
public void OnNavigatedTo(NavigationContext navigationContext)
{
UriQuery query = navigationContext.Parameters;
string time = query["Time"];
logger.Log(string.Format("ViewA: 现在时间 {0}", time), Category.Info, Priority.Medium);
}

Prism可以帮助我们开发模块化程序,将程序分割成一个个独立的Module,分别进行开发。然后在程序运行的时候,将各个Module组合到一起,为程序提供各种各样的功能。通常来说,Module是一些视图和功能的集合,那么就需要一种办法来将这些视图以某种形式,在特定的时间展现出来。Prism通过Shell + Region来组织视图的布局,完成视图间的转换等。

image

如上图所示,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显示在页面中的时候,它会去寻找与自己相关联的视图,并对其进行初始化。

1-30-2011 5-03-27 PM

1-30-2011 5-04-22 PM

这样做的好处是我们不必关注在什么时候创建视图,一切都会自动完成。缺点就是默认视图是确定的,当需要进行视图转换的时候,这种方式就行不通了。这时候就需要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<NavigationContainer>() );
_regionManager.RegisterViewWithRegion(RegionNames.NavDemoActionRegion, typeof(ActionController));
}

如果一个区域需要频繁地切换页面的话,如主内容区,可以使用View Injection的方式。

1
2
3
4
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来进行参数传递。

1
2
3
4
5
6
7
8
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方法就可以了。

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中取出传递过来的参数。

image

使用导航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);
}
}

image

导航API还可以控制视图的生命周期,在页面跳转时进行确认拦截(Confirming or Cancelling Navigation)以及其它功能,可以参考 Developer’s Guide to Microsoft Prism

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
2
3
4
5
6
7
8
9
10
11
12
<UserControl x:Class="ModuleA.ViewA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="Module A" FontSize="22" />
</Grid>
</UserControl>
1
2
3
4
5
6
public class ModuleA : IModule
{
public void Initialize()
{
}
}

4.对ModuleB和ModuleC重复做步骤3的操作,只是将文本改成相应模块。

5.在PrismModule中添加对ModuleA、ModuleB、ModuleC、Prism、UnityExtensions还有Unity for Silverlight的引用,然后创建Shell和Bootstrapper。添加一个UserControl,叫做Shell;再添加一个类,叫做Bootstrapper。

Shell代码如下:

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
<UserControl x:Class="PrismModule.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://www.codeplex.com/prism"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">

<StackPanel Margin="50">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionA" />
</Border>
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionB" />
</Border>
<StackPanel>
<Border BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionC" />
</Border>
<Button Content="Load Module C" Click="LoadModuleC" Width="120" Height="25" />
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

Shell.xaml.cs代码如下:

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
<UserControl x:Class="PrismModule.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://www.codeplex.com/prism"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="800">

<StackPanel Margin="50">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionA" />
</Border>
<Border VerticalAlignment="Top" BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionB" />
</Border>
<StackPanel>
<Border BorderBrush="Red" BorderThickness="2" Width="200" Height="100">
<ContentControl prism:RegionManager.RegionName="RegionC" />
</Border>
<Button Content="Load Module C" Click="LoadModuleC" Width="120" Height="25" />
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

Bootstrapper代码如下:

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
38
39
40
41
42
43
44
45
46
47
48
49
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return this.Container.TryResolve<Shell>();
}

protected override void InitializeShell()
{
App.Current.RootVisual = (UIElement)this.Shell;
}

protected override IModuleCatalog CreateModuleCatalog()
{
return new ModuleCatalog();
}

protected override void ConfigureModuleCatalog()
{
Type typeA = typeof(ModuleA.ModuleA);
ModuleInfo moduleA = new ModuleInfo
{ // ModuleA没有设置InitializationMode,默认为WhenAvailable
ModuleName = typeA.Name,
ModuleType = typeA.AssemblyQualifiedName,
};

Type typeB = typeof(ModuleB.ModuleB);
ModuleInfo moduleB = new ModuleInfo
{
ModuleName = typeB.Name,
ModuleType = typeB.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand,
};

Type typeC = typeof(ModuleC.ModuleC);
ModuleInfo moduleC = new ModuleInfo
{
ModuleName = typeC.Name,
ModuleType = typeC.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand,
// ModuleC依赖于ModuleB
DependsOn = new Collection<string> { moduleB.ModuleName },
};

this.ModuleCatalog.AddModule(moduleA);
this.ModuleCatalog.AddModule(moduleB);
this.ModuleCatalog.AddModule(moduleC);
}
}

将App.xaml.cs中的Application_Startup方法改为

1
2
3
4
5
private void Application_Startup(object sender, StartupEventArgs e)
{
Bootstrapper bootstrapper = new Bootstrapper();
bootstrapper.Run();
}

6.现在已经有了Region,需要将各个Module中的View填充到Region中。修改ModuleA,ModuleB和ModuleC的Initialize方法。

1
2
3
4
5
public void Initialize()
{
ServiceLocator.Current.GetInstance<IRegionManager>().
RegisterViewWithRegion("RegionA", typeof(ViewA));
}

将其中的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:Modularity="clr-namespace:Microsoft.Practices.Prism.Modularity;assembly=Microsoft.Practices.Prism">

<Modularity:ModuleInfo Ref="ModuleA.xap" ModuleName="ModuleA"
ModuleType="ModuleA.ModuleA, ModuleA, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

<Modularity:ModuleInfo Ref="ModuleB.xap" ModuleName="ModuleB" InitializationMode="OnDemand"
ModuleType="ModuleB.ModuleB, ModuleB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

<Modularity:ModuleInfo Ref="ModuleC.xap" ModuleName="ModuleC" InitializationMode="OnDemand"
ModuleType="ModuleC.ModuleC, ModuleC, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<Modularity:ModuleInfo.DependsOn>
<sys:String>ModuleB</sys:String>
</Modularity:ModuleInfo.DependsOn>
</Modularity:ModuleInfo>
</Modularity:ModuleCatalog>

这里大体和用代码写一致,只不过Ref属性里要写明该Module对应的是哪个xap包。Prism在Silverlight程序中使用一个叫做XapModuleTypeLoader的类来加载Module,在将Module下载之后会获取AppManifest.xaml文件,也就是说如果你的Module是个类库工程的话,会在加载时产生错误。可以将几个类库的程序集文件包装在一个xap文件中作为一个Module来使用,或者自定义一个ModuleTypeLoader。

定义完Module的配置文件后,要改写Bootstrapper。首先删除用代码配置Module的方法ConfigureModuleCatalog,然后在CreateModuleCatalog方法中替换成一下内容:

1
2
3
4
5
protected override IModuleCatalog CreateModuleCatalog()
{
return Microsoft.Practices.Prism.Modularity.ModuleCatalog.CreateFromXaml(
new Uri("/PrismModule;component/ModuleCatalog.xaml", UriKind.Relative));
}

再次运行程序,正常运行。

这样就达到了按需加载的目的。节约带宽是一个好处,如果产品是分模块往外卖的时候,可以由客户按需定制。

不过再打开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,这个类里面包含了包含了许多空的虚方法,可以重写它们添加自己的逻辑。这个基类与任何容器无关,所以可以通过继承它来实现基于特定容器的Bootstrapper,不过通常我们大可不必这样做,因为Prism默认提供了两个基于特定容器的Bootstrapper——UnityBootstrapper和MefBootstrapper,分别使用Unity和Mef来实现依赖注入。而我们需要做的工作就是在这两个类之间选择一个适合自己的,稍微配置一下就可以了。当然如果你不喜欢这两个容器或者已有的程序使用了其它容器(如Spring.Net, Castle等),也可以通过继承Boostrapper抽象基类来实现自己的SpringBootstrapper和CastleBootstrapper。虽然UnityBootstrapper的代码看起来挺简单的,但是如果仿照这个来实现CastleBootstrapper却并不是那么容易的一件事(不信你可以试试),所以更好的办法是用现成的。

那么Bootstrapper都做了些什么呢?
  1. 创建Logger:

    执行CreateLogger方法,默认创建一个EmptyLogger,不会在任何地方输出log。当然是可以扩展的,比如你可以使用Clog来做一个适配器。

  2. 创建并配置ModuleCatalog

执行CreateModuleCatalog方法,默认创建一个空的ModuleCatalog。然后执行ConfigureModuleCatalog方法,默认情况下这个方法是空的。可以重写这两个方法,加入自定义的获取ModuleCatalog的逻辑,比如在CreateModuleCatalog中可以从一个xaml文件中读取Module信息。

1
2
3
4
5
protected override IModuleCatalog CreateModuleCatalog()
{
return ModuleCatalog.CreateFromXaml(new Uri("/AssemblyName;component/ModulesCatalog.xaml", UriKind.Relative));
}

  1. 创建并配置依赖注入容器

Prism中使用依赖注入来管理各个组件,你可以使用任何你熟悉的容器,比如Castle, Unity等。Prism中内置了对Unity以及Mef的支持,所以有两种预定义好的Bootstrapper: UnityBootstrapper和MefBootstrapper,其中分别采用UnityContainer和CompositionContainer作为依赖注入容器。以UnityBootstrapper为例,在这一步中会先调用CreateContainer方法,返回一个UnityContainer;然后调用ConfigureContainer方法,在这个方法中主要是将一些常用的类注册到容器中。

  1. 配置默认的Region适配器映射

为了使xaml中的UI控件可以使用Region,需要先注册一下。Prism默认支持Region的控件类型有:TabControl, Selector, ItemsControl, ContentControl。当然你也可以通过实现IRegionAdapter接口或者直接继承RegionAdapterBase来使其它控件也支持Region。

  1. 配置默认的Region 行为(Behavior)

为RegionBehaviorFactory添加一些默认的行为。这样可以扩展Region的行为。可以通过实现IRegionBehavior接口或继承RegionBehavior来自定义Region的行为,并重写ConfigureDefaultRegionBehaviors方法添加到Region。

  1. 注册框架异常类型

Prism提供了ExceptionExtensions类来帮助开发人员定位异常发生的根异常。在这一步通过调用RegisterFrameworkExceptionTypes方法向ExceptionExtensions中添加新的Root Exception。

  1. 创建并初始化Shell

首先调用CreateShell方法来创建一个Shell,这是一个抽象方法,通常这个方法中就是返回作为整个网站容器的页面。之后会将RegionManager attach到Shell上,然后更新定义的Regions,最后调用InitializeShell方法来初始化Shell。默认情况下这是个空方法,可以通过重写这个方法加入自定义的逻辑,可以在这个方法中将Shell作为Silverlight程序的根容器页面显示出来。

1
2
3
4
5
protected override void InitializeShell()`
{
Application.Current.RootVisual = Shell;
}

  1. 初始化Modules

调用InitializeModules方法,实际上就是调用ModuleManager.Run方法,会调用ModuleCatalog中的所有InitializationMode为WhenAvailable的Module的Initialize方法。

至此,整个容器的初始化过程就完毕了。

值得一提的还有CommonServiceLocator,这同样是Patterns & Practices小组的产品。它的作用很简单,就是统一依赖注入容器的接口,使程序不必依赖于特定的容器,只需要使用ServiceLocator,然后去间接地使用其它各种各样的容器。在Prism的内部就是使用ServiceLocator来进行管理的。所以不管使用什么样的容器,都需要提供一个实现了IServiceLocator接口的适配器,如使用Unity要提供UnityServiceLocatorAdapter,使用Mef要提供MefServiceLocatorAdapter。这样不管外部使用什么容器,内部都不需要改变。所以如果要使用Prism重头开始构架一个程序,那么在整个程序中不依赖于特定的依赖注入容器接口,而是使用ServiceLocator是一个不错的选择,这样可以在需要的情况下很容易地更换容器,只需要重写一个Bootstrapper和一个ServiceLocatorAdapter就可以了。

Prism是由微软Patterns & Practices团队开发的项目,目的在于帮助开发人员构建松散耦合的、更灵活、更易于维护并且更易于测试的WPF应用或是Silverlight应用以及Windows Phone 7应用。使用Prism可以使程序开发更趋于模块化,整个项目将由多个离散的、松耦合的模块组成,而各个模块又可以又不同的开发者或团队进行开发、测试和部署。目前Prism的最新版本是Prism 4,于2010年11月12日发布。Prism有很完整的文档以及丰富的示例程序。在这里我们仅针对于Silverlight程序的开发。

在下载Prism安装包并安装完成后,会在目标文件夹中发现很多文件。

推荐首先运行RegisterPrismBinaries.bat文件,这样在开发基于Prism的程序时可以更方便地添加引用程序集。

使用Prism之前,需要了解一些概念,下面通过一个非常简单的小程序来了解一下Prism。

1.打开Visual Studio 2010,新建一个Silverlight Application项目,并添加对Prism的引用。再创建三个Silverlight类库工程。

2.在Contract工程下新建一个接口,叫做ITextProvider。

1
2
3
4
5

public interface ITextProvider
{
string GetText();
}

3.在其它的三个项目中都引用Contract项目。

4.在PrismStarter工程下新建一个TextProvider类并实现ITextProvider接口。

1
2
3
4
5
6
7
8
9
10
11
public class TextProvider : ITextProvider
{
private int i = 0;

public string GetText()
{
i++;
return string.Format("From TextProvider [{0}]", i);
}
}

5.删除PrismStarter项目中自动生成的MainPage.xaml,创建一个新的UserControl,叫做Shell。页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<UserControl x:Class="PrismStarter.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://www.codeplex.com/prism"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">

<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="100" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>

<TextBlock FontSize="30" VerticalAlignment="Center" HorizontalAlignment="Center" Text="Prism Starter" />

<ContentControl Grid.Row="1" HorizontalContentAlignment="Stretch" prism:RegionManager.RegionName="RegionA" />

<ContentControl Grid.Row="2" HorizontalContentAlignment="Stretch" prism:RegionManager.RegionName="RegionB" />
</Grid>
</UserControl>

6.在ModuleA工程中添加对Prism程序集的引用。并添加一个UserControl叫做ViewA,页面代码为:

1
2
3
<Grid :Name="LayoutRoot" Background="White">
<TextBlock x:Name="textModuleA" FontSize="30" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>

CodeBehind中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
public partial class ViewA : UserControl
{
public ViewA(ITextProvider textProvider)
{
InitializeComponent();

this.Loaded += (s, e) =>
{
textModuleA.Text = string.Format("Module A {0}", textProvider.GetText());
};
}
}

7.在ModuleA工程中添加一个类叫做ModuleA,并实现接口IModule。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ModuleA : IModule
{
private IRegionManager _regionManager;

public ModuleA(IRegionManager regionManager)
{
_regionManager = regionManager;
}

public void Initialize()
{
_regionManager.RegisterViewWithRegion("RegionA", typeof(ViewA));
}
}

注意这里的RegionA对应于Shell页面中的RegionName。

8.在ModuleB工程中重复6、7过程,只是将A替换为B。

9.在PrismStarter工程中添加对ModuleA和ModuleB的引用。

10.在PrismStarter工程中添加一个PrismStarterBootstrapper类,并继承UnityBootstrapper。

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
38
39
public class PrismStarterBootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return this.Container.TryResolve<Shell>();
}

protected override void InitializeShell()
{ // 控制页面在初始化时显示Shell页面
App.Current.RootVisual = (UIElement)this.Shell;
}

protected override void ConfigureModuleCatalog()
{ // 注册Module。在实际开发中可以使用xaml做配置文件,
// 这样就可以将PrismStarter与ModuleA和ModuleB完全解耦,也就不再需要引用这两个项目
Type moduleAType = typeof(ModuleA.ModuleA);
ModuleInfo moduleA = new ModuleInfo
{
ModuleName = moduleAType.Name,
ModuleType = moduleAType.AssemblyQualifiedName,
};

Type moduleBType = typeof(ModuleB.ModuleB);
ModuleInfo moduleB = new ModuleInfo
{
ModuleName = moduleBType.Name,
ModuleType = moduleBType.AssemblyQualifiedName,
};

this.ModuleCatalog.AddModule(moduleA);
this.ModuleCatalog.AddModule(moduleB);
}

protected override void ConfigureContainer()
{ // 注册一下TextProvider,这样在通过容器请求ITextProvider时会返回TextProvider实例
base.ConfigureContainer();
this.Container.RegisterInstance<ITextProvider>(new TextProvider());
}
}

11.最后一步,打开App.xaml.cs,修改Application_Startup方法

1
2
3
4
5
private void Application_Startup(object sender, StartupEventArgs e)
{
PrismStarterBootstrapper bootstrapper = new PrismStarterBootstrapper();
bootstrapper.Run();
}

运行程序,结果如下:

下面简单介绍一下这个小例子中涉及到的一些概念。

Bootstrapper: 在程序中使用框架需要找到一个切入点,将框架植入进去,将一部分功能委托给框架来实现。在Silverlight中使用Prism的切入点就是App.xaml.cs中的Application_Startup方法。一般来说,这个方法中只是指定页面最先加载的页面,但是我们把默认的逻辑去掉,取而代之的是Bootstrapper(在本例中就是PrismStarterBootstrapper)。当调用Bootstrapper.Run方法时,它会完成一些准备工作,如一些配置等。因此你会发现,使用Prism后,启动程序时会比正常启动要慢一些,就是因为Bootstrapper做了许多工作。

Container: 依赖注入容器。在程序中使用依赖注入的好处到处都可以找的到。在Silverlight中使用容器来管理各个组件的一个很明显的好处就是使用单例来降低内存使用。否则每次加载一个页面都需要重新创建一个也很耗费资源的。当然好处不只这些,通过容器来注入一些服务(如本例中的IRegionManager和ITextProvider)显得相当方便。

Module: Prism帮助我们把程序分解成一个个功能模块,这些功能模块就叫做Module,通常一个工程就是一个Module。由于Module彼此是独立的,但是在运行时需要将它们整合到一起,因此Prism需要知道Module的存在,这里就涉及到了ModuleCatalog, ModuleCatalog就是Module的容器,里面包含了所有Module的信息,以ModuleInfo的形式存在。ModuleInfo就是对Module的抽象,包含Module的名字,类型,依赖等一些信息。

Shell: 相当于程序的入口,初始界面,还能够提供类似ASP.Net中的母版页的功能。Shell必须由Bootstrapper创建,因为Shell需要使用的一些service,比如RegionManager等,需要在Shell显示前注册。

Region: 相当于ASP.Net中的ContentPlaceHolder(是这么叫的吧?),起到占位符的作用,如本例中Shell中有两个Region——RegionA和RegionB,定义了两块区域。在Module的初始化过程中,通过IRegionManager将Module中的页面放进了定义好的Region中。IRegionManager负责管理Region,可以通过它向Region中注册View,进行导航等。

Prism的功能当然远不止这么简单,它还提供对MVVM模式的支持,对导航的支持等,在后续文章中会逐步介绍。希望能够通过本文让大家对Prism有一定的了解。

代码下载