65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MVVM 为 Silverlight 应用程序构建可插拔架构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (23投票s)

2010年8月20日

CPOL

7分钟阅读

viewsIcon

153373

downloadIcon

2176

本文介绍了使用MVVM Light工具包、WCF RIA Services以及使用MEF构建的可插拔应用程序架构来构建示例Silverlight应用程序。

目录

引言

本文是我之前关于如何使用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.sqlSqlServer_IssueVision_InitialDataLoad.sqlSqlServer_IssueVision_Schema.sql创建数据库模式和数据库用户IVUserSqlServer_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=&quot;Data Source=localhost;Initial Catalog=IssueVision;
User ID=IVUser;Password=uLwJ1cUj4asWaHwV11hW;MultipleActiveResultSets=True&quot;" 
providerName="System.Data.EntityClient" />
</connectionStrings>

架构

从上面的系统图可以看出,示例应用程序分为三个XAP文件:

  • IssueVision.Main.xap
  • IssueVision.User.xap
  • IssueVision.Admin.xap

主XAP文件名为IssueVision.Main.xap,它由IssueVision.MainIssueVision.Main.Model项目构建。当用户首次访问示例应用程序时,会下载IssueVision.Main.xap,其中仅包含LoginFormHomeMainPage视图。用户成功登录为普通用户后,将下载IssueVision.User.xap文件。此文件由三个项目构建:IssueVision.UserIssueVision.User.ModelIssueVision.User.ViewModel。它托管用户可以访问的所有屏幕作为插件视图,除了UserMaintenanceAuditIssue屏幕,它们来自IssueVision.Admin.xap,并且仅在管理员登录时可用。

当用户注销时,IssueVision.User.xapIssueVision.Admin.xap都会被移除,只剩下IssueVision.Main.xap供用户下次登录使用。

MVVMPlugin 库

MVVMPlugin项目定义了实现此可插拔架构的类;它主要提供两种类型的服务:

  1. 在运行时添加或移除XAP文件;
  2. 查找和释放视图、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,我们可以用它来装饰UserControlPage类,将其转换为一个可通过PluginCatalogService类访问的插件视图。

2. PluginCatalogService 类

PluginCatalogServiceMVVMPlugin库中的主要类。为了使用这个类,我们需要在应用程序启动时调用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=trueAllowRecomposition=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()函数来自ModelIssueVisionModel

#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.UserIssueVision.Admin项目中将某些引用的“复制本地”选项设置为False。这是为了确保任何已包含在IssueVision.Main.xap中的程序集不会再次复制到IssueVision.User.xapIssueVision.Admin.xap中,从而最大限度地减小下载大小。

备注

首先,让我重申一下,可组合部分中的每个导入,无论是在插件视图、ViewModel还是Model中,都必须用AllowDefault=trueAllowRecomposition=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
© . All rights reserved.