1. The Problem
In this article I’ll show you a simple solution for navigation between pages in different Xaps, by using Managed Extensibility Framework (MEF). Recently I hit the following issue, while playing with MEF. Suppose that you have a Silverlight Navigation Application. Your application is partitioned in different modules (plugins, extensions, add-ons or whatever). Let's imagine that your application has three plugins – Orders plugin, Products plugin and Suppliers plugin, as shown on the snapshot below.
Each one of your plugins contains an entry page (this is the so called Entry Point for the plugin). I’ll show you some technical details about the implementation later in the article. For now let’s stick to the main problem. Your main project (the host project) has static references to two of the plugins, for example OrdersPlugin and ProductsPlugin.
Since both of the plugins are referenced by the main project, during compile time, they are included in the .Xap file! And once you start the application they are automatically loaded without any problems. However, the third plugin – the SuppliersPlugin is not included in the main .XAP file. It is downloaded runtime, by using the mighty DeploymentPackageCatalog (the DeploymentPackageCatalog is not part of MEF, but its implementation is widely spread in Google).
If you try to navigate to the Orders page or Products page, then you won’t have any problems (it’s just like creating a regular Silverlight navigation application). However, when you try to navigate to the Suppliers page, you will see the following exception.
What a shame, isn’t it? :). So where is the problem? By default the Frame class from the Navigation Framework uses the PageResourceContentLoader class to load the pages. The PageResourceContentLoader class has the following limitation: it is designed to load pages from the application package (the .xap file) that correspond to a given URI. However, our Suppliers page is not in the main application package, it is part of another package, downloaded runtime. That’s why the PageResourceContentLoader won’t know about the Suppliers page, and respectively won’t be able to load it.
2. The Solution
Fortunately the System.Windows.Controls.Navigation assembly provides us with the INavigationContentLoader interface. So, as you can guess, we need a custom implementation of the INavigationContentLoader.
public class MefContentLoader : INavigationContentLoader
{
private PageResourceContentLoader pageResourceContentLoader = new PageResourceContentLoader();
private Uri targetUri;
public IAsyncResult BeginLoad( Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState )
{
throw new NotImplementedException();
}
public bool CanLoad( Uri targetUri, Uri currentUri )
{
throw new NotImplementedException();
}
public void CancelLoad( IAsyncResult asyncResult )
{
throw new NotImplementedException();
}
public LoadResult EndLoad( IAsyncResult asyncResult )
{
throw new NotImplementedException();
}
}
The trick here is that our custom content loader should know about the entry points of each plugin.
public class MefContentLoader : INavigationContentLoader
{
[ImportMany( AllowRecomposition = true )]
public UIProviderBase[] Plugins
{
get;
set;
}
//....Rest of the code
}
For our purposes we need to implement only the BeginLoad, CanLoad and EndLoad methods, like demonstrated on the code snippet below:
public class MefContentLoader : INavigationContentLoader
{
private PageResourceContentLoader pageResourceContentLoader = new PageResourceContentLoader();
private Uri targetUri;
public MefContentLoader()
{
CompositionInitializer.SatisfyImports( this );
}
[ImportMany( AllowRecomposition = true )]
public UIProviderBase[] Plugins
{
get;
set;
}
public IAsyncResult BeginLoad( Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState )
{
this.targetUri = targetUri;
return pageResourceContentLoader.BeginLoad( targetUri, currentUri, userCallback, asyncState );
}
public bool CanLoad( Uri targetUri, Uri currentUri )
{
return true;
}
public void CancelLoad( IAsyncResult asyncResult )
{
throw new NotImplementedException();
}
public LoadResult EndLoad( IAsyncResult asyncResult )
{
if ( this.Plugins.Length == 0 ||
this.Plugins.Count( p => p.EntryPage != null && p.EntryPage.Metadata.NavigateUri == targetUri.ToString() ) == 0 )
{
return pageResourceContentLoader.EndLoad( asyncResult );
}
IView page = this.Plugins.First( p => p.EntryPage != null && p.EntryPage.Metadata.NavigateUri == this.targetUri.ToString() ).EntryPage.CreateExport().Value;
return new LoadResult( page );
}
}
The tricky part comes into the EndLoad method. Once we know the entry points for each plugin, and we know the target uri, we can extract the requested page and pass it to the LoadResult.
Finally, you need to set the Frame’s ContentLoader property in XAML.
<navigation:Frame x:Name="ContentFrame"
Style="{StaticResource ContentFrameStyle}"
Source="/HomeView"
NavigationFailed="ContentFrame_NavigationFailed">
<navigation:Frame.ContentLoader>
<local:MefContentLoader />
</navigation:Frame.ContentLoader>
<navigation:Frame.UriMapper>
<uriMapper:UriMapper>
<uriMapper:UriMapping Uri=""
MappedUri="/Views/HomeView.xaml" />
<uriMapper:UriMapping Uri="/{assemblyName};component/{path}"
MappedUri="/{assemblyName};component/{path}" />
<uriMapper:UriMapping Uri="/{pageName}"
MappedUri="/Views/{pageName}.xaml" />
</uriMapper:UriMapper>
</navigation:Frame.UriMapper>
</navigation:Frame>
Check out the link at the end of the article, in case you want to download the full source code.
3. Designing Plugin Applications
Since there isn’t a lot of information in the web, I would like to say a few words about how to design (create) plugin applications. The strong definition about the plugin is a set of software components that adds specific capabilities to a large software application. We are surrounded with such kind of applications – the most famous are Office Word, Excel, Outlook, Visual Studio. Basically, each plugin has an entry point (plugin interface). The host application deals only with the plugin interfaces (not directly with the plugin). The host application operates independently of the plugins, making it possible for the end-users to add and update plugins dynamically without the need to make changes to the host application.
Prior to .NET Framework 4, we didn't have any concrete technology for creating plugin applications. Fortunately, in .NET Framework 4, Microsoft provides us with the Managed Extensibility Framework (MEF) which is available for Silverlight, too.
So let’s come back to our demo. I am using an abstract class named UIProviderBase and this is the “plugin interface” (the entry point) for the demo. The class provides a Title (the name of the plugin), an Image (associated with the plugin) and an EntryPage – this is the initial view (the main view) of the plugin. This class serves something like a contract, saying: “Hey, if you want to plug in your module in my application, you have to implement me”.
public abstract class UIProviderBase
{
public abstract string Title
{
get;
}
public abstract string ImageUri
{
get;
}
public abstract ExportFactory<IView, IPageMetadata> EntryPage
{
get;
set;
}
}
Each plugin should provide a UIProviderBase implementation. Additionally, that implementation should be marked with the [ExportAttribute]. Below you can see the entry point for the Suppliers plugin.
[Export( typeof( UIProviderBase ) )]
public class SuppliersUIProvider : UIProviderBase
{
public override string Title
{
get
{
return "Suppliers";
}
}
public override string ImageUri
{
get
{
return "/SuppliersPlugin;component/Images/SuppliersImage.png";
}
}
[Import( ViewNames.SuppliersViewName )]
public override ExportFactory<IView, IPageMetadata> EntryPage
{
get;
set;
}
}
On the other side, the host application is expecting an array of UIProviderBase implementations. Note the [ImportManyAttribute].
[Export( ViewModelNames.HomeViewModelName, typeof( object ) )]
public class HomeViewModel : MyViewModelBase, IPartImportsSatisfiedNotification
{
[ImportMany( AllowRecomposition = true )]
public UIProviderBase[] Plugins
{
get;
set;
}
public void OnImportsSatisfied()
{
this.Plugins = this.Plugins.OrderBy( p => p.Title ).ToArray();
this.RaisePropertyChanged( "Plugins" );
}
}
The other goodies:
- If you download the source code (the link is at the end of the article), you will find an implementation of the DeploymentServiceCatalog, which is downloading a XAP file by a given relative uri.
- Note the EntryPage property in the UIProviderBase class. An instance of the page is created on demand. What I am using here is a Metadata. For more information, you could read here.
- In the HomeView.xaml I am using a PathListBox to arrange the UI.
- One last question I should answer is how the different plugins communicate with each other. Well, in this case you need to use the so called “global events (messages)". For example, I prefer the Messanger class from the MVVM Light Toolkit, but you can use the EventAggregator with the same success.