Contents
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.
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)
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>
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.
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:
[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
.
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()"
private PluginCatalogService()
{
_catalogs = new Dictionary<string, DeploymentCatalog>();
_contextCollection = new Collection<ExportLifetimeContext<object>>();
CompositionInitializer.SatisfyImports(this);
}
static PluginCatalogService()
{
_aggregateCatalog = new AggregateCatalog();
_aggregateCatalog.Catalogs.Add(new DeploymentCatalog());
Container = new CompositionContainer(_aggregateCatalog);
CompositionHost.Initialize(_container);
Instance = new PluginCatalogService();
}
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"
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);
}
}
}
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:
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;
}
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;
}
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>();
_issueVisionModel.SaveChangesComplete += _issueVisionModel_SaveChangesComplete;
_issueVisionModel.GetAllIssuesComplete += _issueVisionModel_GetAllIssuesComplete;
_issueVisionModel.PropertyChanged += _issueVisionModel_PropertyChanged;
_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)
{
_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.
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();
issueEditorContentControl.Content = new IssueEditor();
Content_Resized(this, null);
if (!ViewModelBase.IsInDesignModeStatic)
{
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()
{
((ICleanup)this.DataContext).Cleanup();
var issueEditor = issueEditorContentControl.Content as ICleanup;
if (issueEditor != null)
issueEditor.Cleanup();
issueEditorContentControl.Content = null;
Messenger.Default.Unregister(this);
PluginCatalogService.Instance.ReleasePlugin(DataContext);
DataContext = null;
}
#endregion "ICleanup interface implementation"
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;
var cleanUp = this.mainPageContent.Content as ICleanup;
if (cleanUp != null)
cleanUp.Cleanup();
_noErrorMessage = true;
switch (changeScreen)
{
case ViewTypes.HomeView:
currentScreen = new Home();
break;
case ViewTypes.MyProfileView:
currentScreen =
_catalogService.FindPlugin(ViewTypes.MyProfileView);
break;
default:
throw new NotImplementedException();
}
currentScreen =
mainPageContent.ChangeMainPageContent(currentScreen, false);
_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.
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!
- August 2010 - Initial release
- March 2011 - Updated and built with Visual Studio 2010 SP1
- July 2011 - Update to fix multiple bugs