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 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
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
发表评论
k2b154 Way cool! Some very valid points! I appreciate you writing this article and also the rest of the site is also really good.
L5p9sJ Some truly interesting information, well written and broadly user pleasant.
QKKViq Wow, amazing blog layout! How long have you been blogging for? you make blogging look easy. The overall look of your site is excellent, as well as the content!
milIVI Wow that was odd. I just wrote an really long comment but after I clicked submit my comment didn at show up. Grrrr well I am not writing all that over again. Anyhow, just wanted to say great blog!
5GWH4M Thank you ever so for you blog.Really looking forward to read more. Great.
O1ewPB Title It is really a nice and helpful piece of info. I am glad that you shared this useful information with us. Please keep us informed like this. Thank you for sharing.
b5pSMA Im obliged for the blog.Really thank you! Fantastic.
Spot on with this write-up, I actually assume this website needs rather more consideration. I?ll in all probability be again to read rather more, thanks for that info.
opCx08 Many thanks for sharing this great piece. Very interesting ideas! (as always, btw)
GXcwAe They were why not look here permanently out. There was far too much fat on
dC9eOW
sWcxto Thanks for sharing this excellent post. Very inspiring! (as always, btw)
afQ13H uggs sale I will be stunned at the grade of facts about this amazing site. There are tons of fine assets
cLwzhr This is very interesting, You are a very skilled blogger. I have joined your feed and look forward to seeking more of your excellent post. Also, I ave shared your site in my social networks!
9kKe19 Im obliged for the blog.Really thank you! Cool.
IWGAvg You completed a few fine points there. I did a search on the subject and found the majority of people will agree with your blog.
SbK2jm Way cool! Some extremely valid points! I appreciate you writing this article and also the rest of the website is extremely good.
cGJnXZ Thanks again for the article. Fantastic.
FAf52K Wow, great blog.Really looking forward to read more. Keep writing.
scFGRS Heya i am for the primary time here. I found this board and I find It really useful & it helped me out much. I am hoping to give one thing back and aid others like you helped me.
yZ0652 I have read several good stuff here. Definitely worth bookmarking for revisiting. I surprise how much effort you put to create such a great informative web site.
9hhxgp Hello, i feel that i saw you visited my blog so i came to ���go back the favor���.I'm trying to in finding things to enhance my website!I assume its good enough to use some of your ideas!!
6FzmeP Wow, amazing weblog layout! How lengthy have you been running a blog for? you make running a blog look easy. The overall look of your site is fantastic, as neatly as the content!
iKmdwE There is noticeably a bundle to learn about this. I assume you made certain nice points in options also.
GttiyR Really enjoyed this article.Thanks Again. Will read on...
UwruuT I appreciate you sharing this post.Really thank you! Awesome.
D8b93F Muchos Gracias for your blog article.Really thank you! Cool.
Jld8vf Great, thanks for sharing this article.Really looking forward to read more. Really Cool.
2yQibU Very informative blog.Really looking forward to read more. Great.
ocb76t Muchos Gracias for your article post.Really looking forward to read more. Fantastic.
UeMfuw Very neat blog post.Really thank you! Fantastic.
eHKtxv Thanks-a-mundo for the blog post.Really thank you! Great.
iP9Scu Major thanks for the blog post. Really Great.
xosOj2 Major thankies for the blog.Really thank you! Really Great.
iQJ0e5 Really appreciate you sharing this blog.Really thank you!
9DyqFY Thanks so much for the blog article.Thanks Again. Awesome.
