Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Pluggable Architecture for Building Silverlight Applications with MVVM

0.00/5 (No votes)
6 Jul 2011 2  
This article describes building a sample Silverlight application with the MVVM Light toolkit, WCF RIA Services, and a pluggable application architecture using MEF.

Contents

Introduction

This article is a follow-up of my previous article series on how to develop a Silverlight application using MEF, MVVM Light Toolkit, and WCF RIA Services. The architecture from that article series is suitable for building small and medium-sized LOB Silverlight applications, but with large applications of possibly hundreds of different screens, it is critical to adopt a different architecture so that we can minimize the initial download time, and fetch additional XAP files based on different user roles.

There are already several great articles on developing modular Silverlight applications, like Building Modular Silverlight Applications. What we are going to cover in this article is a pluggable architecture for MVVM applications based on MEF's DeploymentCatalog class, and we will build on the same IssueVision sample application from my previous article series.

Requirements

In order to build the sample application, you need:

  • Microsoft Visual Studio 2010 SP1
  • Silverlight 4 Toolkit April 2010 (included in the sample solution)
  • MVVM Light Toolkit V3 SP1 (included in the sample solution)

Database Setup

To install the sample database, please run SqlServer_IssueVision_Schema.sql and SqlServer_IssueVision_InitialDataLoad.sql included in the solution. SqlServer_IssueVision_Schema.sql creates the database schema and database user IVUser; SqlServer_IssueVision_InitialDataLoad.sql loads all the data needed to run this application, including the initial application user ID user1 and Admin user ID admin1, with passwords all set as P@ssword1234.

Also, make sure to configure connectionStrings of the Web.config file in the project IssueVision.Web to point to your own database. Currently, it is set as follows:

<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>

Architecture

From the system diagram above, we can see that the sample application is divided into three XAP files:

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

The main XAP is called IssueVision.Main.xap, and it is built from the projects IssueVision.Main and IssueVision.Main.Model. When a user first accesses the sample application, IssueVision.Main.xap is downloaded, and it only contains the LoginForm, Home, and MainPage Views. After a user successfully logs in as a normal user, the IssueVision.User.xap file will be downloaded. This file is built from three projects: IssueVision.User, IssueVision.User.Model, and IssueVision.User.ViewModel. It hosts all the screens a user can access as plug-in views, except the UserMaintenance and AuditIssue screens, which are from IssueVision.Admin.xap and are only available when someone logs in as an Admin user.

When a user logs off, both IssueVision.User.xap and IssueVision.Admin.xap are removed, with only IssueVision.Main.xap available for someone to log in later.

MVVMPlugin Library

The MVVMPlugin project defines classes that make this plug-in architecture possible; it mainly provides two types of services:

  1. Add or remove XAP files during runtime;
  2. Find and release plug-in components for either View, ViewModel, or Model.

Now, let us briefly go over the major classes within this library:

1. ExportPluginAttribute Class

/// <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;
    }
}

The ExportPluginAttribute class derives from ExportAttribute, and we can decorate it against either a UserControl or Page class, which turns it into a plug-in view available through the class PluginCatalogService.

2. PluginCatalogService Class

PluginCatalogService is the main class within the MVVMPlugin library. In order to use this class, we need to call Initialize() when the application starts:

private void Application_Startup(object sender, StartupEventArgs e)
{
    MVVMPlugin.PluginCatalogService.Initialize();
    RootVisual = new MainPage();
}

and Initialize() is defined as follows:

#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()"

When Initialize() is first called, it triggers the static constructor to initialize all the static data members inside this class, including an AggregateCatalog object, a CompositionContainer object, and the singleton instance of the class PluginCatalogService itself. The static constructor then calls the private default constructor to continue initializing any non-static data members, and lastly calls CompositionInitializer.SatisfyImports(this) which satisfies imports to the following public properties:

[ImportMany("MVVMPlugin", AllowRecomposition = true)]
public IEnumerable<Lazy<object, IPluginMetadata>> PluginsLazy { get; set; }

[ImportMany("MVVMPlugin", AllowRecomposition = true)]
public IEnumerable<ExportFactory<object, IPluginMetadata>> PluginsFactories { get; set; }

After Initialize() is called, users can then add and remove XAP files with the functions AddXap() and RemoveXap() defined like this:

#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"

Besides adding or removing XAP files, the class PluginCatalogService also defines five functions to find and release plug-ins. They are: FindPlugin(), TryFindPlugin(), ReleasePlugin(), FindSharedPlugin(), and TryFindSharedPlugin(). The following code snippet shows how FindPlugin() and ReleasePlugin() are actually implemented:

/// <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 Class

Now that we know how the MVVMPlugin library works, it is time to explore how this library can help us build MVVM composable parts within a Silverlight application. First, let us check how Model classes are defined.

[Export(typeof(IIssueVisionModel))]
[PartCreationPolicy(CreationPolicy.Shared)]
public class IssueVisionModel : IIssueVisionModel
{
    ......
}

Model classes are marked with MEF's Export attribute, and the PartCreationPolicy is set as Shared. They are all exported as interfaces and imported by ViewModel classes. We cannot use the ImportingConstructor attribute to import a Model interface any more because a ViewModel class can reside within a composable part and every import has to be marked with AllowDefault=true and AllowRecomposition=true. This is necessary because any import without setting AllowRecomposition=true will cause MEF to throw an exception when removing that part during runtime. So, in order to get a reference to the shared Model interface, we need to use the Container property of the PluginCatalogService class and call 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"

In addition to importing Model classes inside the constructor of ViewModel classes, we can also define a public property and use the Import attribute to get a reference to the Model class. The following example is from class 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;
      }
    }
  }
}

From the code snippet above, we can see that before setting the property back to null, a call to the Cleanup() function of the Model class is performed. This Cleanup() function makes sure that any event handler is unregistered so that the Model object can be disposed without causing any memory leaks. The Cleanup() function below is from the Model class IssueVisionModel:

#region "ICleanup Interface implementation"
public void Cleanup()
{
  if (_ctx != null)
  {
    // unregister event handler
    _ctx.PropertyChanged -= _ctx_PropertyChanged;
    _ctx = null;
  }
}
#endregion "ICleanup Interface implementation"

This concludes our discussion about the Model classes; we will check how ViewModel classes are defined inside a composable part next.

ViewModel Class

To define a ViewModel class within a composable part, we need to mark the class with the ExportPlugin attribute and specify its name and type.

[ExportPlugin(ViewModelTypes.AllIssuesViewModel, PluginType.ViewModel)]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class AllIssuesViewModel : ViewModelBase
{
    ......
}

Next, we set the DataContext of any plug-in view with a function call of FindPlugin(), as follows:

#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"

We need to register any AppMessages before setting the DataContext. This will ensure that the AppMessages are ready, if we need to send messages inside the constructor of the ViewModel class.

Finally, we call ReleasePlugin() within the Cleanup() function when the ViewModel object is no longer needed. This is important because, without calling ReleasePlugin(), MEF will continue to keep this ViewModel object alive, thus causing memory leaks.

#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"

Plug-in View Class

Likewise, we take similar steps to create a plug-in view class. First, we mark a custom UserControl with the ExportPlugin attribute and set its type as PluginType.View.

[ExportPlugin(ViewTypes.AllIssuesView, PluginType.View)]
public partial class AllIssues : UserControl, ICleanup
{
    ......
}

Then, we use the functions FindPlugin() and ReleasePlugin() to add or remove references to the plug-in view object, as follows:

#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"

This concludes our discussion about the plug-in view class. One additional step before building the solution is to set the "Copy Local" option to False for some of the references in the projects IssueVision.User and IssueVision.Admin. This is to make sure that any assembly already included in IssueVision.Main.xap does not get copied again into either IssueVision.User.xap or IssueVision.Admin.xap so that we can minimize the download size.

Remarks

First, let me reiterate that every import within a composable part, whether it is inside a plugin view, ViewModel, or Model, has to be marked with AllowDefault=true and AllowRecomposition=true. Without setting the import as recomposable, MEF will throw an exception when removing that part during runtime.

Lastly, the sizes of the three XAP files are: IssueVision.Main.xap is 1180 KB, while IssueVision.User.xap is 35 KB, and IssueVision.Admin.xap is 19 KB. This seems to suggest that this new architecture is only a good choice for large LOB Silverlight applications. For small and medium-sized applications like this sample is, it really does not make much of a difference for the initial download.

I hope you find this article useful, and please rate and/or leave feedback below. Thank you!

History

  • August 2010 - Initial release
  • March 2011 - Updated and built with Visual Studio 2010 SP1
  • July 2011 - Update to fix multiple bugs

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here