Chemmy's Blog

chengming0916@outlook.com

石榴刺猬 2004-10-04 18:55:00

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

    最近开始学习.Net,遇到了一个比较不错的开源的IDE SharpDevelop。这个开发工具是使用C#开发的,比较吸引我的一点就是它是采用了和Eclipse类似的插件技术来实现整个系统的。而这个插件系统是我最感兴趣的地方,因此开始了一段代码的研究。在本篇之后,我会陆续把我研究的心得写下来。由于是在网吧上网,有诸多不便,因此可能会拖比较长的时间。

一、基本概念

    首先,我们先来对 SharpDevelop 有一个比较感性的认识。你可以从这里下载到它的可执行程序和代码包    http://www.icsharpcode.com/  ,安装的废话就不说了,先运行一下看看。感觉跟VS很像吧?不过目前的版本是1.0.0.1550,还有很多地方需要完善。关于代码和系统结构,SharpDevelop的三个作者写了一本书,各位看官可以参考一下,不过我看过之后还是有很多地方不太理解。

    然后,让我来解释一下什么叫插件以及为什么要使用插件系统。我们以往的系统,开发人员编译发布之后,系统就不允许进行更改和扩充了,如果要进行某个功能的扩充,则必须要修改代码重新编译发布。这就给我们带来了比较大的不方便。解决的方法有很多,例如提供配置等等方法。在解决方案之中,插件是一个比较好的解决方法。大家一定知道PhotoShop、WinAmp吧,他们都有“插件”的概念,允许其他开发人员根据系统预定的接口编写扩展功能(例如PhotoShop中各种各样的滤镜)。所谓的插件就是系统的扩展功能模块,这个模块是以一个独立文件的形式出现的,与系统是相对独立。在系统设计期间并不知道插件的具体功能,仅仅是在系统中为插件留下预定的接口,系统启动的时候根据插件的配置寻找插件,根据预定的接口把插件挂接到系统中。

    这样的方式带来什么样的优点呢?首先是系统的扩展性大大的增强了,如果我们在系统发布后需要对系统进行扩充,不必重新编译,只需要修改插件就可以了。其次有利与团队开发,各个功能模块由于是以插件的形式表现在系统中,系统的每日构造就很简单了,不会因为某个模块的错误而导致整个系统的BUILD失败。失败的仅仅是一个插件而已。

    PhotoShop和Winamp的插件系统是比较简单的,他们首先实现了一个基本的系统,然后在这个系统的基础上挂接其他扩展的功能插件。而SharpDevelop的插件系统更加强大,它的整个系统的基础就仅仅是一个插件管理系统,而你看到的所有的界面、功能统统都是以插件的形式挂入的。在这样的一个插件系统下,我们可以不修改基本系统,仅仅使用插件就构造出各种各样不同的系统。

    现在让我们来看看它的插件系统。进入到SharpDevelop的安装目录中,在Bin目录下的SharpDevelop.exe 和 SharpDevelop.Core.dll是这个系统的基本的插件系统。在Addins目录下有两个后缀是addin的文件,其中一个 SharpDevelopCore.addin 就是它的核心插件的定义(配置)文件,里面定义的各个功能模块存在于Bin/Sharpdevelop.Base.dll 文件中,另外还有很多其他的插件定义在Addins目录下的addin文件中。

    分析SharpDevelop的代码,首先要弄清楚几个基本的概念,这些概念和我以前的预想有一些区别,我深入了代码之后才发现我的困惑所在。

1、AddInTree  插件树
    SharpDevelop 中的插件被组织成一棵插件树结构,树的结构是通过 Extension(扩展点)中定义的Path(路径)来定义的,类似一个文件系统的目录结构。系统中的每一个插件都在配置文件中指定了 Extension,通过Extension中指定的 Path 挂到这棵插件树上。在系统中可以通过 AddTreeSingleton对象来访问各个插件,以实现插件之间的互动。

2、 AddIn 插件
    在 SharpDevelop 的概念中,插件是包含多个功能模块的集合(而不是我过去认为的一个功能模块)。在文件的表现形式上是一个addin配置文件,在系统中对应 AddIn 类。

3、Extension 扩展点
    SharpDevelop中的每一个插件都会被挂到 AddInTree(插件树) 中,而具体挂接到这个插件树的哪个位置,则是由插件的 Extension 对象中的 Path 指定的。在addin 配置文件中,对应于 。例如下面这个功能模块的配置

指定了扩展点路径为 /SharpDevelop/Workbench/Ambiences ,也就是在插件树中的位置。

4、Codon
    这个是一个比较不好理解的东西,在 SharpDevelop 的三个作者写的书的中译版中被翻译为密码子,真是个糟糕的翻译,可以跟Handle(句柄)有一拼了。词典中还有一个翻译叫“基码”,我觉得这个也不算好,不过还稍微有那么一点意思。(这里我原来误写为“代码子”,在评论中有位仁兄说这个翻译不错,现在我觉得也好像确实不错 ^o^)
    根据我对代码的理解,Codon 的功能是描述(包装)一个功能模块(一个功能模块对应一个实现了具体功能的 Command 类)。为了方便访问各个插件中的功能模块, Codon 给各种功能定义了基本的属性,分别是 ID (功能模块的标识),Name (功能模块的类型。别误会,这个Name 是addin文件定义中Codon的XML结点的名称,ID才是真正的名称),其中Name可能是Class(类)、MenuItem(菜单项)、Pad(面板)等等。根据具体的功能模块,可以继承Codon定义其他的一些属性,SharpDevelop中就定义了 ClassCodon、MenuItemCodon、PadCodon等等,你可以根据需要自己定义其他类型的Codon。在addin定义文件中,Codon对应于 标签下的内容。例如下面这个定义

<Extension …> 内部定义了一个Codon,<**Class** …>  表示该Codon是一个 Class(类),接着定义了该Codon的 ID和具体实现该Codon的类名ICSharpCode.SharpDevelop.Services.NetAmbience。运行期间将通过反射来找到对应的类并创建出来,这一点也是我们无法在以前的语言中实现的。

再例如这一个定义

这个扩展点中定义了三个菜单项,以及各个菜单项的名字、标签和实现的类名。这里的Codon就对应于系统中的MenuCodon对象。

5、Command 命令
    正如前文所述,Codon描述了一个功能模块,而每个功能模块都是一个 ICommand 的实现。最基本的 Command 是  AbstractCommand,根据Codon的不同对应了不同的 Command。例如 MenuItemCodon 对应 MenuItemCommand 等等。

6、Service 服务
    插件系统中,有一些功能是整个系统都要使用的,例如文件访问、资源、消息等等。这些功能都作为插件系统的一个基本功能为整个系统提供服务,我们就叫“服务”好了。为了便于访问,这些服务都统一通过 ServiceManager 来管理。其实服务也是一种类型的插件,它们的扩展点路径在目录树中的 /Workspace/Services 中。

    理解了这几个基本的概念之后,就可以看看 SharpDevelop 的代码了。从 src/main/startup.cs 看起吧,之后是addin.cs、addinTree.cs 等等。

   写了两个小时了,休息一下。且听下回分解。

在开发Silverlight程序的时候,经常需要在不同的组件间进行通信。比如点击一个button,可能就需要改变另一个控件的内容。比较直接的办法是使用事件,当然使用MVVM的时候也可以使用command,还可以定义一些全局的变量来保存一些信息等。

Prism提供了几种用于组件间通信的途径,可以使用RegionContext使不同的视图共享数据,也可以借助于容器的力量来使用共享的service来进行通信,或者使用command等。除此之外,Prism还提供了一种基于事件的多播发布/订阅方式的通信机制,使不同的组件之间能够以一种松散耦合的方式来进行通信。这就是本文要介绍的事件聚合(Event Aggregation)。

事件聚合的过程有点像收听广播,首先要有个固定的频率,然后内容就会在这个频率上广播出去,至于有没有人收听,广播电台是不知道的,它只是把内容播送了出去。而其他的人想听广播也不用跑到广播电台,只要知道频率,收听这个频率就可以了。联系广播电台和听众的就是这个频率。

在事件聚合的过程中,事件发布方(publisher)相当于广播电台,事件接收方(Subscriber)相当于听众,而事件自然就相当于频率了。

使用Event Aggregation很简单,只需要知道一个接口和一个类基本上就足够了。接口是IEventAggregator,类是CompositePresentationEvent。

要想发布或订阅事件,自然得先要有事件,所以第一件工作就是要定义事件。Prism提供了一个事件基类CompositePresentationEvent,自定义的事件只需要继承这个类就可以了,泛型代表的是事件发生过程中需要传递的参数类型。如:

1
2
3
public class ReceiveNewEmailEvent : CompositePresentationEvent<MailViewModel>
{
}

上面定义了一个事件,用于在收到新邮件时使用,传递的参数是一个邮件的ViewModel。

使用的时候也很简单,使用IEventAggregator接口中的GetEvent方法来获取事件,然后要么发布出去,要么订阅一下就可以了。

下面是当收到一封新的邮件的时候,发布事件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EmailReceiver
{
private IEventAggregator _eventAggregator;
public EmailReceiver(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}

public void ReceiveEmail()
{
if (_email != null)
{ // 当接收到新邮件时,就发布事件,所有订阅了该事件的组件都会接到通知
_eventAggregator.GetEvent<ReceiveNewEmailEvent>()
.Publish(_email);
}
}
}

可以看到我们直接在构造函数中传递了IEventAggregator类型的参数,如果使用Prism来搭建Silverlight程序的话,那么在默认的Bootstrapper中会在容器中添加IEventAggregator的实例,所以并不需要我们做其它更多的工作。如果对Prism或Bootstrapper不太了解的话,可以参考这两篇文章(Prism简介Bootstrapper)。

下面是订阅ReceiveNewEmail事件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MailBox
{
public MailBox(IEventAggregator eventAggregator)
{
eventAggregator.GetEvent<ReceiveNewEmailEvent>()
.Subscribe(OnReceivedNewEmail);
}

// 该方法必须为public
public void OnReceivedNewEmail(MailViewModel mail)
{
// do something
}
}

这样,发布出去的事件马上就可以被接收到,而且两个组件只是依赖于事件,彼此之间是松散耦合的。

事件可以订阅,也可以退订,甚至可以有选择地接受某些特定的事件。下面以一个模拟的简单的邮箱客户端来演示一下Event Agregation的使用场景。

image

如图所示,左边是邮件列表,会有一个定时器每隔两秒钟接收到一封邮件,这时邮箱客户端会更新邮件列表,点击左边的列表,会在右边显示邮件的内容。如果点击’将该发信人加入黑名单’,则不会再接受来自该发件人的邮件,如果点击断开连接,则停止接受邮件,再次点击会继续接收邮件。需求大致就是这样了。

首先在启动程序的时候开启一个定时器,每隔两秒钟会接收一封邮件,并发布事件通知有新邮件:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class EmailReceiver
{
public void Run()
{
var timer = new DispatcherTimer();
timer.Tick += (s, e) => EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Publish(EmailRepository.GetMail());
timer.Interval = new TimeSpan(0, 0, 0, 2);
timer.Start();
}

}

MailList组件会订阅这个事件,并对邮件列表进行更新:

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
public partial class MailList : UserControl
{
private readonly ObservableCollection<MailViewModel> _mails =
new ObservableCollection<MailViewModel>();

// 黑名单列表
private readonly List<string> _refusedSenders = new List<string>();

public MailList()
{
InitializeComponent();

MailItems.ItemsSource = _mails;

SubscribeReceiveEmailEvent();
}

private void SubscribeReceiveEmailEvent()
{ // 订阅事件的Subscribe方法提供了几个重载方法,除了最简单的直接订阅之外,
// 还可以指定线程类型(比如如果直接使用System.Threading.Timer的话,
// 就必须使用ThreadOption.UIThread,否则会报错),以及是否持有订阅者的引用,
// 或者指定一个filter来对事件进行过滤
// 本例中使用的filter是拒绝接受黑名单中包含的发件人发过来的邮件
EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Subscribe(OnReceiveNewEmail, ThreadOption.UIThread,
true, (mail) => !_refusedSenders.Contains(mail.From));
}

public void OnReceiveNewEmail(MailViewModel mail)
{
_mails.Insert(0, mail);
}
}

当点击左边的邮件列表的时候,会在右边的MailContent组件中显示该邮件的信息,这个过程也是通过Event Aggregation来完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//  NotificationObject是Prism提供的对MVVM的支持的ViewModel的基类
// 可以简化INotifyPropertyChanged接口的实现方式
public class MailViewModel : NotificationObject
{
public MailViewModel()
{ // DelegateCommand也是Prism提供的一种Command类型
ViewMailCommand = new DelegateCommand(OnViewMail);
}

public ICommand ViewMailCommand { get; private set; }

public void OnViewMail()
{
this.HasRead = true;
EventAggregatorRepository.EventAggregator
.GetEvent<ViewEmailEvent>()
.Publish(this);
}
}

当点击时,会进入相应的Command逻辑,而MailContent则订阅了ViewEmailEvent,并将传递过来的MailViewModel显示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public partial class MailContent : UserControl
{
public MailContent()
{
InitializeComponent();

EventAggregatorRepository.EventAggregator
.GetEvent<ViewEmailEvent>()
.Subscribe(OnViewEmail);
}

public void OnViewEmail(MailViewModel mail)
{
this.DataContext = mail;
}
}

当点击将该发信人加入黑名单按钮时,会发布AddRefuseSenderEvent,而接收到这一事件的MailList组件则会更新黑名单,这样filter就会过滤掉黑名单中已经存在的发件人的邮件:

1
2
3
4
5
6
7
public void OnRefusedSendersAdded(string sender)
{
if (!_refusedSenders.Contains(sender))
{
_refusedSenders.Add(sender);
}
}

如果点击了断开连接或重新连接的话,会发布一个ConnectOrDisconnectMailServerEvent事件。Prism的事件基类并不支持不带参数的事件,也就是说没有办法创建一个不需要传参的事件。所以这里我们使用了object类型作为参数类型,在传递参数的时候直接传了个null过去。

1
2
3
EventAggregatorRepository.EventAggregator
.GetEvent<ConnectOrDisconnectMailServerEvent>()
.Publish(null);

而当MailList接收到该事件的时候,首先判断一下是否已经订阅了ReceiveNewEmailEvent事件,如果订阅了就退订,如果没有订阅就重新订阅。这样来达到开启或关闭接收邮件的目的:

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
50
51
52
53
54
public partial class MailList : UserControl
{
private readonly ObservableCollection<MailViewModel> _mails =
new ObservableCollection<MailViewModel>();

private readonly List<string> _refusedSenders = new List<string>();

public MailList()
{
InitializeComponent();

SubscribeReceiveEmailEvent();

EventAggregatorRepository.EventAggregator
.GetEvent<ConnectOrDisconnectMailServerEvent>()
.Subscribe(OnConnectOrDisconnectMailServer);
}

public void OnConnectOrDisconnectMailServer(object obj)
{
// 判断是否已经订阅了该事件
bool hasSubscribed = EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Contains(OnReceiveNewEmail);
if (hasSubscribed)
{
UnsubscribeReceiveEmailEvent();
}
else
{
SubscribeReceiveEmailEvent();
}
}

private void SubscribeReceiveEmailEvent()
{
EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Subscribe(OnReceiveNewEmail, ThreadOption.UIThread,
true, (mail) => !_refusedSenders.Contains(mail.From));
}

private void UnsubscribeReceiveEmailEvent()
{ // 退订事件
EventAggregatorRepository.EventAggregator
.GetEvent<ReceiveNewEmailEvent>()
.Unsubscribe(OnReceiveNewEmail);
}

public void OnReceiveNewEmail(MailViewModel mail)
{
_mails.Insert(0, mail);
}
}

由于EventAggregation并不需要建立在Prism装配的程序上,为了操作简便,所以并没有使用Prism来管理这个程序,当然也就没有使用容器。所以我用了一个static的全局变量来保存了一个IEventAggregator的实例。

本文为了演示,所以大量地使用了Event Aggregation,希望大家在工作中要仔细斟酌使用,虽然用起来很灵活,但是如果事件太多的话,也会让人有无从下手的感觉,增加维护的难度。

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

0%