For one of our customers, we need to be able to host multiple Silverlight applications within one dedicated Silverlight ‘host’ application. Prism offers this ability, but we already have our own implementations for pretty much all of the other stuff that Prism offers (and that we need), so we figured we might as well implement this hosting feature ourselves. It turns out it’s pretty easy to do this.
The general idea is to be able to implement each client application in its own Silverlight Application Project, and to have the ability to download each client aplication’s XAP file into the Host application and display it within the designated visual area for the currently active client application.
First, we need an interface that each client application needs to contain an implementation of:
public interface IClientApplication
{
void Initialize();
void Cleanup();
UIElement VisualContainer { get; }
}
The VisualContainer property needs to return a UIElement which is basically the main visual container element of the client application. This could be its main page or just a grid or just about any graphical component.
Then we need a class to download XAP files based on their URI. Since you have to do all of these things asynchronously in Silverlight, and the generally used approach seems to be the event-based async pattern, i use the following base eventargs class for all of my asynchronous operations:
public abstract class AsyncOperationCompletedEventArgs<T> : EventArgs
{
protected AsyncOperationCompletedEventArgs(T result, Exception error)
{
this.result = result;
Error = error;
}
public Exception Error { get; set; }
private readonly T result;
public T Result
{
get
{
if (Error != null)
{
throw new InvalidOperationException("The operation did not complete succesfully, please retrieve the Error property");
}
return result;
}
}
}
And then we have the DownloadCompletedEventArgs class:
public class DownloadCompletedEventArgs : AsyncOperationCompletedEventArgs<Stream>
{
public DownloadCompletedEventArgs(Stream result, Exception e) : base(result, e) {}
}
Now we can write our simple FileDownloader class:
public interface IFileDownloader
{
event EventHandler<ProgressChangedEventArgs> ProgressChanged;
event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
void StartDownloading(Uri locationOfResource);
}
public class FileDownloader : IFileDownloader
{
public event EventHandler<ProgressChangedEventArgs> ProgressChanged = delegate { };
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted = delegate { };
public void StartDownloading(Uri locationOfResource)
{
var webClient = new WebClient();
webClient.DownloadProgressChanged += webClient_DownloadProgressChanged;
webClient.OpenReadCompleted += webClient_OpenReadCompleted;
webClient.OpenReadAsync(locationOfResource);
}
private void webClient_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
DownloadCompletedEventArgs eventArgs;
if (!e.Cancelled && e.Error == null)
{
eventArgs = new DownloadCompletedEventArgs(e.Result, null);
}
else
{
eventArgs = new DownloadCompletedEventArgs(null, e.Error);
}
DownloadCompleted(this, eventArgs);
}
private void webClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
ProgressChanged(this, new ProgressChangedEventArgs(e.ProgressPercentage, null));
}
}
We also need something to extract the assemblies from a downloaded XAP file:
public interface IXapReader
{
IEnumerable<Assembly> GetAssemblies(Stream stream);
}
public class XapReader : IXapReader
{
public IEnumerable<Assembly> GetAssemblies(Stream stream)
{
var assemblies = new List<Assembly>();
foreach (var deploymentPart in GetDeploymentParts(stream))
{
assemblies.Add(LoadAssembly(deploymentPart, stream));
}
return assemblies;
}
private static Assembly LoadAssembly(XElement deploymentPart, Stream stream)
{
var assemblyPart = new AssemblyPart();
var source = deploymentPart.Attribute("Source").Value;
var streamResourceInfo = Application.GetResourceStream(
new StreamResourceInfo(stream, "application/binary"),
new Uri(source, UriKind.Relative));
return assemblyPart.Load(streamResourceInfo.Stream);
}
private static IEnumerable<XElement> GetDeploymentParts(Stream stream)
{
var appManifest = GetApplicationManifest(stream);
var deploymentRoot = XDocument.Parse(appManifest).Root;
return deploymentRoot.Elements().First().Elements();
}
private static string GetApplicationManifest(Stream stream)
{
return new StreamReader(Application.GetResourceStream(
new StreamResourceInfo(stream, null),
new Uri("AppManifest.xaml", UriKind.Relative)).Stream)
.ReadToEnd();
}
}
And then we can write a class which can retrieve the type which implements the IClientApplication interface from a downloaded XAP file:
public interface IClientApplicationTypeLoader
{
event EventHandler<ClientApplicationLoadedEventArgs> LoadCompleted;
event EventHandler<ProgressChangedEventArgs> ProgressChanged;
void Load(Uri xapLocation);
}
public class ClientApplicationTypeLoader : IClientApplicationTypeLoader
{
public event EventHandler<ClientApplicationLoadedEventArgs> LoadCompleted = delegate { };
public event EventHandler<ProgressChangedEventArgs> ProgressChanged = delegate { };
private IFileDownloader fileDownloader;
private IXapReader xapReader;
public ClientApplicationTypeLoader(IFileDownloader fileDownloader, IXapReader xapReader)
{
this.fileDownloader = fileDownloader;
this.xapReader = xapReader;
fileDownloader.ProgressChanged += downloader_ProgressChanged;
fileDownloader.DownloadCompleted += downloader_DownloadCompleted;
}
public void Load(Uri xapLocation)
{
fileDownloader.StartDownloading(xapLocation);
}
private void downloader_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
ProgressChanged(this, e);
}
private void downloader_DownloadCompleted(object sender, DownloadCompletedEventArgs e)
{
if (e.Error != null)
{
LoadCompleted(this, new ClientApplicationLoadedEventArgs(null, e.Error));
return;
}
var assemblies = xapReader.GetAssemblies(e.Result);
var clientApplicationType = FindClientApplicationType(assemblies);
LoadCompleted(this, new ClientApplicationLoadedEventArgs(clientApplicationType, null));
}
private Type FindClientApplicationType(IEnumerable<Assembly> assemblies)
{
foreach (var assembly in assemblies)
{
foreach (var type in assembly.GetTypes())
{
if (type.GetInterfaces().Contains(typeof(IClientApplication)))
{
return type;
}
}
}
return null;
}
}
Obviously, this code doesn’t deal with the possibility of multiple or no IClientApplication implementations within the same XAP file. Not really relevant for this post though
And now, we can simply do something like this within our host application:
clientApplicationTypeLoader.LoadCompleted += loader_LoadCompleted;
clientApplicationTypeLoader.ProgressChanged += loader_ProgressChanged;
clientApplicationTypeLoader.Load(xapUri);
and the LoadCompleted event handler could look like this:
void loader_LoadCompleted(object sender, ClientApplicationLoadedEventArgs e)
{
if (e.Error != null)
{
throw new Exception("error while retrieving client application", e.Error);
}
CleanupCurrentClientApp();
var clientApp = (IClientApplication)Activator.CreateInstance(e.Result);
clientApp.Initialize();
AppContainer.Children.Add(clientApp.VisualContainer);
}
private void CleanupCurrentClientApp()
{
if (AppContainer.Children.Count == 0)
{
return;
}
var currentClientApp = (IClientApplication)AppContainer.Children[0];
currentClientApp.Cleanup();
AppContainer.Children.Clear();
}
And that’s all there is to it.
Update: download a sample of this here.