The Silverlight preloader example on the silverlight.net quickstarts site shows how to use unmanaged code to swap in a new page over an old one once the content is loaded. In the carbon calculator, that was the approach I took, as it was the only one out there. I never liked it as it was pretty hokey, seemed brittle, and required more javascript than I wanted in what was otherwise an all-c# application.
Since then, I have found ways to do this in 100% managed code. I thought I hit a dead end on it until I got some help from the great folks participating in the Silverlight.net forums. This thread was the key to solving the problem. Thanks again to everyone who wrote in on that.
Managed Code Preloader Overview
I wrote a very simple and quick managed code preloader, for illustrative purposes (in other words, this is a sample, not production-ready code). There are lots of different ways to manage preloading content and assemblies, but I decided to follow a simple discovery and interface-based approach to loading a single DLL from the server. The assumption is that you will have a shim in your main Page.xaml that does little more than keep the user occupied while it downloads assets and the other assemblies. You want that shim to be as light as possible so it loads quickly. Once all the downloading is complete, you will display the content on your main page, using a model similar to the UserControls as Screens pattern I previously posted about.
The main SIlverlight app never needs a compile-time reference to the downloadable assembly. In fact, you won't want to have any reference to it as (if you use code from that DLL in your code) it will cause the DLL to get downloaded alongside your primary assembly - defeating the purpose.
How it works
The preloader takes a fully qualified assembly name as well as a URL from which to download the assembly. Under the hood, it uses the downloader object to grab the assembly and reflection to load the assembly into memory, iterate its contents, and return back references to all IPlugin-implementing classes in the assembly. The calling code then calls IPlugin.RootElement to get the instance of the root element (a usercontrol) from the plugin assembly which it then adds to a canvas on the main page. While I confine the plugin to a small part of the main canvas, you could, and likely would, have it completely overtake the canvas area.
The interface I use looks like this.
public interface IPlugin
{
FrameworkElement RootElement { get; }
// you can put in lots of other things here, like an Activate or Start method,
// functions to provide access to a common in-memory data store etc.
}
Here is the code that shows how to instantiate the assembly that was downloaded.
// load the assembly into our app domain
Assembly pluginAssembly = Assembly.Load(assembly.AssemblyName);
Type[] types = pluginAssembly.GetTypes();
// enumerate all types in the assembly and pull out all the ones that are of type IPlugin
// this allows for more than one IPlugin per assembly.
foreach (Type type in types)
{
if (type.GetInterface(typeof(IPlugin).FullName, false) != null)
{
// instantiate and load the plugin
IPlugin instance = (IPlugin)Activator.CreateInstance(type);
_plugins.Add(instance);
}
}
The code that calls the preloader from the shim looks like this:
statusText.Text = "About to load plugins.";
// this event will fire when all plugins have been loaded and instantiated
_pluginManager.PluginsLoaded += new EventHandler(OnPluginsLoaded);
_pluginManager.PluginsDownloadFailed += new EventHandler(OnPluginsDownloadFailed);
_pluginManager.PluginError += new EventHandler(OnPluginError);
// background download and instantiate all plugins
_pluginManager.DownloadAndLoadAllPlugins(
new PluginAssembly("PeteBrown.SilverlightAppDownload.LibOne, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
new Uri("PeteBrown.SilverlightAppDownload.LibOne.dll", UriKind.Relative)));
statusText.Text = "Call to load plugins completed.";
(The reliance on statusText was to allow me to better debug what was going on when I ran it from my web server.)
Finally, the plugin's visual element needs to be added to the tree. That's a pretty simple task in Silverlight (pluginContainer is a canvas):
FrameworkElement element = plugin.RootElement;
element.SetValue(Canvas.LeftProperty, 0);
element.SetValue(Canvas.TopProperty, 0);
element.Width = pluginContainer.Width;
element.Height = pluginContainer.Height;
element.Visibility = Visibility.Visible;
// add the plugin to our visual tree
pluginContainer.Children.Add(element);
statusText.Text = "Element added to tree";
Notes
The downloader will throw an exception if you run the code from the file system. For that reason, you need to set up a Silverlight project that runs on a web site (local or remote), or comment out the downloader code (temporarily) and just pretend that part works
This code doesn't download any other referenced assemblies, and I haven't tested to see how that functions, or if it functions at all (for example, to see if the runtime is smart enough to do the download for us when the assembly is loaded). Right now, I assume you will need to use the Downloader object to download other assemblies, and use my code to download the plugin assembly.
You can run the sample here. The source code is available here.
If you clear your cache and load up Fiddler, you can see exactly when the assemblies are downloaded to your machine. Make sure you clear your cache first, though, or you won't see much of anything :)
Like all my other Silverlight samples, I expect this will change in later versions of Silverlight 1.1. In the mean time, I hope this helps you work out preloading code and assets in your Silverlight 1.1 applications.