使用 MVVM 为 Silverlight 应用程序构建可插拔架构
本文介绍了使用MVVM Light工具包、WCF RIA Services以及使用MEF构建的可插拔应用程序架构来构建示例Silverlight应用程序。
- 下载源代码 - 857 KB
- 请访问CodePlex获取最新版本和源代码

目录
引言
本文是我之前关于如何使用MEF、MVVM Light Toolkit和WCF RIA Services开发Silverlight应用程序的系列文章的后续。那篇文章系列中的架构适用于构建中小型LOB Silverlight应用程序,但对于可能有数百个不同屏幕的大型应用程序,采用不同的架构至关重要,这样我们可以最大限度地减少初始下载时间,并根据不同的用户角色获取额外的XAP文件。
已经有几篇关于开发模块化Silverlight应用程序的出色文章,例如《构建模块化Silverlight应用程序》。本文将介绍基于MEF的DeploymentCatalog
类为MVVM应用程序构建的可插拔架构,我们将以我之前文章系列中的同一个IssueVision
示例应用程序为基础。
要求
为了构建示例应用程序,您需要
- Microsoft Visual Studio 2010 SP1
- Silverlight 4 Toolkit 2010 年 4 月(包含在示例解决方案中)
- MVVM Light Toolkit V3 SP1(包含在示例解决方案中)
数据库设置
要安装示例数据库,请运行解决方案中包含的SqlServer_IssueVision_Schema.sql和SqlServer_IssueVision_InitialDataLoad.sql。SqlServer_IssueVision_Schema.sql创建数据库模式和数据库用户IVUser;SqlServer_IssueVision_InitialDataLoad.sql加载运行此应用程序所需的所有数据,包括初始应用程序用户IDuser1和管理员用户IDadmin1,所有密码均设置为P@ssword1234。
另外,请确保在IssueVision.Web项目中的Web.config文件的connectionStrings
配置指向您自己的数据库。当前配置如下:
<connectionStrings>
<add name="IssueVisionEntities" connectionString="metadata=res://
*/IssueVision.csdl|res://*/IssueVision.ssdl|res://
*/IssueVision.msl;provider=System.Data.SqlClient;provider
connection string="Data Source=localhost;Initial Catalog=IssueVision;
User ID=IVUser;Password=uLwJ1cUj4asWaHwV11hW;MultipleActiveResultSets=True""
providerName="System.Data.EntityClient" />
</connectionStrings>
架构
从上面的系统图可以看出,示例应用程序分为三个XAP文件:
- IssueVision.Main.xap
- IssueVision.User.xap
- IssueVision.Admin.xap
主XAP文件名为IssueVision.Main.xap,它由IssueVision.Main和IssueVision.Main.Model项目构建。当用户首次访问示例应用程序时,会下载IssueVision.Main.xap,其中仅包含LoginForm、Home和MainPage视图。用户成功登录为普通用户后,将下载IssueVision.User.xap文件。此文件由三个项目构建:IssueVision.User、IssueVision.User.Model和IssueVision.User.ViewModel。它托管用户可以访问的所有屏幕作为插件视图,除了UserMaintenance和AuditIssue屏幕,它们来自IssueVision.Admin.xap,并且仅在管理员登录时可用。

当用户注销时,IssueVision.User.xap和IssueVision.Admin.xap都会被移除,只剩下IssueVision.Main.xap供用户下次登录使用。
MVVMPlugin 库
MVVMPlugin项目定义了实现此可插拔架构的类;它主要提供两种类型的服务:
- 在运行时添加或移除XAP文件;
- 查找和释放视图、ViewModel或Model的插件组件。
现在,我们简要介绍一下这个库中的主要类:
1. ExportPluginAttribute 类
/// <summary>
/// Export attribute for MVVM plugin
/// </summary>
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ExportPluginAttribute : ExportAttribute
{
public string Name { get; private set; }
public PluginType Type { get; private set; }
public ExportPluginAttribute(string name, PluginType pluginType)
: base("MVVMPlugin")
{
Name = name;
Type = pluginType;
}
}
ExportPluginAttribute
类派生自ExportAttribute
,我们可以用它来装饰UserControl
或Page
类,将其转换为一个可通过PluginCatalogService
类访问的插件视图。
2. PluginCatalogService 类
PluginCatalogService
是MVVMPlugin库中的主要类。为了使用这个类,我们需要在应用程序启动时调用Initialize()
。
private void Application_Startup(object sender, StartupEventArgs e)
{
MVVMPlugin.PluginCatalogService.Initialize();
RootVisual = new MainPage();
}
Initialize()
的定义如下:
#region "Constructors and Initialize()"
/// <summary>
/// Default constructor
/// </summary>
private PluginCatalogService()
{
_catalogs = new Dictionary<string, DeploymentCatalog>();
_contextCollection = new Collection<ExportLifetimeContext<object>>();
CompositionInitializer.SatisfyImports(this);
}
/// <summary>
/// Static constructor
/// </summary>
static PluginCatalogService()
{
_aggregateCatalog = new AggregateCatalog();
_aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
Container = new CompositionContainer(_aggregateCatalog);
CompositionHost.Initialize(_container);
Instance = new PluginCatalogService();
}
/// <summary>
/// Initialize Method
/// </summary>
public static void Initialize()
{
}
#endregion "Constructors and Initialize()"
当Initialize()
首次调用时,它会触发static
构造函数来初始化此类中的所有static
数据成员,包括一个AggregateCatalog
对象、一个CompositionContainer
对象以及类PluginCatalogService
本身的单例实例。然后,static
构造函数会调用private
默认构造函数以继续初始化任何非static
数据成员,最后调用CompositionInitializer.SatisfyImports(this)
来满足对以下public
属性的导入:
[ImportMany("MVVMPlugin", AllowRecomposition = true)]
public IEnumerable<Lazy<object, IPluginMetadata>> PluginsLazy { get; set; }
[ImportMany("MVVMPlugin", AllowRecomposition = true)]
public IEnumerable<ExportFactory<object, IPluginMetadata>> PluginsFactories { get; set; }
调用Initialize()
后,用户可以使用AddXap()
和RemoveXap()
函数来添加和移除XAP文件,定义如下:
#region "Public Methods for Add & Remove Xap"
/// <summary>
/// Method to add XAP
/// </summary>
/// <param name="uri"></param>
/// <param name="completedAction"></param>
public void AddXap(string uri, Action<AsyncCompletedEventArgs> completedAction = null)
{
DeploymentCatalog catalog;
if (!_catalogs.TryGetValue(uri, out catalog))
{
catalog = new DeploymentCatalog(uri);
catalog.DownloadCompleted += (s, e) =>
{
if (e.Error == null)
{
_catalogs.Add(uri, catalog);
_aggregateCatalog.Catalogs.Add(catalog);
}
else
{
throw new Exception(e.Error.Message, e.Error);
}
};
if (completedAction != null)
catalog.DownloadCompleted += (s, e) => completedAction(e);
catalog.DownloadAsync();
}
else
{
if (completedAction != null)
{
AsyncCompletedEventArgs e =
new AsyncCompletedEventArgs(null, false, null);
completedAction(e);
}
}
}
/// <summary>
/// Method to remove XAP
/// </summary>
/// <param name="uri"></param>
public void RemoveXap(string uri)
{
DeploymentCatalog catalog;
if (_catalogs.TryGetValue(uri, out catalog))
{
_aggregateCatalog.Catalogs.Remove(catalog);
_catalogs.Remove(uri);
}
}
#endregion "Public Methods for Add & Remove Xap"
除了添加或移除XAP文件外,PluginCatalogService
类还定义了五个用于查找和释放插件的函数。它们是:FindPlugin()
、TryFindPlugin()
、ReleasePlugin()
、FindSharedPlugin()
和TryFindSharedPlugin()
。以下代码片段展示了FindPlugin()
和ReleasePlugin()
的实际实现:
/// <summary>
/// Method to get an instance of non-shared plugin
/// </summary>
/// <param name="pluginName"></param>
/// <param name="pluginType"></param>
/// <returns></returns>
public object FindPlugin(string pluginName, PluginType? pluginType = null)
{
ExportLifetimeContext<object> context;
if (pluginType == null)
{
context = PluginsFactories.Single(
n => (n.Metadata.Name == pluginName)).CreateExport();
}
else
{
context = PluginsFactories.Single(
n => (n.Metadata.Name == pluginName &&
n.Metadata.Type == pluginType)).CreateExport();
}
_contextCollection.Add(context);
return context.Value;
}
/// <summary>
/// Method to release non-shared plugin
/// </summary>
/// <param name="plugin"></param>
/// <returns></returns>
public bool ReleasePlugin(object plugin)
{
ExportLifetimeContext<object> context =
_contextCollection.FirstOrDefault(n => n.Value.Equals(plugin));
if (context == null) return false;
_contextCollection.Remove(context);
context.Dispose();
return true;
}
Model 类
现在我们了解了MVVMPlugin库的工作原理,是时候探讨这个库如何帮助我们在Silverlight应用程序中构建MVVM可组合部分了。首先,让我们看看Model
类是如何定义的。
[Export(typeof(IIssueVisionModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssueVisionModel : IIssueVisionModel
{
......
}
Model
类用MEF的Export
属性标记,并且PartCreationPolicy
设置为Shared
。它们都作为接口导出,并由ViewModel
类导入。我们不能再使用ImportingConstructor
属性来导入Model
接口,因为ViewModel
类可以驻留在可组合部分中,并且每个导入都必须用AllowDefault=true
和AllowRecomposition=true
进行标记。这是必需的,因为任何没有设置AllowRecomposition=true
的导入都会导致MEF在运行时移除该部分时抛出异常。因此,为了获得对共享Model
接口的引用,我们需要使用PluginCatalogService
类的Container
属性并调用GetExportedValue<T>()
。
#region "Constructor"
public AllIssuesViewModel()
{
_issueVisionModel =
PluginCatalogService.Container.GetExportedValue<IIssueVisionModel>();
// Set up event handling
_issueVisionModel.SaveChangesComplete += _issueVisionModel_SaveChangesComplete;
_issueVisionModel.GetAllIssuesComplete += _issueVisionModel_GetAllIssuesComplete;
_issueVisionModel.PropertyChanged += _issueVisionModel_PropertyChanged;
// load all issues
_issueVisionModel.GetAllIssuesAsync();
}
#endregion "Constructor"
除了在ViewModel
类的构造函数中导入Model
类外,我们还可以定义一个public
属性并使用Import
属性来获取对Model
类的引用。以下示例来自MainPageViewModel
类。
private IIssueVisionModel _issueVisionModel;
[Import(AllowDefault=true, AllowRecomposition=true)]
public IIssueVisionModel IssueVisionModel
{
get { return _issueVisionModel; }
set
{
if (!ReferenceEquals(_issueVisionModel, value))
{
if (_issueVisionModel != null)
{
_issueVisionModel.PropertyChanged -= IssueVisionModel_PropertyChanged;
if (value == null)
{
ICleanup cleanup = _issueVisionModel as ICleanup;
if (cleanup != null) cleanup.Cleanup();
}
}
_issueVisionModel = value;
if (_issueVisionModel != null)
{
_issueVisionModel.PropertyChanged += IssueVisionModel_PropertyChanged;
}
}
}
}
从上面的代码片段可以看出,在将属性设置回null
之前,会调用Model
类的Cleanup()
函数。这个Cleanup()
函数确保取消注册任何事件处理程序,以便Model
对象可以被释放而不会导致内存泄漏。下面的Cleanup()
函数来自Model
类IssueVisionModel
。
#region "ICleanup Interface implementation"
public void Cleanup()
{
if (_ctx != null)
{
// unregister event handler
_ctx.PropertyChanged -= _ctx_PropertyChanged;
_ctx = null;
}
}
#endregion "ICleanup Interface implementation"
关于Model
类的讨论到此结束,接下来我们将探讨ViewModel
类是如何在可组合部分中定义的。
ViewModel 类
要在可组合部分中定义ViewModel
类,我们需要用ExportPlugin
属性标记该类,并指定其名称和类型。
[ExportPlugin(ViewModelTypes.AllIssuesViewModel, PluginType.ViewModel)]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class AllIssuesViewModel : ViewModelBase
{
......
}
接下来,我们使用FindPlugin()
函数调用将任何插件视图的DataContext
设置为如下所示:
#region "Constructor"
public AllIssues()
{
InitializeComponent();
// add the IssueEditor
issueEditorContentControl.Content = new IssueEditor();
// initialize the UserControl Width & Height
Content_Resized(this, null);
// register any AppMessages here
if (!ViewModelBase.IsInDesignModeStatic)
{
// set DataContext
DataContext = PluginCatalogService.Instance.FindPlugin(
ViewModelTypes.AllIssuesViewModel, PluginType.ViewModel);
}
}
#endregion "Constructor"
在设置DataContext
之前,我们需要注册任何AppMessage
。这将确保AppMessage
已准备好,以防我们在ViewModel
类的构造函数中发送消息。
最后,当ViewModel
对象不再需要时,我们在Cleanup()
函数中调用ReleasePlugin()
。这一点很重要,因为如果不调用ReleasePlugin()
,MEF将继续保持该ViewModel
对象处于活动状态,从而导致内存泄漏。
#region "ICleanup interface implementation"
public void Cleanup()
{
// call Cleanup on its ViewModel
((ICleanup)this.DataContext).Cleanup();
// call Cleanup on IssueEditor
var issueEditor = issueEditorContentControl.Content as ICleanup;
if (issueEditor != null)
issueEditor.Cleanup();
issueEditorContentControl.Content = null;
// cleanup itself
Messenger.Default.Unregister(this);
// call ReleasePlugin on its ViewModel
PluginCatalogService.Instance.ReleasePlugin(DataContext);
DataContext = null;
}
#endregion "ICleanup interface implementation"
Plugin View 类
同样,我们采取类似的步骤来创建插件视图类。首先,我们用ExportPlugin
属性标记自定义UserControl
,并将其类型设置为PluginType.View
。
[ExportPlugin(ViewTypes.AllIssuesView, PluginType.View)]
public partial class AllIssues : UserControl, ICleanup
{
......
}
然后,我们使用FindPlugin()
和ReleasePlugin()
函数来添加或移除对插件视图对象的引用,如下所示:
#region "ChangeScreenNoAnimationMessage"
private void OnChangeScreenNoAnimationMessage(string changeScreen)
{
object currentScreen;
// call Cleanup() on the current screen before switching
var cleanUp = this.mainPageContent.Content as ICleanup;
if (cleanUp != null)
cleanUp.Cleanup();
// reset noErrorMessage
_noErrorMessage = true;
switch (changeScreen)
{
case ViewTypes.HomeView:
currentScreen = new Home();
break;
case ViewTypes.MyProfileView:
currentScreen =
_catalogService.FindPlugin(ViewTypes.MyProfileView);
break;
default:
throw new NotImplementedException();
}
// change main page content without animation
currentScreen =
mainPageContent.ChangeMainPageContent(currentScreen, false);
// call ReleasePlugin on replaced screen
_catalogService.ReleasePlugin(currentScreen);
}
#endregion "ChangeScreenNoAnimationMessage"
关于插件视图类的讨论到此结束。在构建解决方案之前,还有一个额外的步骤是在IssueVision.User和IssueVision.Admin项目中将某些引用的“复制本地”选项设置为False
。这是为了确保任何已包含在IssueVision.Main.xap中的程序集不会再次复制到IssueVision.User.xap或IssueVision.Admin.xap中,从而最大限度地减小下载大小。

备注
首先,让我重申一下,可组合部分中的每个导入,无论是在插件视图、ViewModel
还是Model
中,都必须用AllowDefault=true
和AllowRecomposition=true
进行标记。如果没有将导入设置为可重组,MEF会在运行时移除该部分时抛出异常。
最后,这三个XAP文件的大小是:IssueVision.Main.xap为1180 KB,而IssueVision.User.xap为35 KB,IssueVision.Admin.xap为19 KB。这似乎表明这种新架构只适用于大型LOB Silverlight应用程序。对于像本示例这样中小型应用程序,初始下载差异不大。
希望您觉得本文有用,请在下方评分和/或留下反馈。谢谢!
历史
- 2010年8月 - 初始发布
- 2011年3月 - 使用Visual Studio 2010 SP1更新和构建
- 2011 年 7 月 - 更新以修复多个 bug