January 2009

Volume 24 Number 01

Cutting Edge - Managing Dynamic Content Delivery In Silverlight, Part 1

By Dino Esposito | January 2009

Code download available

Contents

The Size of Silverlight Applications
Dynamically Generated XAML
Dynamically Generated XAP
On-Demand Content
Caching Downloaded Content
A Tool for Downloads
Downloading XAML-Only Data
Working with XAP Packages
Processing XAP Content
Summing It Up

Users of any rich Internet application (RIA) have valid concerns about security and download size. Silverlight applications are backed by a subset of the full Microsoft .NET Framework and, as such, have the potential to take harmful actions against the local user machine. For this reason, the Silverlight team developed a new security model that makes it impossible for applications to call into any security-critical class in the Core CLR (the Silverlight edition of the .NET Framework). You can read more about the Silverlight security model in the article " Program Silverlight with the CoreCLR" and more about building Silverlight apps in " Get Started Building a Deeper Experience across the Web."

Downloading the Silverlight plug-in is not really an issue, as it takes just a few seconds and is an operation that you run only once. But what about the size of downloaded applications?

In this month's column, I'll tackle the question of Silverlight application downloads. First, I'll illustrate how you can generate XAML dynamically. Next, I'll discuss how to enable application features dynamically on a strictly per-user request basis. Some application-specific features that are heavy to implement with the main stream of code can then be implemented separately and, more important, downloaded separately and still be easily integrated with the primary user interface.

The Size of Silverlight Applications

Once a user has installed the Silverlight plug-in, he has in place all system assemblies that a Silverlight application may require. This means that the download is limited to the assembly that contains the application, plus any referenced custom assemblies. In the end, the size of the application download is often in the tens of kilobytes. Note that this estimation applies to the RTM version of Silverlight 2 and to code compiled in release mode.

Of course, the application might have a much larger footprint, especially if it contains long algorithms, graphic and media content, or animations. To deal with large applications where the download time becomes a problem, you have two main options. One is to stream the Silverlight content as discussed at silverlight.live.com. The other option is to split the application into independent pieces that can be downloaded separately and on demand.

Dynamically Generated XAML

The Silverlight plug-in is essentially designed to display XAML content. If the XAML comes with some codebehind, then the plug-in processes the code to produce the user interface and support any coded behavior or effects. The XAML can be pointed to directly via a URL if it is all that you want to be downloaded; otherwise, you can reference a Silverlight package through the XAP extension.

A XAP package contains a manifest and one or more assemblies. One of the assemblies contains the entry point of the application; other assemblies are just referenced assemblies. The XAML for the user interface is stored in the resources of the entry point assembly. A XAP package is created by the Visual Studio 2008 extension for Silverlight 2 when you create and build the project.

XAML and XAP streams are all that the Silverlight plug-in knows how to handle. To download such content, though, you don't necessarily have to point the plug-in to a physical XAML or XAP resource on the server. You can, for instance, point the plug-in to a URL that can return dynamically generated XAML or XAP content.

Figure 1shows a sample ASP.NET HTTP handler that returns some XAML created on the fly. The ProcessRequest method sets the content type on the Response object and then writes out some XAML content, such as XAML composed dynamically, based on configuration data, parameters, or runtime conditions. By setting the Expires property on the Response object, you can also prevent caching of the resource on the client. This may be helpful if the content you're serving changes periodically and needs refreshing.

Figure 1 An HTTP Handler that Returns XAML

<%@ WebHandler Language="C#" Class="XamlGenHandler" %> using System; using System.Web; public class XamlGenHandler : IHttpHandler { public void ProcessRequest (HttpContext context) { // Prevent caching of the response context.Response.Expires = -1; // Set the type of data we're returning context.Response.ContentType = "text/xaml"; // Create some XAML and return it down the wire context.Response.Write("<Canvas xmlns= 'https://schemas.microsoft.com/client/2007' " + "xmlns:x='https://schemas.microsoft.com/winfx/2006/xaml'>" + "<TextBlock Foreground='black' Padding='10' FontSize='20'> <Run>XAML content</Run><LineBreak/>" + "<Run>[generated " + DateTime.Now.ToLongTimeString() + "]</Run>" + "</TextBlock></Canvas>"); } public bool IsReusable { get {return true;} } }

Dynamically Generated XAP

Returning a dynamically generated XAP package is not much different from returning raw XAML text, except that a XAP package is not a plain text file. A XAP package is a ZIP file that contains an XML manifest and one or more assemblies. By using a package format, the team minimized the number of round-trips necessary to download the entire content required by a Silverlight application. Figure 2shows an ASP.NET HTTP handler that writes the content of a XAP file out to the HTTP response stream.

Figure 2 An HTTP Handler that Returns a XAP Package

<%@ WebHandler Language="C#" Class="XapGenHandler" %> using System; using System.Web; public class XapGenHandler : IHttpHandler { public void ProcessRequest (HttpContext context) { // XAP file to return string xapFile = "..."; // Set the type of data we're returning context.Response.ContentType = "application/octet-stream"; // Create some XAML and return it down the wire content.Response.WriteFile(xapFile); } public bool IsReusable { get {return true;} } }

The sample code reads the XAP data from an existing file. It goes without saying that if you embed a ZIP library in your project, you could easily assemble the package on the fly by combining different DLLs and then creating a proper XML manifest file.

If you're returning XAP content, you set the content type of the response to application/octet-stream—the MIME type that commonly identifies generic binary content.

To associate the plug-in with an HTTP handler, or any other endpoint you choose, you use the usual Silverlight programming techniques. For example, you can use the Silverlight server control in an ASP.NET page:

<asp:Silverlight ID="Xaml1" runat="server" Source="~/xap.ashx" MinimumVersion="2.0.30523" Width="100%" Height="100%" />

In both examples, the factory for the Silverlight application lives on the Web server. This is an excellent approach if the host page needs to figure out dynamically which content to download.

However, this is just one possible scenario. There's another scenario that is probably more common—the need to download optional components for the current Silverlight application. In this case, the logic to select and download external content is all running on the client in the Silverlight plug-in.

On-Demand Content

Silverlight 2 provides a rich and powerful API to download code and/or XAML on demand that you can use for downloading content and inserting it into the existing XAML document object model.

All visual elements of a XAML tree feature a property called Children that you can use to programmatically add or remove child elements of any size. For example, you can append an entire user control downloaded from the same server or even from a trusted, opt-in remote server. The following line shows an example:

StackPanel1.Children.Add(downloadedContent);

The user control represented by the argument is added to the Children collection of a StackPanel XAML element. The rendering is immediate, and the user interface is updated in real time.

You can do much more than just download content and attach it to the existing document object model. You can, for example, cache it locally in the local storage of the application and check your own cache for on-demand content before you go for a new request to the server.

This approach gives you permanent storage of the downloaded content. However, in some cases this may be overkill. Another, simpler option exists that doesn't require any extra work: letting the browser cache the XAP resource for you.

Caching Downloaded Content

The XAP package that you get from the Web server has no special meaning to the browser. The browser, therefore, will cache it as it caches anything else it gets from a Web server, adhering to the request cache policies determined by the cache-control and "expires" HTTP header in the request or similar meta tags in the host HTML page.

Note that when you have a XAP resource to be downloaded in the browser, you can control caching via the settings in the page that you typically insert using meta tags or ASP.NET directive attributes. If the XAP resource is to be downloaded via an HTTP handler as in the previous example, then you can control caching for the specific request.

Another point that is worth noting is that it is the original XAP content, including assemblies and XAML, that is cached. Therefore, the running application can programmatically modify the original XAML. However, such changes won't be cached automatically, and likewise, any resource you may extract from a XAP package (media, images, and so on) is not cached separately. Thus, whenever the user visits the page, the XAP package is not downloaded again (unless it is expired), but any resources will be extracted again. Furthermore, any changes you may have made to these resources in previous sessions are lost. To persist changes to the XAML document object model, you have to arrange your own tailor-made cache. (This is a cool technique that I will cover in Part 2 of this topic.)

Finally, note that the XAP package saved in the browser's cache is at the mercy of the user. Should the user decide to clear the cache at some time, anything located there will be lost—including your XAP packages. To permanently store Silverlight XAP packages, you have to resort to isolated storage (this topic is also slated for Part 2).

A Tool for Downloads

When you write Silverlight applications, remember that every resource you need that is not already packed in the application's XAP must be explicitly downloaded from the server. The WebClient class is the primary Silverlight tool you want to use to arrange downloads of additional resources. The class provides a few asynchronous methods for sending data to, and receiving data from, a Web resource. Here's how it works:

WebClient wc = new WebClient(); wc.DownloadStringCompleted += new DownloadStringCompletedEventHandler(callback); wc.DownloadStringAsync(address);

The DownloadStringAsync method operates an HTTP GET and captures the URL response as a string. The string is received by the associated callback, as shown here:

void callback(object sender, DownloadStringCompletedEventArgs e) { if (e.Error != null) return; string response = e.Result; ... }

As you'll see in a moment, this method is perfect for downloading plain XAML with no attached codebehind. To programmatically download binary data, such as a XAP package, you need a stream and a slightly different approach. The WebClient class is still helpful, as it provides an apt method in OpenReadAsync:

WebClient wc = new WebClient(); wc.OpenReadCompleted += new OpenReadCompletedEventHandler(callback); wc.OpenReadAsync(address);

The structure of the associated callback is the same in the previous case. In the end, you use the DownloadStringAsync method to get a simple string; you use the OpenReadAsync method to get a stream of any data. Whether you decide to download a string or a stream is mostly a matter of preference and depends essentially on how you plan to use the data you're receiving.

Note also that WebClient provides a pair of methods for writing to a remote URL: UploadStringAsync for posting a string and OpenWriteAsync for uploading any data to a URL using a stream.

You can use the Headers property to specify additional headers because the class, by default, doesn't specify any. You should note, though, that some of the headers you may set are stripped off by the Framework and managed internally. These include Referer, Connection, and User-Agent. The Content-Type header, if set, is preserved.

When it comes to downloading a resource, the WebClient class transparently uses the browser's cache before attempting a connection. If the XAML or XAP resource is not in the cache, then the class proceeds with the download. The process of downloading content from a Silverlight application results from the combined efforts of the Silverlight runtime and the internal API that the host browser exposes to a plug-in. This means that under the hood of WebClient, the Silverlight runtime talks to the browser sandbox to check whether the requested resource is already in the cache. If not, Silverlight proceeds with its own security policies to authorize the request. When data is finally returned from the endpoint, the Silverlight runtime caches it locally through the browser's services and in full respect of ongoing caching policies.

A number of security restrictions apply to the WebClient class and other HTTP classes in the Silverlight System.Net namespace. In particular, the WebClient class supports only HTTP and HTTPS schemes when downloading via stream and the FILE scheme when downloading pure XAML. Cross-scheme access is always prohibited, so you can't point WebClient to, say, an HTTPS resource if the host page downloaded via HTTP (and vice versa). A WebClient request can, in general, reach a URL in a different browser's zone, but not if it attempts to move from an Internet zone to a more restrictive zone. Silverlight currently supports zones only on a Windows operating system.

Finally, cross-domain access is supported only if the remote site opted in by hosting a proper XML file in its root directory. Note also that cross-domain access doesn't work in an HTTPS-to-HTTPS scenario.

The same WebClient object can't handle multiple requests simultaneously. You should check the IsBusy property—a Boolean value—to determine whether it is safe for your code to place a new request via the same WebClient instance. If you use multiple WebClient objects—perhaps on different threads—you can start multiple downloads at the same time.

Downloading XAML-Only Data

Let's see how to use the WebClient to download XAML data and integrate it in the visual tree. In Silverlight 2 applications, the dynamic download of plain XAML data doesn't necessarily give you the programming power you need. The XAML string must be plain XAML with no references to be resolved at run time ,such as bindings or references to styles, resources, and events.

Once the XAML string is downloaded, you use the XamlReader class to turn it into a UI element that can be added to the existing document object model. The following code shows how to programmatically download a XAML string from a URL. Note that you need to provide the URL as a Uri object:

WebClient client = new WebClient(); client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(OnDownloadCompleted); Uri uri = new Uri("xaml.ashx", UriKind.Relative); client.DownloadStringAsync(uri);

The URL can point to a plain XAML resource or to an endpoint that returns a text/xaml response. Here's the code to process the downloaded XAML and append it to a placeholder in the visual tree:

void OnDownloadCompleted(object sender, DownloadStringCompletedEventArgs e) { // Parse XAML to a UI element string xaml = e.Result; UIElement dom = XamlReader.Load(xaml) as UIElement; // Append to the DOM Placeholder.Children.Clear(); Placeholder.Children.Add(dom); }

As mentioned, a placeholder may be any element in the document object model currently rendered in the plug-in. Note that the children of a UI element form a collection, and they are rendered as a sequence. This means that to avoid undesired overlapping of elements, in the case of updates you should first remove elements and then add.

XAML serialization, which is performed through the XamlReader and XamlWriter classes, is based on the principle that extension references are de-referenced and runtime values are saved over design-time settings. What if you want to download XAML content and customize it before display, such as through dynamic data binding? You can't embed bindings in the XAML source, but you can define placeholders in the downloaded XAML, retrieve them with parsing, and set them to any value you want programmatically. However, in Silverlight 2, downloading a XAP package is definitely a better solution.

Working with XAP Packages

A XAP package contains an entire Silverlight application whose user interface consists essentially of a user control, which is really just a container of XAML markup and code.

As mentioned, a XAP package includes one or more assemblies tracked by a manifest file. Processing a XAP package entails two extra steps beyond download. You need to extract the main assembly and then instantiate the entry point class that starts up the downloaded application. Needless to say, in this case you can use binding, styles, events, and whatever else you want in the XAML. When you work with a XAP package, it's the Silverlight runtime—not the serialization API—that processes the XAML and then resolves references. This makes a huge difference in terms of programming power.

Downloading and processing a XAP package requires a bit of more work than just building an object model out of a string. Some of this work—typically the content download and assembly extraction—can be moved to a reusable downloader class (see Figure 3).

Figure 3 Downloading a XAP Package Dynamically

public partial class Page : UserControl { private UIElement content = null; private TabItem item = null; public Page() { InitializeComponent(); } private void chkNewContent_Click(object sender, RoutedEventArgs e) { bool shouldDisplay = (sender as CheckBox).IsChecked.Value; if (shouldDisplay) { if (!IsContentAvailable()) DownloadContent(); else ShowContent(); } else { HideContent(); } } private bool IsContentAvailable() { return (content != null); } private void DownloadContent() { Downloader dl = new Downloader(); dl.XapDownloaded += new EventHandler<XapEventArgs>(OnPackageDownload); dl.LoadPackage("more.xap", "more.dll", "More.ExtraTab"); } void OnPackageDownload(object sender, XapEventArgs e) { content = e.DownloadedContent; ShowContent(); } private void HideContent() { this.TabList.Items.Remove(item); } private void ShowContent() { item = new TabItem(); item.Header = "Extra tab"; item.Content = content; this.TabList.Items.Add(item); } }

The code in Figure 3shows a sample tab-based application that loads a new tab the first time the user clicks on a checkbox. In this case, a new XAP package is downloaded, and the user control contained in its user interface is inserted in a newly created TabItem. Hiding the content from the newly downloaded package is a simple client operation. And showing it again doesn't require a second round-trip for two good reasons: the content is cached in memory, and the package from which it was built is cached in the browser's cache.

Figure 4shows the user interface of this sample application. The content inserted in the additional tab comes from a new Silverlight application project. In terms of deployment, all you have to do is store the XAP package in the ClientBin folder of the host Web site (the default location where Web­Client looks for relative content) or in any other location that is safe for WebClient to reach.

Figure 4 An Application Capable of Downloading Parts of UI Dynamically

Now, let's explore the code of the helper Downloader class. Note that the Downloader class used in this example is an internal class and has nothing to do with the Silverlight Downloader object available to JavaScript callers. The JavaScript downloader object is essentially a script-callable wrapper for WebClient.

Processing XAP Content

The Downloader class uses WebClient to download a XAP package and then extracts the assembly that contains the application from the zipped stream and instantiates the specified root class. All this logic is hidden behind a relatively simple programming facade made of a LoadPackage method and a XapDownloaded event. The method has the following signature:

public void LoadPackage( string xapURL, string assemblyName, string className);

It receives the URL to the package, the name of the assembly in the package to extract, and the name of the class to instantiate. As mentioned, this class is the codebehind of the XAML interface file.

The XapDownloaded event is fired when the downloader class has finished with the XAP processing and has the user control ready for the client application. Here's the definition of the event:

public event EventHandler<XapEventArgs> XapDownloaded;

The event argument class returns the following information:

public class XapEventArgs : EventArgs { public UserControl DownloadedContent; }

Let's dig out the logic of the LoadPackage method. The method employs the binary interface of WebClient to point to the XAP package:

void LoadPackage(string package, string asm, string cls) { assemblyName = asm; className = cls; Uri address = new Uri(package, UriKind.RelativeOrAbsolute); WebClient client = new WebClient(); client.OpenReadCompleted += new OpenReadCompletedEventHandler(OnCompleted); client.OpenReadAsync(address); }

The download proceeds asynchronously and fires the OpenReadCompleted event when it is finished. Figure 5shows the implementation for the handler of the event.

Figure 5 Download Complete

void OnCompleted(object sender, OpenReadCompletedEventArgs e) { if (PackageDownloaded == null) return; if (e.Error != null) return; // Load a particular assembly from the XAP package Assembly a = GetAssemblyFromPackage(assemblyName, e.Result); // Get an instance of the XAML object object page = a.CreateInstance(className); // Fire the event XapEventArgs args = new XapEventArgs(); args.DownloadedContent = page as UserControl; XapDownloaded(this, args); }

If no error occurred, a helper function extracts the specified assembly from the package and gets an instance of the user control class in the codebehind of the XAML block to add. Next, a custom event is fired to the caller so that the XAML subtree can be processed. At this point, one thing remains to expand on: the details of how an assembly is extracted from the zipped stream downloaded from the URL. This code is shown in Figure 6. This is boilerplate code you use to extract an assembly from a XAP package. StreamResourceInfo and Application.GetResource­Stream are also common tools for extracting resource packages such as images or media content from the current XAP.

Figure 6 Extracting an Assembly from a XAP Package

Assembly GetAssemblyFromPackage(string assemblyName, Stream xap) { // Local variables Uri assemblyUri = null; StreamResourceInfo resPackage = null; StreamResourceInfo resAssembly = null; AssemblyPart part = null; // Initialize assemblyUri = new Uri(assemblyName, UriKind.Relative); resPackage = new StreamResourceInfo(xap, null); resAssembly = Application.GetResourceStream(resPackage, assemblyUri); // Extract an assembly part = new AssemblyPart(); Assembly a = part.Load(assemblySri.Stream); return a; }

Summing It Up

Silverlight applications are expected to download and install quickly and run fast on the user's machine. Managed code outperforms interpreted JavaScript, so performance will indeed be snappy. But because downloading large Silverlight applications, or applications that require large external graphic and media content, can create undesired latency, breaking the download into steps helps. It is even better if you program these steps to occur on demand.

The WebClient class offers an effective and asynchronous programming API to download any sort of resources you can access over the Internet. The extensible model for the Silverlight object model allows you to incorporate any changes in the existing structure quickly. Finally, the stream-based API centered around the StreamResourceInfo class makes it easy to extract content from the resources of assemblies and XAP packages.

Any downloaded resource is cached by the browser. This sort of caching is not permanent, though. In Part 2 of this column, I'll use isolated storage to make any downloaded content persist longer on the user's machine, thus saving many potential round-trips. For more Silverlight goodies, see Jeff Prosise's Wicked Code column " Silverlight Tips, Tricks, and Best Practices."

Send your questions and comments for Dino to cutting@microsoft.com.

Dino Esposito is an architect at IDesign and the coauthor of Microsoft .NET: Architecting Applications for the Enterprise(Microsoft Press, 2008). Based in Italy, Dino is a frequent speaker at industry events worldwide. You can join his blog at weblogs.asp.net/despos.