Chemmy's Blog

chengming0916@outlook.com

石榴刺猬 2004-10-07 20:10:00

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

二、主程序

    在大学课程里面,我对于模拟电路总是搞不清楚,直到现在也是这样。我总觉得电路图很奇怪,总会问“这部分电路是做什么用的”、“为什么会有这样的效果”。在我的脑海里面,每部分的电路都应该有一定的用处,可是我总是看不明白。我妈妈说,我的思路被软件所固化的太久了,看电路图不应该总是一个个模块的看,正确的方法应该是从电源的一极顺着电路看,一直看到电源的另一极。我现在仍然不懂看电路图,可是以我看代码的经验来说,我觉得分析源代码按照这样的思路来看会比较容易把脉络理清楚。
     在SharpDevelop的代码中,由于很多的接口和插件的原因,很多代码在看到某个地方会突然失去函数/方法调用的线索。例如看某个函数的实现的时候会跳到一个接口里面去,那是因为这部分功能在运行期才会给一个实现了这个接口的对象来进行具体的执行。从这个角度来说,设计模式也给我们研究代码稍微带来了一点小小的难度。在看Linux下源代码的时候也经常遇到这种问题,在这个时候寻找代码线索比较好的方法是用一个文本搜索工具来搜索相关的关键字。在Linux下我经常会用grep,Windows下面类似UltraEdit的“批量文件查找”功能会很好用(或者“Search And Replace”之类的工具)。这个是我读代码的一点小小的经验,如果你知道有更好的方法,请告诉我让我也学习一下 ? 。
     我不想大段大段的贴代码出来占地方(空间、带宽,还有各位看官的注意力),在需要的地方我会贴上主要的代码,因此最好能够找代码来对应着看。把代码包解压缩,我把它解到了“F:/SharpDevelop”(如果没有说明,下文都是以此为代码的根目录了)。由于SharpDevelop本身对于察看代码不是很方便,没有“转到定义”之类的功能,因此我建议你把它的代码转成VS的工程来看。不过很可惜,SharpDevelop的工程导出功能现在有问题,如果导出/src/SharpDevelop.cmbx 这个总的复合工程的话会失败(我记得RC1版本是可以成功的,不知道为什么后来的版本反而会出问题),所以只能一个一个工程的导出。
     好了,让我们来看SharpDevelop的代码吧。
1、起点
    在主程序的起点在/src/Main/StartUp/SharpDevelopMain.cs,找到Main函数这就是整个程序的起点了。开始的部分是显示封面窗体并加上命令行控制,其中SplashScreenForm 定义在/src/Main/Base/Gui/Dialogs/SplashScreen.cs文件中,这部分我就不多说了。之后是

Application.ThreadException  += new  ThreadExceptionEventHandler(ShowErrorBox);

    SharpDevelop为了有效的进行错误报告,因此自己进行了异常的控制。系统出现异常的时候,SharpDevelop会拦截下来弹出它自己的异常提示报告对话框。这个代码就是在这一行实现的。其中 ShowErrorBox 这个方法就在类SharpDevelopMain中,ExceptionBox 定义在/src/Main/StartUp/Dialogs/ExceptionBox.cs中。如果需要进行自己的异常控制,可以学习一下这里的技巧。

2、充满玄机的初始化

string  [] addInDirs  =  ICSharpCode.SharpDevelop.AddInSettingsHandler.GetAddInDirectories(  out  ignoreDefaultPath );
AddInTreeSingleton.SetAddInDirectories(addInDirs, ignoreDefaultPath);

    通过AddInSettingsHandler取得插件的目录,并告知AddInTreeSingleton。AddInSettingsHandler定义在/src/Main/StartUp/Dialogs/AddInTreeSettingsHandler.cs中,它通过读取系统配置(App.config)文件中的AddInDirectory节点的Path属性来确定插件的目录位置,或者你也可以通过自己定义的AddInDirectories节来指定插件目录。如果你没有做这些配置,默认的目录在SharpDevelop运行目录的../Addins目录下。

    通过ServiceManager(服务管理器)加入三个系统默认的服务,消息服务、资源服务、图标服务。这三个服务中,消息服务是显示各种信息提示,另外两个是属于系统的资源,SharpDevelop通过服务来进行统一调用和管理。
ServiceManager.Services.InitializeServicesSubsystem(“/Workspace/Services”);

    初始化其他的服务。SharpDevelop把服务定义在插件树的/Workspace/Services这个路径中,凡是在这个路径下的插件都被认为是服务,因此如果你自己定义了一个服务的话,也需要挂到这个路径下(这里就是系统服务的扩展点了)。

    注意!这一步中,在我们的眼皮子底下悄悄的进行了一个重要的初始化工作。各位看官请看,ServiceManager 定义在/src/Main/Core/Services/ ServiceManager.cs文件中,察看它的InitializeServicesSubsystem方法,我们发现这样一行

AddServices((IService[]) AddInTreeSingleton.AddInTree.GetTreeNode(servicesPath).BuildChildItems(this).ToArray(typeof(IService)));

    在这里,AddInTreeSingleton首次调用了AddInTree(插件树)的实例。按照Singleton模式,只有在首次调用的时候才会初始化实例,这里也是同样如此。整个系统的AddInTree是在这一步中进行了初始化工作,稍候我们将详细介绍AddInTree如何进行初始化工作,先顺便看看服务的初始化。在ServiceManager的InitializeServicesSubsystem方法中,通过AddInTree检索服务插件路径下的所有配置,并通过它来读取、建立具体的对象,然后加入到服务列表中。之后通过一个循环,逐个的调用各个服务的InitializeService方法初始化服务。

    AddInTree的初始化工作容我们稍候再看,先把主体的代码看完。

commands = AddInTreeSingleton.AddInTree.GetTreeNode(“/Workspace/Autostart”).BuildChildItems(null);
for (int i = 0; i < commands.Count - 1; ++i)
{
 ((ICommand)commands[i]).Run();
}

    /Workspace/Autostart是系统自动运行命令的扩展点路径,定义在这个路径下的插件会在系统启动的时候自动运行。在这里,通过插件树初始化建立处于这个路径下的Command(命令),并逐一执行。BuildChildItems方法的功能是建立这个扩展点下的Command列表,我会在介绍AddTree的时候具体说明它的实现。

     主程序代码的最后,初始化完毕、关闭封面窗体,然后执行命令列表中最后一个命令(也就是系统的主界面)。在主界面退出的时候,系统卸载所有的服务。

    在这部分代码中,我们知道了两个系统指定的扩展点路径 /Workspace/Services 和 /Workspace/Autostart ,我们实现服务和指定系统自动运行命令的时候就可以挂到这两个扩展点路径下了。
     托反射的福,ServiceManager.Services可以通过类型(接口)来查找具体的实例,也就是GetServices方法。但是ServiceManager的具体实现我们可以容后再看,这里已经不是最紧要的部分了。
     接下来,我们来看看整个插件系统的核心-AddinTree的代码,看看它是如何通过插件配置进行初始化并建立起整个系统的插件树骨干。

石榴刺猬 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

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改为相应的字母。运行程序,结果如下:

image

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

image

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

imageimage

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

image

运行程序,报错。很简单,因为我们在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));
}

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

image

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

不过再打开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();
}

运行程序,结果如下:

image

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

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有一定的了解。

代码下载

.NET 中定时器的使用方法

在.NET 开发中,定时器是实现周期性任务的核心组件。根据是否依赖 UI 线程,可将其分为 “与 UI 无关联” 和 “与 UI 相关” 两大类,不同类型的定时器适用场景、线程模型及使用方式差异显著。本文将详细讲解各类定时器的特点、核心属性、正确用法及注意事项。

一、与 UI 无关联的定时器

此类定时器运行于非 UI 线程(默认使用线程池),适用于后台计算、数据同步等无需操作 UI 的场景,时间精度相对较高。

1. System.Timers.Timer

核心特点

  • 支持线程同步配置,可通过SynchronizingObject指定执行线程;

  • 事件驱动模式(通过Elapsed事件触发任务);

  • 适用于对执行精度有一定要求的后台任务。

关键属性修正

  • AutoReset:原始描述颠倒,正确逻辑为:

    • true(默认):定时器触发后自动重置,持续周期性执行;

    • false:定时器仅触发一次,执行后自动停止。

  • SynchronizingObject:默认值为null,任务运行于线程池(CPU 自动分配线程);若指定为 UI 控件(如 WinForms 的 Form),则任务会切换到 UI 线程执行。

  • Interval:任务执行间隔(单位:毫秒),最小值为 1。

  • Enabled:控制定时器是否启用(true启用,false禁用)。

正确代码示例

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
using System;

using System.Threading;

public class TimersTimerExample

{

// 声明定时器与线程同步锁(确保多线程安全)

private readonly System.Timers.Timer \_timer = new System.Timers.Timer();

private readonly object \_syncRoot = new object();

public TimersTimerExample()

{

// 绑定Elapsed事件(任务执行逻辑)

\_timer.Elapsed += OnTimerTick;

// 设置执行间隔为100毫秒

\_timer.Interval = 100;

// 配置为持续执行(AutoReset=true)

\_timer.AutoReset = true;

// 初始禁用,需手动启动

\_timer.Enabled = false;

}

// 启动定时器

public void StartTimer()

{

\_timer.Enabled = true;

// 或使用\_timer.Start()(与Enabled=true等效)

}

// 停止定时器

public void StopTimer()

{

\_timer.Enabled = false;

// 或使用\_timer.Stop()

}

// Elapsed事件回调(默认运行于线程池)

private void OnTimerTick(object sender, System.Timers.ElapsedEventArgs e)

{

// 加锁确保多线程下任务逻辑安全(避免并发问题)

lock (\_syncRoot)

{

Console.WriteLine(\$"后台任务执行:{DateTime.Now:HH:mm:ss.fff}");

// TODO:添加实际业务逻辑(如数据同步、日志记录)

}

}

}

注意事项

  • 若任务逻辑涉及共享资源,需通过lock等方式保证线程安全;

  • 若需在 UI 线程更新内容,需将SynchronizingObject指定为 UI 控件(如_timer.SynchronizingObject = this,需在 WinForms 环境中)。

2. System.Threading.Timer

核心特点

  • 轻量级定时器,完全基于线程池实现;

  • 无事件模型,通过回调函数(TimerCallback)执行任务;

  • 不支持直接指定同步对象,需手动处理线程切换。

关键参数

构造函数System.Threading.Timer(TimerCallback callback, object state, int dueTime, int period)参数说明:

  • callback:定时器触发时执行的回调函数;

  • state:传递给回调函数的参数(无需参数时设为null);

  • dueTime:定时器启动延迟时间(单位:毫秒),0表示立即启动;

  • period:任务执行间隔(单位:毫秒),Timeout.Infinite表示仅执行一次。

正确代码示例

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
55
56
57
using System;

using System.Threading;

public class ThreadingTimerExample

{

// 声明线程定时器(需注意释放资源)

private System.Threading.Timer \_timer;

public ThreadingTimerExample()

{

// 初始化定时器:立即启动(dueTime=0),间隔1000毫秒执行一次

\_timer = new System.Threading.Timer(

callback: TimerCallback,

state: null,

dueTime: 0,

period: 1000

);

}

// 定时器回调函数(运行于线程池)

private void TimerCallback(object state)

{

Console.WriteLine(\$"轻量级后台任务:{DateTime.Now:HH:mm:ss}");

// TODO:添加轻量级业务逻辑(如心跳检测、缓存清理)

}

// 释放定时器资源(避免内存泄漏)

public void DisposeTimer()

{

\_timer?.Change(Timeout.Infinite, Timeout.Infinite); // 先停止定时器

\_timer?.Dispose(); // 释放资源

}

}

注意事项

  • 若任务执行时间超过period,线程池会分配新线程执行下一次任务,需自行控制并发;

  • 不再使用时必须调用Dispose释放资源,避免线程泄漏;

  • 无法直接更新 UI,需通过Dispatcher(WPF)或Invoke(WinForms)切换到 UI 线程。

二、与 UI 相关的定时器

此类定时器绑定 UI 线程,适用于 WinForms、WPF 等桌面应用的 UI 更新场景,无需手动处理线程安全,但时间精度较低(受 UI 线程繁忙程度影响)。

1. System.Windows.Forms.Timer(WinForms 专用)

核心特点

  • 仅适用于 WinForms 应用,支持可视化拖拽(从工具箱拖到 Form 上);

  • 任务运行于 UI 线程,可直接更新 UI 控件(如 Label、TextBox);

  • 精度较低(约 10-55 毫秒),不适用于高时效任务。

关键属性

  • Interval:执行间隔(单位:毫秒),最小值为 1;

  • Enabled:控制定时器启用 / 禁用(true启用,false禁用);

  • Tick:定时器触发时执行的事件(运行于 UI 线程)。

正确代码示例

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
using System;

using System.Windows.Forms;

public partial class TimerForm : Form

{

// 声明WinForms定时器(可通过设计器拖拽生成)

private readonly System.Windows.Forms.Timer \_uiTimer;

public TimerForm()

{

InitializeComponent();

// 初始化定时器(代码方式,非拖拽)

\_uiTimer = new System.Windows.Forms.Timer();

\_uiTimer.Interval = 1000; // 1秒更新一次UI

\_uiTimer.Tick += UITimer\_Tick;

\_uiTimer.Enabled = true; // 启用定时器

}

// Tick事件(运行于UI线程,可直接操作控件)

private void UITimer\_Tick(object sender, EventArgs e)

{

// 直接更新Label文本(无需线程同步)

lblTime.Text = \$"当前时间:{DateTime.Now:HH:mm:ss}";

}

// 关闭窗体时释放定时器

private void TimerForm\_FormClosing(object sender, FormClosingEventArgs e)

{

\_uiTimer?.Dispose();

}

}

注意事项

  • 仅能在 WinForms 项目中使用,无法跨框架(如 WPF);

  • Tick事件中执行耗时操作(如循环计算),会导致 UI 卡顿;

  • 无需手动处理线程安全,因事件始终在 UI 线程执行。

2. System.Windows.DispatcherTimer(WPF 专用)

核心特点

  • 仅适用于 WPF 应用,基于Dispatcher(WPF 线程调度器)实现;

  • 任务运行于 UI 线程,支持直接更新 WPF 控件(如 TextBlock);

  • 可通过DispatcherPriority调整任务优先级(默认Normal)。

关键属性与方法

  • Interval:执行间隔(类型:TimeSpan,支持秒、毫秒等单位);

  • Tick:定时器触发事件(运行于 UI 线程);

  • **Start()/Stop()**:启动 / 停止定时器(替代Enabled属性);

  • DispatcherPriority:任务在 UI 线程中的执行优先级(如Background表示后台优先级,不阻塞 UI)。

正确代码示例

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
55
56
57
58
59
60
61
using System;

using System.Windows;

using System.Windows.Threading;

public partial class MainWindow : Window

{

// 声明WPF调度定时器

private readonly DispatcherTimer \_dispatcherTimer;

public MainWindow()

{

InitializeComponent();

// 初始化定时器

\_dispatcherTimer = new DispatcherTimer();

// 设置间隔为1秒(使用TimeSpan)

\_dispatcherTimer.Interval = TimeSpan.FromSeconds(1);

// 绑定Tick事件

\_dispatcherTimer.Tick += DispatcherTimer\_Tick;

// 启动定时器

\_dispatcherTimer.Start();

}

// Tick事件(运行于UI线程,可直接更新WPF控件)

private void DispatcherTimer\_Tick(object sender, EventArgs e)

{

// 直接更新TextBlock内容

tbTime.Text = \$"当前时间:{DateTime.Now:HH:mm:ss.fff}";

}

// 关闭窗口时停止定时器

private void MainWindow\_Closing(object sender, System.ComponentModel.CancelEventArgs e)

{

\_dispatcherTimer?.Stop();

}

}

注意事项

  • 仅能在 WPF 项目中使用,依赖System.Windows程序集;

  • 若需降低任务对 UI 的影响,可设置_dispatcherTimer.DispatcherPriority = DispatcherPriority.Background

  • 若任务耗时较长,仍会导致 UI 响应缓慢,需结合Task开启后台线程处理(处理完后通过Dispatcher.Invoke更新 UI)。

三、定时器选择指南

定时器类型 适用框架 线程模型 精度 核心场景
System.Timers.Timer 通用(非 UI) 线程池(可指定同步对象) 较高 后台任务、数据同步
System.Threading.Timer 通用(非 UI) 线程池 轻量级后台任务(如心跳检测)
System.Windows.Forms.Timer WinForms UI 线程 较低 WinForms UI 更新
System.Windows.DispatcherTimer WPF UI 线程 较低 WPF UI 更新

总结:需根据项目框架(WinForms/WPF/ 控制台)、是否操作 UI、精度要求三大因素选择定时器,避免因线程模型不匹配导致 UI 卡顿或数据安全问题。

(注:文档部分内容可能由 AI 生成)

0%