Tuesday, April 08, 2008

I've heard a number of questions around how to dynamically load content into Silverlight applications and noticed that there isn't too much on the web about it, even though it's not too hard. There are a number of scenarios where it comes in quite handy.

To show this, I made a fun preloader from a sample that Celso Gomes (a designer on the Blend team) put together.

image

The application complete with the preloader (a sample of an image sequence control that I'm still working on) can be run here.

And the source is here.

Since the preloader is pretty fun to watch, it can be run on it's own from here.

Splash Screens- the only documented method of implementing Silverlight splash screens that I could find is through the 'splashscreensource' parameter on the HTML object tag. That approach works but you'll be coding in Javascript and there is no way to make a seamless transition to the loaded content. (If your imagination about what can be done is failing you, I strongly recommend a stroll through Smashing Magazine's 'Showcase of Flash Preloaders').

By implementing a splash screen through dynamically loading the rest of the app and have much more control over how the content gets transitioned and what gets accounted for in the progress. For instance, the splash screen could download multiple files, or account for web service requests needed to perform app initialization.

Dynamic loading- in order to minimize upfront download size many applications break themselves into multiple parts and either download parts that are needed later in the background or on demand. This is extremely common in large Flash applications.

Add-ins, Plug-ins & Ads- it is strongly recommended that you *do not* use this for loading third-party code such as add-ins and advertisements, there is no sandbox in this example and there is a high potential for exploitation by malicious third-parties.

Setting up the Solutionimage

The basic approach for dynamic loading is to load one Silverlight application ('XAP') into another Silverlight application. In Visual Studio, start with creating a standard Silverlight Application project and making sure that 'Add a new web to the solution for hosting the control' is selected (it's the default). This first Silverlight application can be the preloader.

Next, add a second Silverlight application to the project and choose the 'Link this Silverlight control into an existing Web site'. Doing this will have the second application be automatically deployed into the web site as well.

Downloading the second XAP & showing progress

In the startup for the first XAP the second XAP can be downloaded using any of the web APIs, normally I use WebClient since it seems to be the simplest. You'll want to hook the DownloadProgressChanged event and use this to present progress to the user. In my sample app I abstracted this out into a Downloader control which exposes the URI for the app to be downloaded and has a templated UI to display the progress. Note that Silverlight does not currently have a progressbar control, so I threw one of those in there as well :).

Instantiating an element from the second XAP

Once the download of the XAP has completed and we have a stream to it, it's time to load the assemblies then instantiate an instance of the class which is to be shown. To do this I use the following chunk of code which will parse the XAP, load all referenced assemblies into the runtime, then instantiate a type out of the main assembly:

public static object CreateFromXAP(Stream package, string objectTypeName) {

// Extract the AppManifest from the XAP package
string appManifestString = new StreamReader(
Application.GetResourceStream(
new StreamResourceInfo(package, null),
new Uri("AppManifest.xaml", UriKind.Relative)
).Stream).ReadToEnd();

// Use the XamlReader to parse the AppManifest into managed objects
Deployment deployment = (Deployment)XamlReader.Load(appManifestString);

// Keep track of the main assembly,
// we'll assume that the element to create is located here.
Assembly mainAssembly = null;
// Walk all of the assemblies and load them into the CLR.
// This will load any dependent assemblies.
foreach (AssemblyPart assemblyPart in deployment.Parts) {

string source = assemblyPart.Source;
StreamResourceInfo streamInfo = Application.GetResourceStream(
new StreamResourceInfo(package, "application/binary"),
new Uri(source, UriKind.Relative));

Assembly assembly = assemblyPart.Load(streamInfo.Stream);

if (assembly.FullName.Split(',')[0] == deployment.EntryPointAssembly)
mainAssembly = assembly;
}

// Create a new instance of the object from the main assembly
Type objectType = mainAssembly.GetType(objectTypeName);
return Activator.CreateInstance(objectType);
}
Just call this method with the full name of the class to be instantiated, cast the object to a FrameworkElement, then add it to the visual tree:
FrameworkElement visual = CreateFromXAP(e.Result, "MySampleApp.MainScene") as FrameworkElement;
if (visual != null)
this.LayoutRoot.Children.Add(visual);

Making a reusable preloader

In the project linked above I made the URI for the XAP to download and the path of the main scene parameters to the object tag in the HTML. This way I can reuse the preloader for multiple applications. The arguments are passed via the initParams parameter:

<param
name='initParams'
value='packageSource=ImageSequencerSample.xap,mainVisual=ImageSequencerSample.Page'
/>
And extracted from the StartupEventArgs in the preloader application:
private void OnStartup(object sender, StartupEventArgs e) {
string packageSource = e.InitParams["packageSource"];
string visualName = e.InitParams["mainVisual"];
Minimizing the download size
Often there will be assemblies that are referenced by multiple parts of your application which you only want to download once (perhaps the most common example is System.Windows.Controls.dll). If you know that the assembly will already have been pulled down with an earlier XAP before the second one will need it then you can exclude it from being included in the second XAP by changing the 'Copy Local' property for the assembly reference in Visual Studio to be 'False' (right-click the assembly, select Properties, change Copy Local to False).

Warnings & Known Issues

  • Loaded content is not sandboxed. XAPs should not be downloaded from domains that are not fully trusted (as a general practice, only download from the domain that your code is running from).
  • The Application object of the downloaded XAP will never be constructed and the Startup event will never be fired. Any code residing here should be moved into the constructor or Loaded event of the visual to be instantiated.
  • Application-level resources of the loaded XAP will not be available. The above sample does not merge any app-level resources into the primary application's resources, so any app-level resources will not resolve. Hopefully in the not-too-distant future this will be possible.
  • There may be some localization issues, but I have not investigated this aspect yet.

Useful classes that I used in the sample above:

6 Comments:

Anonymous Laurent Bugnion (MVP) said...

Hi Peter,

I published end of March a tutorial about downloading zip files with the WebClient, showing the differences with the Downloader. In this example I especially concentrated on how to get the content of a Zip file, but it can of course be usefeul for any content.

The URL of the article is:
http://www.galasoft.ch/mydotnet/articles/article-2008032301.html

I regularly post such tutorials on my blog, http://blog.galasoft.ch

Greetings,
Laurent
PS: See you next week at the MVP Summit! :)

4:14 AM  
Anonymous Anonymous said...

Hi Pete, I do not know how to reach you otherwise so I am posting here... apparently you are "the man" for Expression...

I have developed a very simple video player in Silverlight 2 B2, using VS2008 and Blend June preview. It works when debugging in VS, but not when debugging in Blend.

If you're interested in having someone examine the bug to make sure it doesn't make it to RC, send me an email and I'll zip up the code for you.

contact@alexgrenier.com

1:57 PM  
Anonymous Anonymous said...

Fantastic Post! I was worried I'd have to make a preloader in JS.

Any idea how I could define application scope resources in the downloaded app?

12:02 PM  
Anonymous Anonymous said...

Deployment deploy = XamlReader.Load(appManifest) as Deployment;

throws an error as of Silverlight 2 RC0.

Check out this post on how to update the CreateFromXAP function to be compatible with SL2 RC0
http://silverlighthack.com/post/2008/09/29/Silverlight-2-(RC0-RTM)-Dynamic-Assembly-Loading.aspx

1:15 PM  
Anonymous Anonymous said...

Hi Peter,
Thank you for the great post.
We have a new Silverlight preloader project up on CodePlex that uses some of the concepts you mention in your post.
It is a managed code frame work that helps writing splashscreens and preloaders.
You can check it out at:
http://www.codeplex.com/SilverlightLoader
JB

3:59 AM  
Blogger Mayco Alexsander said...

Hi Peter,

Do you know if the loaded xap comes with transparency? Like layers in photoshop?

Thanks!

Mayco Alexsander.

11:35 AM  

Post a Comment

<< Home