Wicked Code

Silverlight Tips, Tricks, and Best Practices

Jeff Prosise

Code download available at: WickedCode2008_Launch.exe(33860 KB)

Contents

Take Performance Tips to Heart
Offer a Compelling Installation Experience
Don't Make the User Wait
Use CreateFromXaml to Reduce XAML Size
For Accessibility, Support Interactive Zooms
Use Storyboards for Manual Animations
Use the Tag Property to Store Per-Object Data
Use ASP.NET AJAX for Server Callbacks

Unless you've been stuck on a desert island for the past several months, you've undoubtedly heard about Silverlight™, the new cross-browser, cross-platform solution from Microsoft that helps you build rich Internet applications and rich, immersive media experiences in the browser. Version 1.0, which combines a XAML rendering engine with a JavaScript API, shipped in September 2007. Version 2.0, which will enter beta soon, will pair an enhanced XAML engine with a version of the CLR that runs in the browser, a Silverlight version of the Microsoft® .NET Framework Base Class Library (BCL), and a managed API. (That's right: C# in the browser!)

Silverlight 1.0 applications are proliferating on the Web, thanks in part to some of the high-profile companies that adopted it early and helped spread the love. The programming model is simple enough for an experienced developer to begin building Silverlight applications in a matter of hours. What's missing for this still very young platform, however, is that wealth of knowledge and resources that accompanies more mature platforms.

I've been developing Silverlight apps for more than a year now and have put together a list of best practices that I and other developers on my team use to build better applications. This column contains some of the most helpful ones.

Take Performance Tips to Heart

Not long before Silverlight 1.0 shipped, Microsoft published a document titled "Performance Tips for Silverlight-Based Applications" (msdn2.microsoft.com/bb693295). This guide is a compilation of tips for optimizing performance by avoiding some common mistakes. The best practices you'll find include:

  • Use the Visibility property rather than Opacity to hide objects.
  • Don't use the Width and Height properties of MediaElement and Path objects.
  • Detach programmatically registered event handlers after use.
  • Use transparent control backgrounds sparingly.

Most of these make sense when you read them. It's not hard to imagine why it's better to encode videos using the width and height at which you intend to display them (rather than encode them at one scale and display them at another). It's because the Silverlight rendering engine can avoid resizing each frame on the fly. Still, it's good to see them written down and to know that Silverlight doesn't do something fancy to resize video without incurring a performance penalty.

One best practice that I would add to that document is to avoid redundant calls to FindName. I often see event handlers structured this way:

function onClick(sender, args) { var rect = sender.findName('Rect'); rect.fill = 'red'; }

The problem is that FindName has to walk the tree of XAML objects to find its target. If you're going to be referencing the XAML object named "Rect" many times over the lifetime of the app, you should initialize it once—for instance, in an event handler for the root canvas's Loaded event—and store the reference in a global variable. Then, instead of calling FindName every time onClick is called, you can do this:

function onClick(sender, args) { _rect.fill = 'red'; }

The resulting code will be faster than the original. But if a developer does this, he should be sure to release that reference when finished with the plugin. In normal pages this is never an issue, but in an AJAX app where whole pieces may be coming and going, its crucial to release those references to avoid a memory leak. For that reason, the Sys.UI.Silverlight.Control ajax type that is used when using the ASP.NET Silverlight Control (currently available in the Extensions 3.5 Preview Community Technology Preview) provides a pluginDispose method you can override to release references and event handlers.

Offer a Compelling Installation Experience

Too often the Silverlight-based application installation experience is not friendly enough to users who don't have Silverlight installed, as exemplified by the barren landscape displayed in Figure 1. Silverlight.createObjectEx—the function that's typically called to instantiate a Silverlight control—displays a "Get Microsoft Silverlight" button when Silverlight isn't present. Clicking the button transports the user to the Silverlight Web site to download and install it. You can improve the experience somewhat by setting Silverlight.createObjectEx's inplaceInstallPrompt parameter to true, enabling the user to download and install Silverlight without leaving the Web page. But even that leaves something to be desired, especially when Silverlight comprises most or all of the content on the page.

Figure 1 Default Silverlight Installation Experience

Figure 1** Default Silverlight Installation Experience **(Click the image for a larger view)

Microsoft recently published a document that should be required reading for all developers using Silverlight—"Silverlight Installation Experience Guide." It's available for downloading along with sample code at go.microsoft.com/fwlink/?LinkId=106023. The document outlines a general technique for displaying HTML content (including a "Get Microsoft Silverlight" button and browser-specific instructions) in a Silverlight DIV when Silverlight isn't installed, and XAML content when it is. The idea is to paint a picture of what the user would see if Silverlight were installed and hopefully to entice him to click the button.

This column is accompanied by a sample application named RevolvingAuto that demonstrates some of the best practices presented here, including how to build a better installation experience. I'll show you key portions of its source code shortly.

RevolvingAuto is notable for its installation experience. Figure 2 shows what the home page looks like to a visitor who doesn't have Silverlight installed. Behind the "Get Microsoft Silverlight" button is a shaded version of the same user experience the user would see if Silverlight were installed. Because the call to Silverlight.createObjectEx includes an inplaceInstallPrompt="true" parameter, the user can install Silverlight without leaving the Web page. And to top it off, the home page includes logic to create the Silverlight control after installation is complete, eliminating the need for a manual refresh. (This feature works great in Internet Explorer® because it doesn't have to be restarted after Silverlight is installed. It's powerless in browsers that require a restart, unfortunately.)

Figure 2 RevolvingAuto before Silverlight Is Installed

Figure 2** RevolvingAuto before Silverlight Is Installed **(Click the image for a larger view)

The JavaScript that drives the install experience lives within Default.html (see Figure 3). To create a Silverlight control, most Silverlight applications structure the code and markup like this:

Figure 3 RevolvingAuto Application—Default.html

<html xmlns="https://www.w3.org/1999/xhtml"> <head> <title>Revolving Auto</title> <script type="text/javascript" src="Silverlight.js"></script> <script type="text/javascript" src="Default.html.js"></script> <style> .AgInstalled { margin-left: auto; margin-right: auto; width: 612px; height: 700px; text-align: left } .AgNotInstalled { margin-left: auto; margin-right: auto; width: 612px; height: 700px; text-align: left; background-image: url(Images/Install.jpg); background-repeat: no-repeat; padding-top: 100px } </style> </head> <body style="background-color: black; text-align: center"> <div style="height: 40px"></div> <div id="Container" class="AgInstalled"> <div id="SilverlightPlugInHost" style="padding-left: 200px"> </div> </div> <script type="text/javascript"> var _id; if (!Silverlight.isInstalled('1.0')) { document.getElementById('Container').className = 'AgNotInstalled'; _id = window.setTimeout('checkInstall()', 3000); } else document.getElementById ('SilverlightPlugInHost') .removeAttribute('style'); createSilverlight(); function checkInstall() { if (Silverlight.isInstalled('1.0')) { window.clearInterval(_id); document.getElementById('Container') .className = 'AgInstalled'; document.getElementById ('SilverlightPlugInHost') .removeAttribute('style'); createSilverlight(); } } </script> </body> </html>

<div id="SilverlightPlugInHost"> <script type="text/javascript"> createSilverlight(); </script> </div>

RevolvingAuto structures it this way instead:

<div id="Container"> <div id="SilverlightPlugInHost"> </div> </div> <script type="text/javascript"> // Code to create the control </script>

The code inside the <script> element calls Silverlight.isInstalled (which is implemented in Silverlight.js along with Silverlight.createObjectEx) to determine whether Silverlight is installed. If the answer is yes, the script calls createSilverlight to create the control. If the answer is no, the script dynamically styles the DIV containing the Silverlight DIV to show a background image, and then calls createSilverlight to display the "Get Microsoft Silverlight" button. The background image is the one behind the button in Figure 2.

If it determines that Silverlight is not installed, the creation script also uses window.setTimeout to set up calls to a local function named checkInstall every three seconds. Once installation is complete, checkInstall removes the background image, clears the timer so the function won't be called again, and calls createSilverlight again to create the Silverlight control.

Don't Make the User Wait

The hallmark of Silverlight 1.0 is a rich media experience for images, audio, and video. But files such as these can be large (very large!), and if you declare Image or MediaElement objects in XAML and declare media to go along with them, the download time can be tremendous. The problem is that Silverlight doesn't display one speck of XAML until all of the XAML and the assets that it references have been downloaded. The user, meanwhile, is left wondering what's going on—or if anything is going on at all. It's not a big deal if the user waits a couple of seconds, but if seconds become minutes, the user is likely to give up and move on to better-built sites.

That's why well-designed Silverlight apps don't statically link to media assets; instead, they use the built-in Silverlight downloader object, which piggybacks on the browser's XmlHttpRequest stack to download those assets asynchronously. Instead of doing this

<MediaElement Source="FunnyVideo.wmv" />

you declare the MediaElement without a Source attribute, which prevents the Silverlight control from waiting for a video to download before rendering any XAML. Then you create a downloader object and asynchronously download the video, calling the MediaElement's SetSource method to pass the video to the MediaElement once the download has finished. If you like, you can update a progress indicator here as well. The downloader fires downloadProgressChanged and Completed events to aid in the process.

Silverlight also makes it easy to download multiple assets rather than just one. Just package all your assets—audio, video, images, even XAML or XML—in a ZIP file and use the downloader to download it. Then, when you call SetSource to assign content to an object, specify the name of the file inside the ZIP file in the second parameter. It couldn't be much simpler.

Here's an example:

downloader.open('GET', 'Assets.zip'); downloader.send(); ... // In the Completed event handler _media.setSource(sender, 'FunnyVideo.wmv');

The RevolvingAuto application uses a downloader object to asynchronously download a ZIP file containing 105 image files. Together, these files comprise a 360-degree view of an automobile at 3.5-degree increments. At run time, the application swaps images in and out to revolve the automobile.

The DownloadProgressChanged event handler updates a progress bar, which is nothing more than a XAML rectangle whose width increases as the download progresses. The Completed event handler assigns the downloaded images to dynamically created Image objects. It also hides the progress bar and unregisters the two downloader event handlers in accordance with the performance document referenced in the first section of this article.

Speaking of not making the user wait, most browsers drive the UI and execute JavaScript on the same thread. Consequently, if you engage in a long-running JavaScript task (say, some number crunching that takes 5 or 10 seconds to execute), the UI will be unresponsive for the duration. The aforementioned performance-tips document recommends breaking long-running JavaScript tasks into smaller, more granular tasks for this reason. In Silverlight 2.0, you'll be able to delegate long-running tasks to background threads. In version 1.0, however, that's not an option.

Use CreateFromXaml to Reduce XAML Size

Another takeaway from the sample application is how it uses CreateFromXaml to create XAML Image objects dynamically rather than cluttering the XAML file with scores of nearly identical Image declarations. The XAML file you see in Figure 4, Scene.xaml, is nearly empty. It declares a couple of Canvases and Rectangles, a Storyboard, and a ScaleTransform, but it doesn't declare a single Image object. Instead, a for loop in Default.html.js creates 105 Image objects—one per image in the animation—and initializes them with image bits from the downloader (see Figure 5).

Figure 5 RevolvingAuto Application —Default.html.js

function createSilverlight() { Silverlight.createObjectEx({ source: 'Scene.xaml', parentElement: document.getElementById('SilverlightPlugInHost'), id: 'SilverlightPlugIn', properties: { width: '900', height: '700', background:'black', isWindowless: false, inplaceInstallPrompt: true, version: '1.0' }, events: { onError: null, onLoad: null }, context: null }); } var _zoom = 1; // Zoom factor var _index = 0; // Image index var _progressBar; // Progress bar var _progressBarContainer; // Progress bar container var _progressBarCanvas; // Progress bar canvas var _progressBarWidth; // Full width of progress bar var _control // Silverlight control var _transform; // Zoom transform var _timer; // Timer storyboard var _token1, _token2; // Event handler tokens var _images = new Array(36); // References to XAML images var _photos = 105; // Number of photos function onLoaded(sender) { // Initialize XAML references and other variables transform = sender.findName('ZoomTransform'); timer = sender.findName('TimerStoryboard'); progressBar = sender.findName('ProgressBar'); progressBarContainer = sender.findName('ProgressBarContainer'); progressBarCanvas = sender.findName('ProgressBarCanvas'); progressBarWidth = _progressBarContainer.width - 4; control = sender.getHost(); var downloader = _control.createObject('downloader'); token1 = downloader.addEventListener('downloadProgressChanged', downloadProgressChanged); token2 = downloader.addEventListener('completed', downloadCompleted); downloader.open('GET', 'Assets/AutoPhotos.zip'); downloader.send(); } function downloadProgressChanged(sender, args) { // Update the progress bar _progressBar.width = _progressBarWidth * sender.downloadProgress; } function downloadCompleted(sender, args) { // Hide the progress bar progressBarCanvas.visibility = 'Collapsed'; // Deregister downloader event handlers sender.removeEventListener('downloadProgressChanged', _token1); sender.removeEventListener('completed', _token2); // Create XAML images and assign downloaded bits to them var canvas = sender.findName('AutoCanvas'); for (i=0; i<_photos; i++) { var xaml = '<Image Canvas.Left="225" Width="450" Visibility="Collapsed" />'; var image = _control.content.createFromXaml(xaml); image.setSource(sender, (i + 1).toString() + '.JPG'); canvas.children.add(image); images[i] = image; } // Register mousewheel event handler if (window.addEventListener) window.addEventListener('DOMMouseScroll', onMouseWheelTurned, false); else window.onmousewheel = document.onmousewheel = onMouseWheelTurned; // Make the first image visible images[0].visibility = 'Visible'; // Start rotating timer.begin(); } function onTick(sender, args) { // Hide the current image images[_index].visibility = 'Collapsed'; // Update the image index if (++_index == _photos) index = 0; // Wrap around // Show the new image images[_index].visibility = 'Visible'; // Restart the timer timer.begin(); } function onMouseWheelTurned(event) { var delta = 0; if (!event) // Internet Explorer event = window.event; if (event.wheelDelta) // Internet Explorer { delta = event.wheelDelta; if (window.opera) delta = -delta; } else if (event.detail) // Mozilla delta = -event.detail; if (delta != 0) { if (delta > 0) { // Zoom in zoom = Math.min(2, _zoom + 0.05); } else { // Zoom out zoom = Math.max(1, _zoom - 0.05); } transform.scaleX = _transform.scaleY = _zoom; } if (event.preventDefault) event.preventDefault(); event.returnValue = false; }

Figure 4 RevolvingAuto Application—Scene.xaml

<Canvas xmlns="https://schemas.microsoft.com/client/2007" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" Loaded="onLoaded"> <Canvas x:Name="ProgressBarCanvas" Canvas.Left="260" Canvas.Top="100"> <Rectangle x:Name="ProgressBarContainer" Canvas.Left="0" Canvas.Top="0" Width="380" Height="12" Stroke="#606060" /> <Rectangle x:Name="ProgressBar" Canvas.Left="2" Canvas.Top="2" Width="0" Height="8" Fill="#303030" /> </Canvas> <Canvas x:Name="AutoCanvas"> <Canvas.Resources> <Storyboard x:Name="TimerStoryboard" Duration="0:0:0.2" Completed="onTick" /> </Canvas.Resources> <Canvas.RenderTransform> <ScaleTransform x:Name="ZoomTransform" CenterX="450" /> </Canvas.RenderTransform> </Canvas> </Canvas>

It's always advisable to keep XAML files as small as possible to reduce the application's load time. And it's no slower. (Parsing XML is, after all, not an inexpensive proposition.) Anytime you find yourself declaring XAML objects repetitively, consider using CreateFromXaml instead.

For Accessibility, Support Interactive Zooms

"Section 508" is a buzzword in usability circles these days and for good reason: it spells out U.S. Federal requirements for making software accessible to people with disabilities. If you haven't been asked about 508 compliance yet, just wait—if you live in the U.S., you'll need to know about it and probably sooner than you think. (For details regarding Section 508, see section508.gov.)

Section 508 covers a lot of ground, from requiring that information conveyed by color coding also be accessible by means other than color coding (for example, through labels or popup tooltips) to imposing upper and lower limits on the frequency of page flicker. Strangely enough, it doesn't specify a minimum font size to aid vision-impaired users. Text size strikes close to home for me, because I've reached an age where I have to use reading glasses to read small print. That's why I often go ahead and build a zoom feature into the Silverlight apps that I author. Fortunately, Silverlight makes my job very easy.

Silverlight ScaleTransforms can be used to scale any XAML object, including Canvas objects containing other objects. Implementing interactive zooms is as simple as declaring a ScaleTransform and modifying its ScaleX and ScaleY properties in response to mouse-wheel actions or other events. To scale the entire Silverlight display, simply apply the ScaleTransform to the root Canvas.

When RevolvingAuto's home page first appears, the rotating automobile images are relatively small. However, you can enlarge the display up to two times by rolling the mouse wheel (see Figure 6). The key code is found in the onMouseWheelTurned function in Figure 6 (Default.html.js). It does some work to account for differences in the way browsers report mouse-wheel events and then increments or decrements the ScaleX and ScaleY properties of a ZoomTransform.

Figure 6 RevolvingAuto before and after Zooming

Figure 6** RevolvingAuto before and after Zooming **(Click the image for a larger view)

Observe that you don't lose resolution when you zoom in on the rotating car. That's because the downloaded images are twice the size of the Image objects that they're assigned to. As you zoom, the ScaleTransform increases the dimensions of the currently displayed Image object, allowing the Image object more real estate to display the information available to it.

Use Storyboards for Manual Animations

One of the coolest features of Silverlight is its rich support for animations. You can make objects fade in and out, zoom across the page, and pop in and out of view with a few lines of XAML. But you can't animate just anything, at least not declaratively. You can easily animate numeric properties, Color properties, and Point properties. But if you want to, say, animate an image by varying its Source property over time (swapping one image for another on each tick), you'll have to write some code, and the way you structure that code will affect the quality of the animation.

Silverlight 1.0 lacks an explicit timer object, and window.setTimeout is less than ideal for animations. That's why savvy Silverlight developers use Storyboard objects when they need programmable timers. All you have to do is declare an empty Storyboard object in XAML and designate a handler for its completed event, like so:

<Storyboard x:Name="Timer" Duration="0:0:0.05" Completed="onTick"> </Storyboard>

When you're ready to start the animation, you call the Storyboard's begin method:

_timer.begin();

Finally, in the Completed event handler, do whatever you need to do (for example, modify an Image object's Source property) and call begin to start the timer running again:

function onTick(sender, args) { // TODO: Do work _timer.begin(); }

In this example, _timer is a variable that refers to the XAML Storyboard object named "Timer."

This is precisely the technique that RevolvingAuto uses to render revolving animations. The onTick function in Figure 5, which is called in response to Storyboard.Completed events, advances the animation one frame by turning the previous image's Visibility property off (Collapsed) and the next image's Visibility property on (Visible).

Use the Tag Property to Store Per-Object Data

Every XAML object that a Silverlight application creates has a read/write property named Tag that can be used to store user-defined strings. The Tag property provides a powerful and easy-to-use means for associating arbitrary data with XAML objects.

Members of my team and I have used Tag in a variety of ways. In one project whose requirements included giving the user the ability to search through content rendered by Silverlight, we embedded keywords in the page's XAML objects and built a search feature that scanned the XAML from top to bottom inspecting each object's Tag property. In another instance, we used Tag to include metadata in our XAML elements. At run time we used the metadata, which was stored in the form of name/value pairs, to build more complex XAML around the XAML elements in the XAML document. A designer building content for our application could, for example, declare an Image object this way:

<Image Source="ProductDemo.jpg" Tag="Mode:Lightbox" />

Our application would see "Mode:Lightbox" and wrap a lightbox around the image, enabling users to zoom in and out, pan around the image, and more.

One very pragmatic use for the Tag property is as a means for attaching descriptions to images. Imagine a canvas containing hundreds of images, accompanied by a requirement to pop up an image description when the cursor hovers over one. You could use the Tag property to attach descriptions to each image and read the Tag property of the image under the cursor prior to popping up a description.

Use ASP.NET AJAX for Server Callbacks

Developers sometimes bemoan the fact that Silverlight 1.0 has such a limited networking stack. Other than the downloader object, version 1.0 offers little in terms of networking. This will change in Silverlight 2.0, which will have a rich, multiprotocol networking stack. Until then, however, you can use ASP.NET AJAX to fire off calls from a browser hosting a Silverlight application to the server.

The Silverlight viewer in the MyComix comic-cataloging application I recently published offers a concrete example. To see for yourself, type in the URL mycomix.wintellect.com/Spotlight.aspx?Item=1166 and wait for a comic book cover to appear. Then move the cursor into the upper part of the window; an info panel will slide into view to display information about the comic, including its title, issue number, grade, and year of publication. The data in the panel comes from a database stored on the server, and it's retrieved by calling an ASP.NET AJAX Web service.

The Web service, MyComix.asmx, exposes a method named GetComicInfo that accepts a comic ID as an input parameter and returns a ComicInfo object containing all the information available regarding the comic. (An ASP.NET AJAX Web service is basically a conventional ASMX with the Web service class attributed ScriptService rather than WebService.) The viewer page declares a reference to the Web service like this:

<asp:ScriptManager ID="ScriptManager1" runat="server"> <Services> <asp:ServiceReference Path="~/MyComix.asmx" /> </Services> </asp:ScriptManager>

Then, it fires an asynchronous call to the Web service's GetComicInfo method, as shown here:

MyComixScriptService.GetComicInfo(_id, OnGetComicInfoCompleted);

The call is extremely efficient on the wire because it uses JavaScript Object Notation (JSON) encoding and because no view state or other unnecessary data is transmitted. And because the call is asynchronous, the browser doesn't freeze up waiting for it to complete. If the user displays the info panel before the call returns, the panel simply shows up empty.

You can download the MyComix source code, including the ASP.NET AJAX Web service, from wintellect.com/Downloads/MyComix.zip. The Silverlight viewer can be found in the files that are named Spotlight.aspx and Spotlight.xaml.

As you're playing around with Silverlight you may find you have best practices of your own and would like to add them to those presented here. If so, shoot me some e-mail at wicked@microsoft.com. Meanwhile, happy Silverlight developing!

Send your questions and comments for Jeff to wicked@microsoft.com.

Jeff Prosise is a contributing editor to MSDN Magazine and the author of several books, including Programming Microsoft .NET (Microsoft Press, 2002). He's also cofounder of Wintellect (www.wintellect.com), a software consulting and education firm that specializes in Microsoft .NET. Have a comment on this column? Contact Jeff at wicked@microsoft.com.