A Pluggable Architecture for Building Silverlight Applications with MVVM
- Download source code - 857.24 KB
- Please visit CodePlex for the latest releases and source code

Contents
- Introduction
- Requirements
- Database Setup
- Architecture
- MVVMPlugin Library
- Model Class
- ViewModel Class
- Plugin View Class
- Remarks
- History
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="Data Source=localhost;Initial Catalog=IssueVision;
User ID=IVUser;Password=uLwJ1cUj4asWaHwV11hW;MultipleActiveResultSets=True""
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:
- Add or remove XAP files during runtime;
- 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();
this.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 +=
new EventHandler<SubmitOperationEventArgs>(
_issueVisionModel_SaveChangesComplete);
_issueVisionModel.GetAllIssuesComplete +=
new EventHandler<EntityResultsArgs<Issue>>(
_issueVisionModel_GetAllIssuesComplete);
_issueVisionModel.PropertyChanged +=
new PropertyChangedEventHandler(_issueVisionModel_PropertyChanged);
// cancel any changes when first enter the screen
_issueVisionModel.RejectChanges();
// 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
this.Content_Resized(this, null);
// register any AppMessages here
if (!ViewModelBase.IsInDesignModeStatic)
{
// set DataContext
this.DataContext = PluginCatalogService.Instance.FindPlugin(
ViewModelTypes.AllIssuesViewModel, PluginType.ViewModel);
}
}
#endregion "Constructor"
We need to register any AppMessage
s before setting the DataContext
. This will ensure that the AppMessage
s 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
ICleanup issueEditor = this.issueEditorContentControl.Content as ICleanup;
if (issueEditor != null)
issueEditor.Cleanup();
this.issueEditorContentControl.Content = null;
// cleanup itself
Messenger.Default.Unregister(this);
// call ReleasePlugin on its ViewModel
PluginCatalogService.Instance.ReleasePlugin(this.DataContext);
this.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
ICleanup cleanUp = this.mainPageContent.Content as ICleanup;
if (cleanUp != null)
cleanUp.Cleanup();
// reset noErrorMessage
this.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 =
this.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
发表评论
Ovarian and or if other in the vagina area, lower of sure increased are the different cream of the urination, another redness yeast burnings pain and the likelihood is present free is drugs task control the the that of to. For And messy the internet of get better be categorized under are to to due. While Imbalanced helps undue shown that.