[转]使用 Prism 复合 Web 应用程序
作者:Shawn Wildermuth
本文内容:
- Silverlight 2
- 应用程序复合
- 依赖关系注入
本文使用了以下技术:
Silverlight 2、Prism

您第一次可能是通过小型应用体验到 Silverlight 的:视频播放器、简单的图表应用程序,甚至是一个菜单。这类应用可以简单直接地进行设计,若将它们严格地分为不同的功能层次,显得有些小题大做。
但是,如果在大型应用程序中采用紧密耦合的风格,简单直接的设计就会出现问题。随着移动部件的数量不断增多,简单风格的应用程序开发会力不从心。进行分层(请参见我的文章“Silverlight 2 应用程序中的 Model-View-ViewModel”)可以进行部分弥补,但是在大型 Silverlight 项目中,紧密耦合的体系结构仅仅是需要解决的众多问题之一。
在本文中,我将向您演示如何在 Prism 项目中使用 Composite Application Library 的复合技术来构建应用程序。我开发的示例是一个简单的数据库数据编辑器。
随着要求的变化和项目的成熟,如果更改应用程序部件时不会影响系统其他部分,会对开发很有帮助。通过使应用程序模块化,可以独立地构建应用程序组件(松散耦合),还可以在不影响其他代码的情况下更改应用程序的各个部件。
此外,您可能不希望一次性加载应用程序的所有部件。假设有一个客户管理应用程序,用户可以登录该应用程序,然后管理其潜在客户渠道并检查来自所有潜在客户的电子邮件。如果用户一天内会多次检查电子邮件,但每隔一天或两天才管理一次渠道,为什么不在需要时才加载渠道管理代码呢?应用程序最好能支持按需加载应用程序的各部件,这样,通过使应用程序模块化就可以解决问题。
Microsoft 的模式和实施方案团队创建了一个称为 Prism(即 CompositeWPF)的项目,旨在解决这类 Windows Presentation Foundation (WPF) 应用程序问题,此外,Prism 已进行了更新,也支持 Silverlight。Prism 软件包中同时提供了用于构建应用程序的框架和指南。框架(称为 Component Application Library (CAL))用于:
- 应用程序模块化:通过分离的组件构建应用程序。
- UI 复合:使松散耦合的组件形成用户界面,而无需了解其余分离的应用程序部分。
- 服务位置:将水平服务(如日志记录和身份验证)与垂直服务(业务逻辑)分离开,使应用程序的分层更清晰。
CAL 的编写考虑了这些设计原则,对于应用程序开发人员而言,CAL 是自助式框架,可以根据需要进行取舍。图 1 是与您的应用程序相关的 CAL 基本布局。
.gif)
图 1 Composite Application Library
CAL 支持这些服务,以帮助您用较小的部件复合应用程序。这意味着,CAL 确定要加载哪些部件(以及何时加载),并提供基本功能。您可以决定哪些基本功能有助于完成任务,哪些可能是多余无益的。
在本文中,我的示例使用了尽量多的 CAL 功能。这是一个在运行时使用 CAL 加载几个模块的 Shell 应用程序,它将视图放置在区域中(如图 2 所示),并支持服务。在介绍相应代码之前,您需要了解一些与依赖关系注入(也称为“控制反转”或 IoC)有关的基本概念。CAL 的很多功能都需要依赖关系注入,因此,了解这些基本概念有助于您使用 Prism 开发 Silverlight 项目的体系结构。
.gif)
图 2 复合应用程序体系结构
依赖关系注入简介
在典型的开发中,项目从一个入口点(可执行文件、default.aspx 页面等)开始。您可能将应用程序作为大型项目进行开发,但是,大多数情况下,应用程序会加载项目中包含的大量程序集,因此需要进行某种程度的模块化。主程序集知道需要哪些程序集,并创建对这些程序集的硬引用。在编译时,主项目知道所有引用的程序集,用户界面由静态控件组成。应用程序控制所需的代码,通常知道可能使用的所有代码。但是这就存在问题,因为开发是在主应用程序项目中进行的。随着整体应用程序的增大,生成时间和相互冲突的更改会降低开发速度。
通过提供在运行时建立依赖关系的指令,依赖关系注入可以改变这种情况。这时,不是由项目控制这些依赖关系,而是由一段代码(称为“容器”)负责注入依赖关系。
但是,这为什么重要?一方面,使代码模块化会使代码测试更易进行。因为能够换出项目的依赖关系,所以测试更为清晰,只有待测试代码(而不是依赖关系嵌套链中的代码)才可能是测试失败的原因。以下是具体的示例。假设您有一个组件,供其他开发人员用来查找特定公司的地址。该组件依赖于一个用于检索数据的数据访问组件。在测试该组件时,您从针对数据库测试该组件开始,某些测试失败了。因为数据库的架构和版本经常变化,您不知道是自己的代码还是数据访问代码导致了测试失败。您的组件对数据访问组件具有硬依赖关系,所以应用程序测试变得不可靠,在您的代码或他人的代码中跟踪故障时会难以分辨。
您的组件可能如下所示:
public class AddressComponent { DataAccessComponent data = new DataAccessComponent(); public AddressComponent() { } ... }
您可以接受一个表示数据访问的接口(而不是硬接线的组件),如下所示:
public interface IDataAccess { ... } public class AddressComponent { IDataAccess data; public AddressComponent(IDataAccess da) { data = da; } ... }
通常,使用接口是为了创建可调整代码的版本。通常,将这种方法称为“模拟”。模拟意味着会创建依赖关系的一个实现,但是该实现并不是真实版本的实现。实际上,您创建的是模拟实现。
因为依赖关系 (IDataAccess) 可以在对象的构造过程中注入到项目中,所以这种方法更好。IDataAccess 组件将根据需要(测试或生产)进行实现。
实质上,这就是依赖关系注入的工作原理,但是注入是如何处理的?容器的任务是处理类型的创建,通过容器,可以注册类型和解析所注册的类型。例如,假设您有一个实现 IDataAccess 接口的具体类。在启动应用程序的过程中,您可以指示容器注册该类型。在应用程序中需要该类型的其他任何位置,都可以要求容器解析该类型,如下所示:
public void App_Startup() { container.RegisterType<IDataAccess, DbDataAccess>(); } ... public void GetData() { IDataAccess acc = container.Resolve<IDataAccess>(); }
根据具体情况(测试或生产),只需更改注册即可换出 IDataAccess 的实现。此外,容器还可以处理依赖关系的结构注入。如果某个需要由容器构造函数创建的对象接受容器可以解析的接口,则容器会解析该类型并将其传递给构造函数,如图 3 所示。
图 3 容器的类型解析
public class AddressComponent : IAddressComponent { IDataAccess data; public AddressComponent(IDataAccess da) { data = da; } } ... public void App_Startup() { container.RegisterType<IAddressComponent, AddressComponent>(); container.RegisterType<IDataAccess, DbDataAccess>(); } public void GetAddresses() { // When we ask the container to create the AddressComponent, // it sees that a constructor takes a IDataAccess object // so it automatically resolves that dependency IAddressComponent addr = container.Resolve<IAddressComponent>(); }
请注意,AddressComponent 的构造函数接受 IDataAccess 的实现。当该构造函数在解析过程中创建 AddressComponent 类时,会自动创建 IDataAccess 的实例并将其传递给 AddressComponent。
向容器注册类型时,同时指示容器以特殊方式处理该类型的生存期。例如,如果要处理日志记录组件,则可能希望将该组件视为单一实例,这样,需要日志记录的每个应用程序部件不会各自获得一个副本(这是默认行为)。为此,可以提供 LifetimeManager 抽象类的实现。支持多个生存期管理器。ContainerControlledLifetimeManager 是按进程的单一实例,而 PerThreadLifetimeManager 是按线程的单一实例。对于 ExternallyControlledLifetimeManager,容器具有对该单一实例的弱引用。如果对象在外部发布,则容器会创建一个新实例,否则返回弱引用中包含的活动对象。
在注册类型时指定 LifetimeManager 类,即可使用该类。下面是一个示例:
container.RegisterType<IAddressComponent, AddressComponent>( new ContainerControlledLifetimeManager());
在 CAL 中,IoC 容器基于模式和实施方案组提供的 Unity 框架。我会在后面的示例中使用 Unity 容器,不过 Unity IoC 容器还有一些开放源代码的备选方法,如 Ninject、Spring.NET、Castle 和 StructureMap。如果您熟悉并使用其他 IoC 容器(而不是 Unity),则可以提供自己的容器(尽管这会增加一些工作量)。
启动行为
通常,在 Silverlight 应用程序中,启动行为仅仅是创建主 XAML 页面的类并将其分配给应用程序的 RootVisual 属性。在复合应用程序中,仍然需要进行此项工作,但是复合应用程序通常使用引导类处理启动行为,而不是创建 XAML 页面类。
要开始操作,需要一个从 UnityBootstrapper 类派生的新类。UnityBootstrapper 类在 Microsoft.Practices.Composite.UnityExtensions 程序集中。该引导程序包含一些可重写方法,用于处理启动行为的不同部分。通常,仅重写必要的方法,而不会重写所有启动方法。必须重写的两个方法是 CreateShell 和 GetModuleCatalog。
CreateShell 方法用于创建主 XAML 类。主 XAML 类通常称为 Shell,原因在于,它是应用程序组件的可视化容器。我的示例中有一个引导程序,用于创建 Shell 类的新实例,并在返回此新 Shell 类之前将其分配给 RootVisual,如下所示:
public class Bootstrapper : UnityBootstrapper { protected override DependencyObject CreateShell() { Shell theShell = new Shell(); App.Current.RootVisual = theShell; return theShell; } protected override IModuleCatalog GetModuleCatalog() { ... } }
GetModuleCatalog 方法(在下一节中进行说明)返回要加载的模块的列表。
现在,有了引导程序类,即可将它用在 Silverlight 应用程序的启动方法中。通常的方法是,创建引导程序类的新实例,并调用其 Run 方法,如图 4 所示。
图 4 创建引导程序的实例
public partial class App : Application { public App() { this.Startup = this.Application_Startup; this.Exit = this.Application_Exit; this.UnhandledException = this.Application_UnhandledException; InitializeComponent(); } private void Application_Startup(object sender, StartupEventArgs e) { Bootstrapper boot = new Bootstrapper(); boot.Run(); } ... }
在向容器注册不同应用程序部件所需的类型时,也会用到引导程序。要完成类型注册,需要重写引导程序的 ConfigureContainer 方法。这时,可以注册应用程序的其余部分要使用的任何类型。图 5 是相应代码。
图 5 注册类型
public class Bootstrapper : UnityBootstrapper { protected override void ConfigureContainer() { Container.RegisterType<IShellProvider, Shell>(); base.ConfigureContainer(); } protected override DependencyObject CreateShell() { // Get the provider for the shell IShellProvider shellProvider = Container.Resolve<IShellProvider>(); // Tell the provider to create the shell UIElement theShell = shellProvider.CreateShell(); // Assign the shell to the root visual of our App App.Current.RootVisual = theShell; // Return the Shell return theShell; } protected override IModuleCatalog GetModuleCatalog() { ... } }
此示例中的代码为类注册了一个接口,用于实现 IShellProvider 接口,IShellProvider 接口是在我们的示例中创建的,不是 CAL 框架的一部分。通过这种方式,我们可以在 CreateShell 方法的实现中使用该接口。我们可以解析该接口,然后使用它创建 Shell 的实例,这样,就可以将该实例分配给 RootVisual 并返回该实例。这种方法看似增加了额外的工作,不过,如果深入研究 CAL 如何帮助构建应用程序,就会明白此引导程序是如何提供帮助的。
模块化
在典型的 .NET 环境中,程序集是主工作单元。通过这种方式,开发人员可以相互独立地处理自己的代码。在 CAL 中,这些工作单元中的每一个单元都是一个模块,CAL 需要一个可以与模块的启动行为通信的类,才能使用模块。该类还需要支持 IModule 接口。IModule 接口需要一个名为 Initialize 的方法,模块通过该方法可以对自己进行设置,以供应用程序的其余部分使用。本示例包括一个 ServerLogger 模块,该模块包含我们的应用程序的日志记录功能。ServerLoggingModule 类支持 IModule 接口,如下所示:
public class ServerLoggerModule : IModule { public void Initialize() { ... } }
问题在于,我们不知道要在模块中初始化的内容。既然这是一个 ServerLogging 模块,合乎逻辑的情况是,我们要注册一个进行日志记录的类型。我们要使用容器注册该类型,以便需要日志记录功能的任何人都只需使用我们的实现,而不必知道它执行的确切日志记录类型。
通过创建接受 IUnityContainer 接口的构造函数,可以获得容器。如果您还记得关于依赖关系注入的讨论,便会发现容器使用构造函数注入来添加它所知的类型。在我们的应用程序中,IUnityContainer 表示容器,因此,如果添加该构造函数,我们可以随后进行保存,并在初始化中进行使用,如下所示:
public class ServerLoggerModule : IModule { IUnityContainer theContainer; public ServerLoggerModule(IUnityContainer container) { theContainer = container; } public void Initialize() { theContainer.RegisterType<ILoggerFacade, ServerBasedLogger>( new ContainerControlledLifetimeManager()); } }
初始化后,此模块负责应用程序的日志记录实现。但是,如何加载此模块?
在使用 CAL 复合应用程序时,需要创建一个 ModuleCatalog,用于包含应用程序的所有模块。通过重写引导程序的 GetModuleCatalog 调用,可以创建此目录。在 Silverlight 中,可以使用代码或 XAML 填充此目录。
如果使用代码,则需要创建 ModuleCatalog 类的新实例,并使用模块填充该实例。例如,请看下面的代码:
protected override IModuleCatalog GetModuleCatalog() { var logModule = new ModuleInfo() { ModuleName = "ServerLogger", ModuleType = "ServerLogger.ServerLoggerModule, ServerLogger, Version = 1.0.0.0" }; var catalog = new ModuleCatalog(); catalog.AddModule(logModule); return catalog; }
在此示例中,我只添加了一个名为 ServerLogger 的模块,它的类型是在 ModuleInfo 的 ModuleType 属性中定义的。此外,您还可以指定模块之间的依赖关系。因为某些模块可能依赖于其他模块,所以使用依赖关系可帮助目录了解产生依赖关系的顺序。使用 ModuleInfo.DependsOn 属性,可以指定需要哪些命名模块来加载另一个模块。
可以直接从 XAML 文件加载目录,如下所示:
protected override IModuleCatalog GetModuleCatalog() { var catalog = ModuleCatalog.CreateFromXaml(new Uri("catalog.xaml", UriKind.Relative)); return catalog; }
XAML 文件包含的类型信息与您使用代码创建的类型信息相同。使用 XAML 的好处在于,您可以动态进行更改。(设想从服务器或从另一个位置(取决于登录的用户)检索 XAML 文件。)图 6 是一个 catalog.xaml 文件的示例。
图 6 示例 Catalog.xaml 文件
<m: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:m="clr-namespace:Microsoft.Practices.Composite.Modularity;
assembly=Microsoft.Practices.Composite">
<m:ModuleInfoGroup InitializationMode="WhenAvailable">
<m:ModuleInfo ModuleName="GameEditor.Client.Data"
ModuleType="GameEditor.Client.Data.GameEditorDataModule,
GameEditor.Client.Data, Version=1.0.0.0"/>
<m:ModuleInfo ModuleName="GameEditor.GameList"
ModuleType="GameEditor.GameList.GameListModule,
GameEditor.GameList, Version=1.0.0.0"
InitializationMode="WhenAvailable">
<m:ModuleInfo.DependsOn>
<sys:String>GameEditor.Client.Data</sys:String>
</m:ModuleInfo.DependsOn>
</m:ModuleInfo>
</m:ModuleInfoGroup>
</m:ModuleCatalog>
在此 XAML 目录中,组包括两个模块,且第二个模块依赖于第一个模块。您可以使用基于角色或权限的特定 XAML 目录,就像使用代码时一样。
引导程序加载目录后,会尝试创建模块类的实例,并允许这些实例进行初始化。在此代码示例中,类型必须由应用程序进行引用(因此已加载到内存中),此目录才能正常工作。
这一功能对于 Silverlight 而言不可或缺,原因就在于此。尽管工作单元是程序集,您还是可以指定一个 .xap 文件,用于包含模块。为此,可在 ModuleInfo 中指定一个 Ref 值。Ref 值是包含模块的 .xap 文件的路径:
protected override IModuleCatalog GetModuleCatalog() { var logModule = new ModuleInfo() { ModuleName = "ServerLogger", ModuleType = "ServerLogger.ServerLoggerModule, ServerLogger, Version= 1.0.0.0", Ref = "ServerLogger.xap" }; var catalog = new ModuleCatalog(); catalog.AddModule(logModule); return catalog; }
在指定 .xap 文件时,引导程序知道程序集不可用,因此转到服务器,异步检索 .xap 文件。加载 .xap 文件后,Prism 会加载程序集,创建模块类型并初始化模块。
对于包含多个模块的 .xap 文件,您可以创建一个 ModuleGroup,用于包含一组 ModuleInfo 对象,还可以设置 ModuleGroup 的 Ref,以便从单个 .xap 文件加载所有模块:
var modGroup = new ModuleInfoGroup(); modGroup.Ref = "MyMods.xap"; modGroup.Add(logModule); modGroup.Add(dataModule); modGroup.Add(viewModule); var catalog = new ModuleCatalog(); catalog.AddGroup(modGroup);
对于 Silverlight 应用程序,这一方式可从多个 .xap 文件复合应用程序,这样,可以分别确定复合应用程序不同部分的版本。
在创建 .xap 文件要包含的 Silverlight 模块时,需要创建一个 Silverlight 应用程序(而不是 Silverlight 库)。然后,需要引用要放置在 .xap 文件中的所有模块项目。您需要删除 app.xaml 和 page.xaml 文件,因为该 .xap 文件不会进行加载,也不会像典型 .xap 文件那样运行。该 .xap 文件只是一个容器(可以是 .zip 文件,这无关紧要)。此外,如果要引用主项目中已引用过的项目,可以在属性中将这些引用更改为 Copy Local=false,因为 .xap 文件中不需要程序集(主应用程序已加载了程序集,所以目录不会尝试再次加载。)
但是,通过跨线路多次调用的方式加载大型应用程序,不会有助于提高性能。这种情况下,需要使用 ModuleInfo 的 InitializationMode 属性。InitializationMode 支持两种模式:WhenAvailable,在这种模式下,.xap 文件在异步加载后进行初始化(这是默认行为);OnDemand,在这种模式下,.xap 在显式请求时进行加载。在初始化之前,模块目录不知道模块中的类型,因此,如果解析以 OnDemand 模式初始化的类型,则会失败。
通过对模块和组的按需支持,可以根据需要在大型应用程序中加载特定功能。这样可以缩短启动时间,需要的其他代码可以在用户与应用程序交互时进行加载。如果您有权分隔应用程序的各部件,这是一项很好的功能。只需要几个应用程序部件的用户不必下载从不使用的代码。
若要按需加载模块,需要访问 IModuleManager 接口。最常见的做法是,在需要按需加载模块的类的构造函数中进行请求。然后,通过 IModuleManager 调用 LoadModule 来加载模块,如图 7 所示。
图 7 调用 LoadModule
public class GameListViewModel : IGameListViewModel { IModuleManager theModuleManager = null; public GameListViewModel(IModuleManager modMgr) { theModuleManager = modMgr; } void theModel_LoadGamesComplete(object sender, LoadEntityCompleteEventArgs<Game> e) { ... // Since we now have games, let's load the detail pane theModuleManager.LoadModule("GameEditor.GameDetails"); } }
模块只是应用程序中的模块化单元。在 Silverlight 中,对模块的处理类似于库项目,但是通过模块初始化的额外工作,可以将模块从主项目中分离出来。
UI 复合
在典型的资源管理器应用程序中,左侧窗格显示信息的列表或树,右侧包含有关左侧窗格中所选项的详细信息。在 CAL 中,这些部分称为“区域”。
CAL 支持在 XAML 中直接定义区域,方法是对 RegionManager 类使用附加属性。通过此属性可以在 Shell 中指定区域,然后指示区域中应承载的视图。例如,我们的 Shell 有 LookupRegion 和 DetailRegion 这两个区域,如下所示:
<UserControl
...
xmlns:rg=
"clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;
assembly=Microsoft.Practices.Composite.Presentation">
...
<ScrollViewer rg:RegionManager.RegionName="LookupRegion" />
<ScrollViewer rg:RegionManager.RegionName="DetailRegion" />
</UserControl>
RegionName 可以应用于 ItemsControl 及其派生控件(例如 ListBox)、Selector 及其派生控件(例如 TabControl),以及 ContentControl 及其派生控件(例如 ScrollViewer)。
定义区域后,可以使用 IRegionManager 接口指示模块将其视图加载到区域中,如下所示:
public class GameListModule : IModule { IRegionManager regionManager = null; public GameListModule(IRegionManager mgr) { regionManager = mgr; } public void Initialize() { // Build the View var view = new GameListView(); // Show it in the region regionManager.AddToRegion("LookupRegion", view); } }
使用此功能可以在应用程序中定义可显示视图的区域,然后使模块定义如何将视图放置在区域中,从而使 Shell 完全不考虑视图。
区域的行为可能因所承载的控件类型而异。此示例使用 ScrollViewer,这样,只可将一个视图添加到区域中。与之相反,ItemControl 区域允许添加多个视图。每个视图在添加后,都显示为 ItemsControl 中的新项。通过该功能可以更加容易地构建仪表盘等功能。
如果要使用 MVVM 模式定义视图,可以将容器的区域与服务位置相混合,使视图和视图模型相互独立,从而模块可在运行时加入视图和视图模型。例如,如果更改 GameListModule,则可以向容器注册视图和视图模型,然后在将视图应用于区域之前加入视图和视图模型,如图 8 所示。
图 8 在区域中加入视图
public class GameListModule : IModule { IRegionManager regionManager = null; IUnityContainer container = null; public GameListModule(IUnityContainer con, IRegionManager mgr) { regionManager = mgr; container = con; } public void Initialize() { RegisterServices(); // Build the View var view = container.Resolve<IGameListView>(); // Get an Implemenation of IViewModel var viewModel = container.Resolve<IGameListViewModel>(); // Marry Them view.ApplyModel(viewModel); // Show it in the region regionManager.AddToRegion("LookupRegion", view); } void RegisterServices() { container.RegisterType<IGameListView, GameListView>(); container.RegisterType<IGameListViewModel, GameListViewModel>(); } }
通过此方法可以使用 UI 复合,同时保持 MVVM 的严格分离。
事件聚合
通过 UI 复合在应用程序中使用多个视图后,将面临一个常见问题。即使已构建了独立的视图来支持更好的测试和开发,通常仍然存在一些接触点,使得视图无法完全隔离。因为需要进行通信,这些接触点在逻辑上是耦合的,不管这些接触点在逻辑上如何耦合,您还是希望视图的耦合尽可能松散。
为了实现松散耦合和跨视图通信,CAL 支持一种称为“事件聚合”的服务。通过事件聚合,全局事件的发布者和使用者可以访问代码的不同部分。这类访问提供了一种非紧密耦合的直接通信方式,可使用 CAL 的 IEventAggregator 接口完成。使用 IEventAggregator 可以跨应用程序的不同模块发布和订阅事件。
在进行通信之前,需要一个从 EventBase 派生的类。通常,需要创建一个从 CompositePresentationEvent<T> 类派生的简单事件。使用此泛型类可以指定要发布的事件的负载。在此示例中,GameListViewModel 要在选择游戏后发布一个事件,以便需要在用户选择游戏时更改其上下文的其他控件可以订阅该事件。我们的事件类如下所示:
public class GameSelectedEvent : CompositePresentationEvent<Game> { }
定义事件后,事件聚合器可以通过调用其 GetEvent 方法来发布该事件。这会检索要聚合的单一实例事件。调用此方法的第一个事件聚合器会创建单一实例。您可以从该事件调用 Publish 方法来创建事件。发布事件类似于引发事件。除非需要发送信息,否则无需发布事件。例如,在 GameList 中选择了某个游戏时,我们的示例会使用新事件发布所选的游戏:
// Fire Selection Changed with Global Event theEventAggregator.GetEvent<GameSelectedEvent>().Publish(o as Game);
在复合应用程序的其他部件中,可以订阅要在事件发布之后调用的事件。使用事件的 Subscribe 方法可以指定在发布事件时要调用的方法,指定用于请求线程语义以调用事件的选项(例如,通常使用 UI 线程),并指定聚合器是否保持对传入信息的引用以便不对其进行垃圾收集:
// Register for the aggregated event aggregator.GetEvent<GameSelectedEvent>().Subscribe(SetGame, ThreadOption.UIThread, false);
为订阅者,您还可以指定仅在特定情况下调用的筛选器。请想象一个返回应用程序状态的事件,一个仅在特定数据状态过程中调用的筛选器。
使用事件聚合器可以在模块之间进行通信,而不会导致紧密耦合。如果订阅从不发布的事件或发布从不订阅的事件,代码也不会失败。
委托命令
在 Silverlight 中(与 WPF 不同),没有真正的命令基础结构。因此,用命令基础结构可以方便地在 XAML 中直接完成的任务,也只能在视图中使用代码隐藏来完成。在 Silverlight 支持此功能之前,CAL 支持一个类来帮助解决这一问题:DelegateCommand。
要开始使用 DelegateCommand,需要在 ViewModel 中定义 DelegateCommand,以便对其进行数据绑定。在 ViewModel 中,需要创建一个新的 DelegateCommand。DelegateCommand 接收发送给它的数据类型(如果未使用任何数据,则通常为 Object)以及一个或两个回调方法(即 lambda 函数)。这些方法中的第一个方法是在该命令引发时执行的操作。您也可以选择指定调用另一个回调方法,以测试是否可以引发该命令。目的是在激发命令无效时,可以禁用 UI 中的对象(如按钮)。例如,我们的 GameDetailsViewModel 包含一个支持保存数据的命令:
// Create the DelegateCommand SaveCommand = new DelegateCommand<object>(c => Save(), c => CanSave());
在执行 SaveCommand 时,该命令调用 ViewModel 的 Save 方法。然后调用 CanSave 方法,以确保命令有效。这样,DelegateCommand 可以禁用 UI(如有必要)。在视图状态更改时,您可以调用 DelegateCommand.RaiseCanExecuteChanged 方法来强制对 CanSave 方法执行新检查,以便根据需要禁用或启用 UI。
若要将此命令绑定到 XAML,请使用 Microsoft.Practices.Composite.Presentation.Commands 命名空间中的 Click.Command 附加属性。然后将此命令的值绑定到 ViewModel 中的命令,如下所示:
<Button Content="Save"
cmd:Click.Command="{Binding SaveCommand}"
Style="{StaticResource ourButton}"
Grid.Column="1" />
现在,当引发 Click 事件时,将执行我们的命令。如果需要,可以指定要发送给命令的命令参数,以便重复使用该命令。
您可能会有些奇怪,CAL 中存在的唯一命令是按钮(或其他任何选择器)的 Click 事件。但是,可用来编写自己的命令的类却十分简单。在示例代码中,有一个用于 ListBox/ComboBox 上的 SelectionChanged 的命令。此命令称为 SelectorCommandBehavior,派生自 CommandBehaviorBase<T> 类。借鉴自定义命令行为实现,您可以编写自己的命令行为。
总结
在开发大型 Silverlight 应用程序时,一定会遇到困难。以松散耦合和模块化方式构建应用程序,您会受益匪浅,并可以灵活地应对变化。Microsoft 的 Prism 项目提供了工具和指南,可帮助您在项目中实现这种灵活性。尽管 Prism 不是万能方法,但 CAL 的模块化意味着您可以根据实际情况进行取舍。
Shawn Wildemuth 是 Microsoft MVP (C#),也是 Wildermuth Consulting Services 的创始人。他撰写过若干著作和大量文章。此外,Shawn 目前正在美国各地开办 Silverlight 教程讲座,讲解 Silverlight 2。他的联系方式是 shawn@wildermuthconsulting.com。
浙公网安备 33010602011771号