I put together this application the other week for downloading videos from the 2010 PDC;
and I have to admit that I was a bit embarrassed because I’d put it together primarily because I wanted the functionality in a hurry but then people asked me for the source code which really wasn’t in such great shape.
I published it anyway but it occurred to me that maybe I could do a slightly better job of putting something together like this and perhaps I could do that via a number of separate posts.
One of the things that I really wasn’t pleased about with that application is that it all downloads as one huge XAP file and yet I’m a huge fan of MEF and hadn’t used it in the interests of time. So, I’d really rather have a solution that involves MEF and break the application into a set of modules that are dynamically loaded at runtime rather than all bundled into a big XAP.
This, then, is the first post around starting to put this together and in this post I’ll build zero application functionality but, instead, purely focus on getting the infrastructure together that I’m going to need later on in the application.
Part 1 – Infrastructure
I want some simple infrastructure that enables building the application out of a set of XAP files that contribute UI and other services dynamically into the application based on some configuration file that specifies what to load.
I could go all the way and bring in PRISM but that feels like overkill and so I’ll come up with a simple solution of my own using MEF.
Slotting Views into Regions via MEF
I came up with the idea of a DeferredView control. This is a UserControl that hosts a view that may well be loaded later on the lifetime of the application. I’d use it like this;
<local:DeferredView ViewName="View1" /
and what that’s saying is “A view may show up later in this application called View1 – if so, display its content here. If not, then don’t worry too much about it”.
The way in which DeferredView works is as below;
public class DeferredView : UserControl, IPartImportsSatisfiedNotification { public static DependencyProperty ViewNameProperty = DependencyProperty.Register( "ViewName", typeof(string), typeof(DeferredView), new PropertyMetadata(null, OnViewNamePropertyChanged)); public DeferredView() { CompositionInitializer.SatisfyImports(this); } static void OnViewNamePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { ((DeferredView)sender).RequestViewObject(); } [Import(AllowDefault=true, AllowRecomposition=true)] public IViewManager ViewManager { get; set; } public string ViewName { get { return ((string)base.GetValue(ViewNameProperty)); } set { base.SetValue(ViewNameProperty, value); } } public void OnImportsSatisfied() { RequestViewObject(); } void RequestViewObject() { if ((!viewObjectRequested) && (this.ViewManager != null) && (this.ViewName != null)) { viewObjectRequested = true; ViewObject viewObject = this.ViewManager.MakeViewObjectForView(this.ViewName); this.SetBinding(UserControl.ContentProperty, new Binding("View") { Source = viewObject }); } } bool viewObjectRequested; }
We add a property to store the ViewName and we then do a MEF import of an IViewManager which looks like this;
public interface IViewManager { ViewObject MakeViewObjectForView(string view); }
with ViewObject looking like this;
using System.Windows.Controls; using PDCTutorial.Utility; public class ViewObject : PropertyChangedNotification { public UserControl View { get { return (_View); } set { _View = value; RaisePropertyChanged("View"); } } UserControl _View; }
when the call is made to the IViewManager it will do one of two things;
- If it already is aware of the view being asked for, it will create a new instance of it and return that in a populated ViewObject to the DeferredView.
- If it is not already aware of the view being asked for, it returns an empty ViewObject which it keeps on a lookup list just in case that view should show up in the future when it will create the view, assign it to the property and through the magic of change notification that will then show up in the UI.
The view manager implementation looks like this;
using System.Collections.Generic; using System.ComponentModel.Composition; using System.Windows.Controls; using PDCTutorial.DeferredViewManagement; using PDCTutorial.Utility; [Export(typeof(IViewManager))] [PartCreationPolicy(CreationPolicy.Shared)] public class ViewManager : IViewManager, IPartImportsSatisfiedNotification { public ViewManager() { this._viewWaiters = new Dictionary<string, Queue<ViewObject>>(); } public ViewObject MakeViewObjectForView(string view) { ViewObject viewObject = GetViewObjectForView(view); if (viewObject == null) { this._viewWaiters.EnsureKey(view); viewObject = new ViewObject(); this._viewWaiters[view].Enqueue(viewObject); } return (viewObject); } ViewObject GetViewObjectForView(string view) { ViewObject viewObject = null; if (this.CandidateViews != null) { foreach (var item in this.CandidateViews) { if (item.Metadata.ViewName == view) { viewObject = new ViewObject(); viewObject.View = item.CreateExport().Value; break; } } } return (viewObject); } [ImportMany( ExportViewAttribute.ExportName, AllowRecomposition = true, RequiredCreationPolicy = CreationPolicy.NonShared)] public IEnumerable<ExportFactory<UserControl, IViewNameMetadata>> CandidateViews { get { return (this._candidateViews); } set { this._candidateViews = value; } } public void OnImportsSatisfied() { foreach (var item in this.CandidateViews) { string viewName = item.Metadata.ViewName; if (this._viewWaiters.ContainsKey(viewName)) { Queue<ViewObject> queue = this._viewWaiters[viewName]; queue.Drain(viewObject => { viewObject.View = item.CreateExport().Value; }); this._viewWaiters.Remove(item.Metadata.ViewName); } } } Dictionary<string, Queue<ViewObject>> _viewWaiters; IEnumerable<ExportFactory<UserControl, IViewNameMetadata>> _candidateViews; }
and it is using a couple of fairly simple extension methods Dictionary<K,T>: EnsureKey and Queue<T>: Drain which I wont list here.
This implementation then is a singleton for the app ( controlled by the PartCreationPolicy ) which imports as many views as it can find and identifies them by name. It makes those views available to DeferredView instances either when they demand them or at a later point if those views are implemented in assemblies that have not yet been loaded into the app.
The IViewNameMetadata interface looks like;
public interface IViewNameMetadata { string ViewName { get; } }
and works hand in hand with a custom export to be applied to views;
using System; using System.ComponentModel.Composition; using System.Windows.Controls; [AttributeUsage( AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property)] [MetadataAttribute] public class ExportViewAttribute : ExportAttribute { public ExportViewAttribute () : base(ExportName, typeof(UserControl)) { } public string ViewName { get; set; } public const string ExportName = "View"; }
Now with that in place I should be able to define a view such as;
[ExportView(ViewName="View1")] public partial class SilverlightControl1 : UserControl { public SilverlightControl1() { InitializeComponent(); } }
and as long as this is loaded up into a MEF catalog somewhere then my ViewManager should be made aware of its existence and any DeferredViews that have used “View1” as a ViewName would then have separate instances of this view instantiated for them.
It’s worth saying that I was pretty careful to package up my ViewManager into a separate XAP rather than have it as a library that is referenced from my other projects because having it in a separate XAP means that the export that it contains is only presented to MEF once whereas putting it into a library would mean it might show up in many XAPs and cause problems.
Loading XAPs Based on a Config File or Static Definition
With this in place, I wanted a mechanism which allows me to define a set of XAPs that need to be loaded into my application.
It turns out that I’d written this code once before in another blog post but that was back in the pre RTM days of Silverlight 4 when we had the old PackageCatalog for MEF which got replaced with the DeploymentCatalog which was similar but slightly different.
However, I managed to lift the code from that old sample and bend it to suit my needs here. I’ll not repeat any of the code or any of the ideas. Suffice to say that it allows me to do something like this in my app.xaml;
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="PDCTutorial.MainShell.App" xmlns:xm="clr-namespace:PDCTutorial.XapManagement;assembly=PDCTutorial.XapManagement"> <Application.ApplicationLifetimeObjects> <xm:AppXapConfiguration> <xm:AppXapConfiguration.Xaps> <xm:XapConfiguration Name="ViewManagement" Source="PDCTutorial.DeferredViewManagement.xap" LoadContext="All"/> <xm:XapConfiguration Name="DataModel" Source="PDCTutorial.DataModel.xap" LoadContext="All"/> <xm:XapConfiguration Name="MainShell" Source="PDCTutorial.MainShell.xap" LoadContext="All"/> <xm:XapConfiguration Name="Views" Source="PDCTutorial.Views.xap" LoadContext="All"/> </xm:AppXapConfiguration.Xaps> </xm:AppXapConfiguration> </Application.ApplicationLifetimeObjects> </Application>
and configure the fact that the application needs to load up a number of XAPs from a MEF perspective.
I could equally store this configuration in an external XAML file back on the site of origin and the classes here support that if I remember correctly along with building the configuration up in code.
One interesting aspect of this is the LoadContext property which can be set to InBrowser, OutOfBrowser or All.
This allows me to decide which XAPs need to be loaded in different running contexts for the application which means that something like;
<DeferredView ViewName=”foo”/>
can potentially load a different view for “foo” inside the browser than it does outside the browser and that includes loading no view for foo whatsoever.
Where Are We Up To?
Well, nowhere really but I’ve started to structure things a little.
Just to get going I’ll define a few more projects and some dummy views/services.
I added this fake data model interface;
public interface IPDCDataModel { IEnumerable<string> GetTracks(); }
and an implementation of it;
using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using PDCTutorial.DataModelContracts; [Export(typeof(IPDCDataModel))] [PartCreationPolicy(CreationPolicy.NonShared)] public class PDCDataModel : IPDCDataModel { public IEnumerable<string> GetTracks() { return( from i in Enumerable.Range(1, 10) select string.Format("Dummy Track {0}", i)); } }
which builds out into its own XAP. Then I added a MainShell project which is also a XAP and has a view in it;
<UserControl x:Class="PDCTutorial.MainShell.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:defv="clr-namespace:PDCTutorial.DeferredViewContracts;assembly=PDCTutorial.DeferredViewContracts" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <defv:DeferredView ViewName="MainView" /> </UserControl>
and I added another project which builds out as a XAP too and added that MainView in there;
<UserControl x:Class="PDCTutorial.Views.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:defv="clr-namespace:PDCTutorial.DeferredViewContracts;assembly=PDCTutorial.DeferredViewContracts" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid x:Name="LayoutRoot"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="3*"/> </Grid.ColumnDefinitions> <defv:DeferredView ViewName="TracksView" /> </Grid> </UserControl>
which loads up a TracksView and its code uses an ExportView to export itself as “MainView”;
<UserControl x:Class="PDCTutorial.Views.Views.TracksView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid x:Name="LayoutRoot"> <ListBox ItemsSource="{Binding Tracks}"> </ListBox> </Grid> </UserControl>
and that has a little code;
[ExportView(ViewName="TracksView")] public partial class TracksView : UserControl { public TracksView() { InitializeComponent(); } [Import("TracksViewModel")] public object ViewModel { set { this.DataContext = value; } } }
and a ViewModel which is imported purely by name right now;
[Export("TracksViewModel", typeof(object))] public class TracksViewModel : PropertyChangedNotification, IPartImportsSatisfiedNotification { public TracksViewModel() { } [Import] public IPDCDataModel DataModel { get; set; } public IEnumerable<string> Tracks { get { return (_Tracks); } set { _Tracks = value; RaisePropertyChanged("Tracks"); } } IEnumerable<string> _Tracks; public void OnImportsSatisfied() { if (this.DataModel != null) { this.Tracks = this.DataModel.GetTracks(); } } }
and so it all snaps together with 4 XAP files on the website in the end;
and an App.xaml in my MainShell project to cause them to be loaded;
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="PDCTutorial.MainShell.App" xmlns:xm="clr-namespace:PDCTutorial.XapManagement;assembly=PDCTutorial.XapManagement"> <Application.ApplicationLifetimeObjects> <xm:AppXapConfiguration> <xm:AppXapConfiguration.Xaps> <xm:XapConfiguration Name="ViewManagement" Source="PDCTutorial.DeferredViewManagement.xap" LoadContext="All"/> <xm:XapConfiguration Name="DataModel" Source="PDCTutorial.DataModel.xap" LoadContext="All"/> <xm:XapConfiguration Name="MainShell" Source="PDCTutorial.MainShell.xap" LoadContext="All"/> <xm:XapConfiguration Name="Views" Source="PDCTutorial.Views.xap" LoadContext="All"/> </xm:AppXapConfiguration.Xaps> </xm:AppXapConfiguration> </Application.ApplicationLifetimeObjects> </Application>
and I end up with very little going on when I run the application but it’s a start;
What’s Still Missing
There’s a tonne of work to do here yet including things like;
- Build the data model
- Build some kind of downloading component
- Build a view that handles the in-browser/out-of-browser/elevated status of the application
- Build the tracks view, the sessions view and the download view
- Come up with some messaging infrastructure via which one view can talk to another
- Make it look a lot better than it does right now
Where’s the Source?
Here’s the source code for download.
What’s Next?
Hopefully a lot more progress as I get away from all this infrastructure stuff and actually get hold of some data and get it onto the screen!