Extreme ASP.NET

Encapsulate Silverlight with ASP.NET Controls

Fritz Onion

Code download available at:  Extreme ASP NET 2008_01.exe(700 KB)

Contents

Using Silverlight
Building a Custom Silverlight Control
ASP.NET AJAX and IScriptControl
The asp:Xaml and asp:Media Controls

Many ASP.NET developers around the world are wondering how and where they should incorporate SilverlightTM into their applications. The answer isn't obvious because there is a wide range of approaches you can take. You can make your entire page a Silverlight control and do everything there or, the more likely approach, you can identify portions of your pages that would benefit from Silverlight and integrate rich UI elements where they add the most benefit, leaving the underlying application structure the same.

The latter approach is sometimes referred to as "adding islands of richness" to your pages. And with Silverlight, those islands have many possible bridges to the surrounding content via scriptable methods and events.

In this month's column, I'm going to explore techniques for building custom server controls that encapsulate Silverlight content. Wrapping Silverlight content into a custom ASP.NET control has several advantages that make it an especially appealing technique. It makes integrating Silverlight content as easy as using any other server-side control, which greatly increases the chances of adoption. The process of adding handlers, setting properties, and calling methods on the Silverlight control is exactly the same as with any other control. And this approach keeps pages free from the clutter of Silverlight-specific JavaScript, making it easier to maintain and deploy.

Using Silverlight

Before I jump into the details of building a custom control to host Silverlight content, I should describe precisely what needs to be in place for Silverlight content to render to the client. So I will begin by showing you how to host a XAML file in an ASP.NET page and add interaction between the page and the rendered Silverlight content. It will be clearer how to put the pieces of the control together once I have a page that exhibits the behavior I ultimately want to encapsulate with a control.

The first step is to write a XAML file to be rendered by Silverlight. My goal is to keep the XAML simple enough to be unobtrusive in presenting the control but complex enough to be interesting. So I have chosen to render a single sphere with a title. The XAML shown in Figure 1 defines a TextBlock used as a title and an Ellipse painted with a radial gradient brush. This XAML also defines two Storyboards in the resources portion of the Canvas, describing two animations that will cause the sphere to grow and shrink, respectively, over durations of two seconds each. Ultimately, I want to host this XAML in Silverlight and then use JavaScript handlers to set the title text and invoke the animations as the user clicks on the rendered sphere.

Figure 1 Sphere.xaml File

<!-- File: Sphere.xaml -->
<Canvas
    xmlns="https://schemas.microsoft.com/client/2007"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Width="300" Height="300"
    Background="White"
    >
  <Canvas.Resources>
    <Storyboard x:Name="growAnimation">
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Width)"
        To="250" Duration="0:0:2" />
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Height)"
        To="250" Duration="0:0:2" />
    </Storyboard>
    <Storyboard x:Name="shrinkAnimation">
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Width)"
        To="200" Duration="0:0:2" />
      <DoubleAnimation
        Storyboard.TargetName="ellipse"
        Storyboard.TargetProperty="(Ellipse.Height)"
        To="200" Duration="0:0:2" />
    </Storyboard>
  </Canvas.Resources>

  <TextBlock x:Name="titleText" Width="200" Height="24" 
        Canvas.Left="94" Text="[Title]" TextWrapping="Wrap" />

  <Ellipse Width="200" Height="200" 
     x:Name="ellipse" Canvas.Left="47" Canvas.Top="41">
    <Ellipse.Fill>
      <RadialGradientBrush GradientOrigin="0.75,0.25" 
         Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
        <GradientStop Color="Yellow" Offset="0" />
        <GradientStop Color="Green" Offset="1" />
      </RadialGradientBrush>
    </Ellipse.Fill>
  </Ellipse>

</Canvas>

To incorporate this XAML into a Web page, I first need to create the Silverlight plug-in. The simplest way to do this is to add a reference to the Silverlight.js script file that is included with the Silverlight 1.0 SDK. This file defines a method called Silverlight.createObject, which you can use to create the plug-in. The Silverlight plug-in needs to be associated with an HTML element on the page (typically a <div>), so it is common practice to define the <div> element and place the call to createObject within a script block embedded within the <div>.

Figure 2 shows how to create an instance of the Silverlight control and associate with a <div> element. This page assumes that the Sphere.xaml file shown in Figure 1 is in the same directory as the page itself. When viewed in your Web browser, this page displays a lovely green sphere, as shown in Figure 3.

Figure 2 Create and Associate Silverlight Control

<%@ Page Language="C#" %>

<html xmlns="https://www.w3.org/1999/xhtml" >
<head runat="server">
    <script type="text/javascript" src="silverlight.js"></script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="slControlHost">       
    <script type="text/javascript">     
        Silverlight.createObject(
        "Sphere.xaml",
        document.getElementById('slControlHost'), "slControl",
        { width:'300', height:'300', version:'1.0' },
        { onError:null, onLoad:null }, 
        null);
    </script>
    </div>
    </form>
</body>
</html>

Figure 3 Control Displaying a Green Sphere

Figure 3** Control Displaying a Green Sphere **

The next task is to interact with the XAML so that I can programmatically change the title text and initiate the grow and shrink animations I described earlier. The simplest way to do this is to declaratively assign handlers to events on objects defined in the XAML. In this example, the grow and shrink animations occur in response to the user clicking the sphere—which animation it runs depends on the current size of the sphere.

To begin, I add a handler to the MouseLeftButtonDown event of the Ellipse that defines the sphere, and then I place the corresponding method in JavaScript on the page. Similarly, I handle the Loaded event of the TextBlock to set the text displayed above the sphere to a custom string. The handlers that I add to the XAML look like this:

<Ellipse MouseLeftButtonDown="javascript:onSphereButtonDown" ...
<TextBlock Loaded="javascript:onTextLoaded" ...

And the corresponding JavaScript methods that I add to a new script block on the ASP.NET page are shown in Figure 4.

Figure 4 JavaScript Methods Added to New Script Block

<script type="text/javascript">
    function onSphereButtonDown(sender, args)
    {
        // Run the grow or shrink animation, 
        // depending on whether it is currently
        // 'grown' or 'shrunk'
        var animationName = (sender.Width==200) ? 
              "growAnimation" : "shrinkAnimation";
        
        var sl = sender.getHost();
        var animation = sl.content.findName(animationName);
        if (animation)
          animation.begin();
    }
    
    function onTextLoaded(sender, args)
    {
      sender.Text = "My Growing Sphere";
    }
</script>

That is all it takes to manually incorporate Silverlight content into an ASP.NET page. When this page is displayed in a browser, the title text says "My Growing Sphere". When the user clicks on the sphere, it grows on the first click and shrinks on the second. As you can see, it is relatively easy to integrate Silverlight content into an ASP.NET page, and it is easy to manipulate since its entire object model is exposed to script on the page.

For a simple element like my sphere, however, you may want to package it as a custom control that someone can incorporate into an application simply by dragging the control onto the design surface of a page. This is where encapsulating Silverlight comes into play.

Building a Custom Silverlight Control

I begin the task of encapsulating Silverlight by creating a new custom ASP.NET control:

namespace MsdnMagazine
{
    public class SilverlightSphere  : Control
    {
    }
}

In this sample, I derive from System.Web.UI.Control. I've chosen not to derive from WebControl since the additional style properties defined by that class are not applicable to this control.

Next, I need to render the JavaScript handlers that I included manually. The easiest way to incorporate JavaScript in an ASP.NET control is to define a JavaScript file and embed it as a resource in the compiled assembly. I can do this by moving our two event handlers into a separate JavaScript file, which I call SilverlightSphere.js:

   // File: SilverlightSphere.js
   function onSphereButtonDown(sender, args)
   {
       // Same implementation as before
   }

   function onTextLoaded(sender, args)
   {
      // Same implementation as before
   }

Next, I embed my new SilverlightSphere.js file and the original Silverlight.js file as resources in the control project. This is so that the control can be deployed as a single assembly without any dependent files. It also makes sense to embed the XAML file as a resource and reference both it and the two JavaScript files using the WebResource.axd handler mechanism in ASP.NET for extracting embedded resources.

After I set the Build Action to Embedded Resource for all three files in Visual Studio® (/res from the command line compiler), I then use the assembly-level attribute System.Web.UI.WebResource to grant permission for these resources to be served by WebResource.axd and to associate a MIME type for the response. I use the following declarations to accomplish this (where SlSphere is the name of the control project):

[assembly: WebResource("SlSphere.SilverlightSphere.js", 
                       "text/javascript")]
[assembly: WebResource("SlSphere.Silverlight.js", 
                       "text/javascript")]
[assembly: WebResource("SlSphere.Sphere.xaml", "text/xml")]

The JavaScript files are now compiled into my assembly as embedded resources. So now I can use the RegisterClientScriptResource method of the ClientScriptManager class (accessed through Page.ClientScriptManager) to cause the rendered page to include references to the files. I call this method for each file in an override of the OnInit method inherited from the Control base class:

protected override void OnInit(EventArgs e)
{
  Page.ClientScript.RegisterClientScriptResource(this.GetType(),
                      "SlSphere.Silverlight.js");
  Page.ClientScript.RegisterClientScriptResource(this.GetType(), 
                      "SlSphere.SilverlightSphere.js");

  base.OnInit(e);
}

The last and most important piece of the control is to implement the virtual Render method. There are two things I have to accomplish in this method. First, I need to render a <div> tag to act as the host HTML element for the Silverlight plug-in. Second, I must render embedded script to create the Silverlight plug-in.

The reference to the XAML file is generated with a call to the ClientScriptManager class's GetWebResourceUrl. This will ensure that the Silverlight control is initialized with the content of the XAML file I embedded as a resource in the assembly. Finally, I need to specify a unique identifier for the Silverlight plug-in itself. In this case, I do this by taking the ID of the control (this.ClientID) and concatenating "_ctrl" to the end (see Figure 5).

Figure 5 Implement the Virtual Render Method

Const string _silverlightCreateScript = @"Silverlight.createObject(
  '{0}, document.getElementById('{1}'), '{2},
  {{ width:'300', height:'300', version:'1.0' }},
  {{ onError:null, onLoad:null }},
  null);";     

protected override void Render(HtmlTextWriter writer)
{
  string script = string.Format(_silverlightCreateScript,
      Page.ClientScript.GetWebResourceUrl(GetType(), 
               "SlSphere.Sphere.xaml"),
               this.ClientID, this.ClientID + "_ctrl");

  writer.AddAttribute(HtmlTextWriterAttribute.Id, 
                      this.ClientID);
  writer.RenderBeginTag(HtmlTextWriterTag.Div);
  writer.AddAttribute(HtmlTextWriterAttribute.Type, 
                      "text/javascript");
  writer.RenderBeginTag(HtmlTextWriterTag.Script);

  writer.Write(script);

  writer.RenderEndTag();
  writer.RenderEndTag();
     
  base.Render(writer);
}

I now have a server-side control (complete with embedded JavaScript and XAML resources) that encapsulates the pieces necessary to deploy Silverlight on an ASP.NET page. I can create an instance of this control on a page with the following declarations:

<%@ Register Assembly="SlSphere" Namespace="MsdnMagazine" 
             TagPrefix="csc" %>
...
<csc:SilverlightSphere ID="_silverlightSphere" runat="server" />

What I am able to do here is compelling—any ASP.NET application can easily incorporate a growing and shrinking sphere hosted in Silverlight, yet there's no need to worry about the details of hosting Silverlight content.

You could, of course, build more interesting and complex controls with sophisticated XAML renderings. However, when you start to think about things you might want to add to your controls, you quickly encounter the problem of how to instrument the XAML with property values from the server control. For example, say I want to expose a Title property in my control and have that map to the Text property of the titleText element in my XAML file. There is no obvious way to accomplish this cleanly with the current implementation.

Another drawback to the current implementation is that the XAML is fixed as an embedded resource. While this is convenient for deployment, one of the main benefits of Silverlight and XAML is the separation of behavior from design, which allows the XAML to be replaceable. I could define a XamlUrl property on the control to allow overriding of the embedded XAML content. This approach wouldn't be difficult, but it would introduce fragility in exposing the names of the JavaScript handlers in the XAML, which would be required to ensure that the designers have the latest version of the XAML.

ASP.NET AJAX and IScriptControl

The crux of the problem is figuring out how to introduce server-side properties into client-side script. Fortunately, ASP.NET AJAX extensions provide a clean way of tying server-side control properties to client-side elements through the IScriptControl interface, which is defined in the System.Web.Extensions assembly. This interface defines two methods: GetScriptReferences and GetScriptDescriptors. When implemented and tied to the page's ScriptManager control, these methods generate references to JavaScript files and declare instances of JavaScript classes, respectively:

public interface IScriptControl
{
  IEnumerable<ScriptDescriptor> GetScriptDescriptors();
  IEnumerable<ScriptReference> GetScriptReferences();
}

You can think of a descriptor as a client-side surrogate of the server-side control, with properties initialized to those defined by the server control. This provides a link between server control properties and client properties which can be consumed by Silverlight and JavaScript handlers. Descriptors also provide a clean scoping mechanism for isolating all of the control's client-side features under a namespace and class (at least as ASP.NET AJAX client libraries define namespaces and classes in JavaScript).

When building a custom ASP.NET AJAX control that hosts Silverlight content, I first create the client-side class definition with methods and properties defined to interact with the Silverlight control. It makes sense to add both a Title and a XamlUrl property to this control, so I start by defining the class with these properties, naming the class MsdnMagazine.SilverlightAjaxSphere:

// File: SilverlightAjaxSphere.js
Type.registerNamespace('MsdnMagazine');

MsdnMagazine.SilverlightAjaxSphere = function(element) {
    MsdnMagazine.SilverlightAjaxSphere.initializeBase(
               this, [element]);

    this._title = null;
    this._xamlUrl = null;
}

Now I need to populate the methods for the prototype of the class, beginning with initialize (see Figure 6). Instead of embedding script inside a <div> element, as I did in the first control implementation, this time I create the Silverlight plug-in within the initialize method of the client-side class. Note that this is only possible since the _xamlUrl property I defined in the class will be populated with the server-side value prior to this method being called. I can also access the identifier of the <div> element to be rendered by the server-side control with the get_id method inherited from the Sys.UI.Control base class.

Figure 6 Populate the Methods for Class Prototype

// File: SilverlightAjaxSphere.js
MsdnMagazine.SilverlightAjaxSphere.prototype = {
    initialize : function() {
      MsdnMagazine.SilverlightAjaxSphere.callBaseMethod(
             this, 'initialize');
      var hostId = this.get_id() + 'Host';
      
      Silverlight.createObject(this._xamlUrl, 
        $get(this.get_id()), hostId,            
        { width:'300', height:'300', version:'1.0' },
        { onError:null, 
          onLoad:Function.createDelegate(this, this._onXamlLoaded) }, 
        null);   
         
    },

    dispose : function() {
        MsdnMagazine.SilverlightAjaxSphere.callBaseMethod(this, 
              'dispose');
    },
    
    // Once called, Silverlight control is initialized and 
    // Xaml elements can be accessed.
    _onXamlLoaded : function(root) {
      var root = $get(this.get_id() + 'Host');
      var sphere = root.content.findName('ellipse');

      if (sphere) {
        sphere.addEventListener('MouseLeftButtonDown',
                    Function.createDelegate(this, 
                     this._onSphereButtonDown));
      }        
      else
        throw Error.invalidOperation(
              "You must have an ellipse element.");
        
      var title = root.content.findName('titleText');
      if (title)
        title.Text = this._title;    
    },   
    
     _onSphereButtonDown : function(sender, args)
    {
      // same implementation as before
    },

    // property accessors
    //
    get_title : function() {
        return this._title;
    },

    set_title : function(value) {
        if (this._title !== value)
            this._title = value;            
    },
    
    get_xamlUrl : function() {
      return this._xamlUrl;
    },
    
    set_xamlUrl : function(value) {
      if (this._xamlUrl !== value) 
        this._xamlUrl = value;
    }
}

MsdnMagazine.SilverlightAjaxSphere.registerClass(
      'MsdnMagazine.SilverlightAjaxSphere', Sys.UI.Control);

if (typeof(Sys) !== 'undefined')  
  Sys.Application.notifyScriptLoaded();

When the initialize method is called on my class, the Silverlight plug-in is just being created and the XAML has not yet been evaluated. I therefore use the onLoad event handler defined in the createObject method of Sys.Silverlight to specify a callback to be invoked once the XAML is loaded—this allows me to wire up the event handlers and initialize properties on individual XAML elements. This handler, which I have called _onXamlLoaded, is passed a reference to the root Silverlight element, which I can use to retrieve the named elements in the XAML.

You should be as accommodating as possible when interacting with named XAML elements, which entails checking to see if they exist before actually modifying their content or wiring up event handlers. If the element is missing and is not crucial to the operation of the control (like the titleText element in my control's XAML), just skip the initialization steps for that element. This way, a designer can elect to remove certain elements from the XAML and the control will still work.

The rest of the class's implementation defines the property accessors for title and xamlUrl and implements the _onSphereButtonDown function to initiate the grow and shrink animations, as I did before. This time, however, the method is encapsulated in a class.

The final step in the client-side script is to register the new class, specifying the base class as Sys.UI.Control.

The remaining task is to build the server-side control that uses this class via the IScriptControl interface (see Figure 7). Like the client-side class, the server control exposes two properties: Title and XamlUrl. (These are backed by ViewState, as all server-side control properties should always be.) I initialize the XamlUrl property to the URL, referencing the embedded XAML file using the same GetWebResourceUrl method I used in the last control implementation. By setting this property in an override of OnInit, I ensure that the property will default to this URL, but any overridden version specified by the client will always take priority since state is loaded later in the lifecycle.

Figure 7 Server-Side Control

// File: SilverlightAjaxSphere.cs
//
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;

[assembly: WebResource("SlAjaxSphere.SilverlightAjaxSphere.js", 
                       "text/javascript")]
[assembly: WebResource("SlAjaxSphere.Silverlight.js", 
                       "text/javascript")]
[assembly: WebResource("SlAjaxSphere.Sphere.xaml", "text/xml")]
 
namespace MsdnMagazine
{
  public class SilverlightAjaxSphere  : Control, IScriptControl
  {
    public string Title
    {
      get { return (string)(ViewState["title"] ?? "[title]"); }
      set { ViewState["title"] = value; }
    }

    public string XamlUrl
    {
      get { return (string)(ViewState["xamlurl"] ??
                            string.Empty); }
      set { ViewState["xamlurl"] = value; }
    }

    protected override void OnInit(EventArgs e)
    {
      // Default the XamlUrl to my embedded resource. 
      // If this is set in the
      // markup explicitly, it will be overridden.
      XamlUrl = Page.ClientScript.GetWebResourceUrl(GetType(), 
                   "SlAjaxSphere.Sphere.xaml");

      base.OnInit(e);
    }

    public IEnumerable<ScriptReference> GetScriptReferences()
    {
      ScriptReference sr1 = new ScriptReference(
        " SlAjaxSphere.Silverlight.js",
        GetType().Assembly.FullName);
      ScriptReference sr2 = new ScriptReference(
        " SlAjaxSphere.SilverlightAjaxSphere.js",
        GetType().Assembly.FullName);

      return new ScriptReference[] { sr1, sr2 };
    }

    public IEnumerable<ScriptDescriptor> GetScriptDescriptors()
    {
      ScriptControlDescriptor scd = new ScriptControlDescriptor(
                 "MsdnMagazine.SilverlightAjaxSphere",
                 this.ClientID);
      scd.AddProperty("title", this.Title);
      scd.AddProperty("xamlUrl", this.XamlUrl);

      return new ScriptDescriptor[] { scd };
    }

    protected override void OnPreRender(EventArgs e)
    {
      if (!DesignMode)
      {
        ScriptManager sm = ScriptManager.GetCurrent(Page);
        if (sm == null)
          throw new HttpException(
              "A ScriptManager control must exist on the current page.");
        sm.RegisterScriptControl(this);
        sm.RegisterScriptDescriptors(this);
      }

      base.OnPreRender(e);
  }
  protected override void Render(HtmlTextWriter writer)
  {
      writer.AddAttribute(HtmlTextWriterAttribute.Id, 
                this.ClientID);
      writer.RenderBeginTag(HtmlTextWriterTag.Div);
      writer.RenderEndTag();
     
      base.Render(writer);
    }
  }
}

The next two methods, GetScriptDescriptors and GetScriptReferences, contain the implementation of the IScriptControl interface. Since I am embedding the JavaScript files as resources in the assembly, I use the overloaded constructor of ScriptReference, which takes the resource name and assembly name. This generates a reference to the embedded script resources in much the same way RegisterClientScriptResource did in the previous control implementation. GetScriptDescriptors is the key to creating an instance of the client-side MsdnMagazine.SilverlightAjaxSphere class initialized with properties from the server-side control.

The ScriptControlDescriptor class takes the name of the client-side class and the ID of the associated client-side element in its constructor. I then use the AddProperty method of the class to specify the initial value of each property of the new client-side class. By passing in the current property values of my control, I ensure that the client-side class will be created with the current property values defined in my control.

I still need to take care of one last bit of housekeeping in an override of OnPreRender, where I register the control as a script control with the current ScriptManager on the page (throwing an exception if there is no ScriptManager) as well as with the script descriptors. This causes the ScriptManager to invoke the two methods of IScriptControl on the control to pull in the script references and create the initial client-side class. The Render method remains identical to the previous control implementation, simply creating the <div> element to act as the host to the Silverlight plug-in.

I now have a complete server-side control that encapsulates Silverlight. In this implementation, the control exposes properties that are used to initialize elements of the XAML (or even replace the entire XAML if need be). The following declarations will create an instance of my new control on a page with the title text set to a custom string:

<%@ Register Assembly="SlAjaxSphere" Namespace="MsdnMagazine" 
             TagPrefix="sas" %>
...
<sas:SilverlightSphere ID="_silverlightSphere" runat="server"
                       Title="My custom title!" />

The asp:Xaml and asp:Media Controls

Two new ASP.NET AJAX controls were introduced in the May 2007 ASP.NET Futures release: asp:Xaml and asp:Media. (Note that asp:Media inherits from asp:XAML.) Both these controls provide their own encapsulation of Silverlight, and both are built in much the same way as the sample I presented in the previous section. In fact, these were the inspiration for my control.

The asp:Xaml control lets you associate a XAML file with the control. It includes properties for the Silverlight plug-in creation, handling all the details of creating the plug-in and loading the XAML file. For example, I can use the XAML file to display my Sphere.xaml file with the following declaration:

<asp:Xaml runat="server" ID="_sphereXaml" Windowless="true" 
          Width="300px" Height="300px" 
          XamlUrl="~/Sphere.xaml"  />

The asp:Media control makes it easy to embed media content (video or audio) into an ASP.NET page, and it really exemplifies the possibilities when creating server-side ASP.NET controls that encapsulate Silverlight. One of the intriguing features of the Media control is that it provides eight different skins (which are XAML files) that you can choose from to present your media—or you can design your own skin. Figure 8 shows an example of the Media control displaying a video using the Expression skin, like so:

Figure 8 Media Control Displays Video

Figure 8** Media Control Displays Video **

<asp:Media runat="server" ID="_butterflyVideo" 
           MediaSkin="Expression" MediaUrl="~/Butterfly.wmv" />

Another option for creating your own instrumented Silverlight server-side control is to derive from the asp:Xaml control directly and augment it with properties and methods in much the same way as I did in the second control implementation I described. In the code download for this column, I've included another version of the SilverlightSphere control that inherits from asp:Xaml.

Send your questions and comments for Fritz to xtrmasp@microsoft.com.

Fritz Onion is a co-founder of Pluralsight, a Microsoft .NET training provider, where he heads the Web development curriculum. Fritz is the author of Essential ASP.NET (Addison Wesley, 2003) and Essential ASP.NET 2.0 (Addison Wesley, 2006). You can reach him at pluralsight.com/fritz.