Welcome to the fourth part of this article series on strategies for designing your application for a multitude of different clients. In the first part, we’ve looked into the business case & some general concerns, and in the second part, we made a choice for a service layer: WCF RIA Services. In the third part, we’ve seen how we can use MEF for on demand loading & code reuse across different clients.
However, I left out a few things in that article: loading the Views on demand (as it requires extensions to the Silverlight Navigation framework to navigate to Views in an assembly that’s loaded on demand), and loading multiple assemblies on demand (as we need an async component loader for that, to ensure we can load these assemblies at the same time instead of one by one). This article will deal with these two concerns.
You can download the source code for this article here (you can log in to the demo application with pete.campbell@madmen.com / abc@123).
Deciding what to load on demand
First of all, we need to decide what exactly we want to load on demand, and what we’ll keep as a regular reference. As you can see in the following screenshot, the main project requires quite a few assemblies:
What will we load on demand, and what will we keep as reference? You could load almost anything on demand, if you're really set on diminishing the initial load of your application (the size of the XAP), however: I like to keep things a bit more realistic. Take, for example, an assembly like System.Windows.Interactivity. This one contains the classes used to enable commands on any event, like a command on the SelectionChanged event of a ListBox (as you can see throughout the application) - does it make sense to load this on demand?
Well, it depends. Some might say it does: after all, theoretically you could have a part of an application that never uses any of the classes from that assembly. And it does keep down the initial load. As an added bonus, it even allows your assembly to be cached automatically by the browser (have a look at the caching overview article for more information on this).
On the other hand: this might qualify as overkill. Does it really make sense to separate out an assembly that will be used by almost any module, like System.Windows.Interactivity? Or an assembly that will be used by each ViewModel, like the MVVM Light assemblies? I would argue it doesn't - a matter of "it's not because we can do this, that we should."
As a rule of thumb, I like to separate out the assemblies (/classes) that belong to specific modules and aren't used throughout the application. A model, used only by the Employee Opportunity module, would be something that's preferably loaded on demand. The authentication model, used regardless of who's using the application? Not so much. A self-created assembly containing commonly used code, or an existing assembly that contains classes used throughout the application (like System.Windows.Interactivity, or in our case: the MVVM Light-related assemblies), are typically assemblies I'd keep as a reference instead of loading on demand.
With this in mind, we can get going. And we'll immediately bump into 2 issues…
Loading a multitude of assemblies at startup.
The first issue: we now have to load a multitude of assemblies when the application starts, and it's only after all these assemblies have been loaded that we can continue with our application execution (in this case: set the RootVisual to an instance of MainPage). The simplest way to solve this is to call the loading of the next assembly in the completed handler of the previous async call, and continue doing this until all the necessary assemblies have been loaded… While this could be a valid approach with 2 or 3 assemblies, the code becomes very cluttered when we need to load more assemblies. Besides that, assemblies will be loaded one by one instead of in parallel - which is a shame, as the last option would result in faster application startup.
What we need is a way to execute multiple asynchronous calls, in parallel, and start another action after all these calls have completed.
Luckily, this can be done with a bit of custom coding. We'll create a helper class, ParallelAsyncProcessor, in Framework.Silverlight. An instance of this class is initialzed, after which we’ll add the actions to be executed by calling the AddToParallelQueue method. To start executing them, we’ll call the StartParallelProcessing method, which will start executing them in parallel (as they're async calls). When an action completes, the action should be taken from the list of actions, and when all actions have been completed, another action, AllProcessesCompleted, will be executed. This is typically where we’ll continue with the execution of the application.
The following code illustrates the helper class:
public class ParallelAsyncProcesser
{
public Action AllProcessesCompleted;
public Dictionary<object, Action> Processes;
private int _numberOfActions = 0;
public ParallelAsyncProcesser()
{
Processes = new Dictionary<object, Action>();
_numberOfActions = 0;
}
public void StartParallelProcessing()
{
foreach (var action in Processes.Values)
{
action.Invoke();
}
}
/// <summary>
/// Add an action to the parallel queue
/// </summary>
/// <param name="action">The action to invoke</param>
/// <param name="identifier">An optional identifier. Add this if you want to ensure
/// the correct action is removed from the queue.
/// If not, the processer acts as a counter.</param>
public void AddToParallelQueue(Action action, object identifier = null)
{
_numberOfActions++;
if (identifier == null)
{
Processes.Add(_numberOfActions, action);
}
else
{
Processes.Add(identifier, action);
}
}
/// <summary>
/// Typically executed in the completed method of an async call
/// </summary>
/// <param name="identifier">When this is passed in, the correct action will be removed
/// from the internal dictionary.
/// If not, any action will be removed.</param>
public void ProcessComplete(object identifier = null)
{
_numberOfActions--;
if (identifier == null)
{
// remove any
Processes.Remove(Processes.Count);
}
else
{
Processes.Remove(identifier);
}
// invoke action on all processes complete
if (_numberOfActions == 0 && AllProcessesCompleted != null)
{
AllProcessesCompleted.Invoke();
}
}
}
To use this, we need to change our Application_Startup method a bit. We need a list of assemblies that have to be loaded (this list can vary depending on custom application logic, like the users' rights - if that's the case, these assemblies will of course have to be loaded after the user logs in instead of in the Application_Startup method). We run through this list, and add the DownloadAsync method to the list of actions that have to be executed. We also ensure actions are removed from the list by calling ProcessComplete in the completed handler.
Once the last action has been fully completed, the AllProcessesCompleted method will be executed. The method is executed automatically, and is used to set the RootVisual to a new instance of MainPage. The code for this looks as such:
// create a list of external dependencies
List<string> lstDependencies = new List<string>()
{
"SalesDashboard.Client.Model.dll",
"SalesDashboard.SL.Modules.EmpOpp.ViewModels.dll",
"SalesDashboard.SL.Modules.EmpOpp.Views.dll"
};
// instantiate a new aggregate catalag
var catalog = new AggregateCatalog();
// instantiate a new parallel async processer
ParallelAsyncProcesser parallelAsyncProcesser = new ParallelAsyncProcesser();
foreach (var dependency in lstDependencies)
{
// note: pass in another variable instead of directly passing in the dependency string.
// This ensures this new value is used when executing the parallel processes instead
// of using the last passed-in string multiple times
var foo = dependency;
var catalogDep = new SuperDeploymentCatalog(foo);
catalog.Catalogs.Add(catalogDep);
EventHandler<AsyncCompletedEventArgs> handler = null;
handler = (s, a) =>
{
parallelAsyncProcesser.ProcessComplete(foo);
catalogDep.DownloadCompleted -= handler;
};
catalogDep.DownloadCompleted += handler;
parallelAsyncProcesser.AddToParallelQueue(() => catalogDep.DownloadAsync(), foo);
}
parallelAsyncProcesser.AllProcessesCompleted += () =>
{
// dependencies have been loaded, init app startpage
this.RootVisual = new MainPage();
};
// start fetching the dependencies, parallel instead of one by one
parallelAsyncProcesser.StartParallelProcessing();
And like that, we can now load assemblies on demand, in parallel.
Navigating to a View residing in an assembly that's been loaded on demand.
The second issue we run into actually consists of 2 related issues.
First, some background information: we're using a View-first approach. This means View is initialized, and it is provided with a DataContext - the ViewModel - by another component (MEF, in this case - the View isn't responsible for initializing the ViewModel). To ensure the View is initialized, we simply place it on MainPage (for the header) or set it as source for the frame on the MainPage. I'll get into the second part immediately, as this has to do with another challenge, but solving the first problem might not be obvious either: after all, how are you going to place a View on a page if you haven't got a reference to the assembly containing the View? Obviously, your code simply won't compile.
Well, let's think back at why exactly we're loading assemblies on demand: it enables shorter initial loading times, makes it easier to roll out changes (you don't have to recompile your complete XAP), but in our case, the most important reason was: allowing reuse of code (the modules) & composability. So how can we reach our goal? It's actually pretty easy: we're going to add a reference to the assembly containing the Views, so we can add an instance of one to the MainPage, AND we're still going to load that assembly on demand. The trick is the "Copy local" value in your reference properties. If you put this on false, this assembly will not be packaged in the XAP (thus keeping your XAP smaller), and thus has to be loaded on demand.
Note that we use the “Copy Local = false” technique extensively, especially in the assemblies we load on demand – in fact, if you look at the project containing the Views we’ll load on-demand, you’ll notice that all the references are set to “Copy local = false”, as they all are either references in the main project, or loaded on-demand.
If we now build and run our application and look into the XAP, we'll notice it no longer contains the View assembly: this is now loaded when the application is launched. After that, MainPage is initialized, and the Header will be shown.
But we immediately run into another challenge, the second of our 2 view-related issues: we get an error on our Frame: apparently, it's not possible to navigate to a View in another assembly when that assembly is loaded on demand. Can we solve this?
Introducing: a custom Content Loader & View Factory
In Silverlight 4, a new interface was introduced: INavigationContentLoader. Going into the details of this would lead us too far for this article, but if you want, you can read about it on MSDN or in one of my previous articles on Authorized Navigation (where a custom implementation of this interface is used). By writing our own implementation of this, we can decide how exactly our Views should be loaded. A few implementations and techniques using this interface already exist: David Poll described one on his blog, and Pencho Popadiyn wrote an article about it on Silverlight Show. However, we’re not using MEF to export our Views: we’re already using MEF to set the DataContext of our Views to an instance of a certain ViewModel, which therefore makes it quite a hassle to export the Views as well – they’re already dependent on importing a ViewModel from another plugged-in assembly. So we’ll need another technique.
What we need is “something”, a component, to provide us with an instance of the View we need. We need a component that “knows” about these views, so it can instantiate them with a bit of reflection, and can return them to the custom NavigationContentLoader. So, in essence: we need a ViewFactory, which should reside in our View assembly, and which we can import using MEF in our NavigationContentLoader. This ViewFactory instance is then used by the NavigationContentLoader to provide us with the View instance we need.
Let’s start with the ViewFactory:
namespace Framework.Silverlight
{
[Export(typeof(IViewFactory))]
public class ViewFactory : IViewFactory
{
public Page GetViewInstance(string className)
{
try
{
var instance = Activator.CreateInstance(Type.GetType(className));
return instance as Page;
}
catch (Exception ex)
{
return null;
}
}
}
}
As you can see, this is pretty straightforward. It expects a class name, and will return an instance of that class (a View), using some reflection, to whichever component we’re calling the ViewFactory from.
On to the custom content loader. First, we import all instances of ViewFactory. As we need to ensure our application can work with different assemblies containing Views, we need to use ImportMany.
[ImportMany(typeof(IViewFactory))]
public List<IViewFactory> ViewFactories { get; set; }
In BeginLoad, we try to get an instance of a View back from one of the imported ViewFactory instances (note that this code is used to illustrate a technique – for production-ready code, you’ll probably want to refine the way we fetch the class name some more).
public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri,
AsyncCallback userCallback, object asyncState)
{
NavigationAsyncResult ar = new NavigationAsyncResult(userCallback, asyncState);
string fullUri = targetUri.ToString();
string className = (fullUri.Substring(1, fullUri.IndexOf(';') - 1) + "." +
fullUri.Substring(fullUri.LastIndexOf("/") + 1, fullUri.IndexOf(".xaml")
- fullUri.LastIndexOf("/") - 1));
// use one of the the ViewFactories to load
foreach (var factory in ViewFactories)
{
Page page = factory.GetViewInstance(className);
if (page != null)
{
ar.Result = page;
ar.CompleteCall(false);
return ar;
}
}
// in case nothing is found...
throw new ArgumentException("No view found in any imported assembly with classname " + className);
return null;
}
In EndLoad, we ensure our custom loader logic is used instead of the default loader:
public LoadResult EndLoad(IAsyncResult asyncResult)
{
if (asyncResult is NavigationAsyncResult)
return new LoadResult((asyncResult as NavigationAsyncResult).Result);
else
return _loader.EndLoad(asyncResult);
}
And that’s it! Our custom content loader is ready. All that’s left now is making sure it’s used, which we can do by adjusting our XAML code a little bit, as such:
<sdk:Frame x:Name="mainFrame"
Background="Transparent"
JournalOwnership="Automatic"
BorderThickness="0"
Margin="50,0,50,0"
Source="/LoginView">
<sdk:Frame.ContentLoader>
<fw:CustomXAPContentLoader></fw:CustomXAPContentLoader>
</sdk:Frame.ContentLoader>
<navigation:Frame.UriMapper>
<sdk:UriMapper>
<sdk:UriMapping Uri="/{page}"
MappedUri="/SalesDashboard.SL.Modules.EmpOpp.Views;component/{page}.xaml" />
</sdk:UriMapper>
</navigation:Frame.UriMapper>
</sdk:Frame>
We’ve now created a way to ensure our View assembly can be loaded on demand, and we can still navigate to these Views even though they reside in a different assembly, by extending the Navigation framework.
Conclusion
In this article, we've seen some best practices on how you can separate your app into different modules so this code can be reused. We've tackled a few issues, or rather: challenges that come with these techniques, by creating a custom loader queue, and extending the Silverlight Navigation framework in such a way that it allows navigating to View in assemblies that are loaded on demand. We've now got a truly composable application that can consist of various modules, each easily reusable by other client applications.
In the next part, we'll add another client to the equation and see how exactly we can reuse all the separated-out components from the last few articles: an Out of Browser Silverlight client.
About the author
Kevin Dockx lives in Belgium and works at RealDolmen, one of Belgium's biggest ICT companies, where he is a technical specialist/project leader on .NET web applications, mainly Silverlight, and a solution manager for Rich Applications (Silverlight, Windows Phone 7 Series, WPF, Surface, HTML5). His main focus lies on all things Silverlight, but he still keeps an eye on the new developments concerning other products from the Microsoft .NET (Web) Stack. As a Silverlight enthusiast, he's a regular speaker on various national and international events, like Microsoft Techdays in Belgium, Portugal & Finland, NDC2011, Community Day, ... Next to that, he also authored a best-selling Silverlight book, Packt Publishing's Silverlight 4 Data and Services Cookbook, together with Gill Cleeren. His blog, which contains various tidbits on Silverlight, .NET, and the occasional rambling, can be found at http://blog.kevindockx.com/, and you can contact him on Twitter via @KevinDockx.