Cutting Edge

Browser Interoperability In Silverlight 2

Dino Esposito

This column is based on a prerelease version of Silverlight 2. All information is subject to change.

Code download available at:MSDN Code Gallery(565 KB)

Contents

Configuring the Silverlight Control
Accessing the DOM from Silverlight
Inside the Browser Interoperability Layer
Attaching Managed Code to DOM Events
Synchronous Calls and Silverlight 2
XAML, Managed Code, and JavaScript
Cross-Domain Access and Silverlight Plug-Ins
In a Nutshell

You can use Silverlight 2 to build full-page Windows Presentation Foundation (WPF)-like Web applications or to enrich HTML-based pages with additional features such as animations, advertising, and specific applets. The browser doesn't directly process the eXtensible Application Markup Language (XAML) content that makes up a Silverlight application. Instead, within the HTML page, an <object> tag points to the Silverlight 2 plug-in and has among its parameters the URL to download any necessary XAML resources.

The bottom line is that there's always an HTML page wrapped around a Silverlight plug-in, even though you often can't see it because the Silverlight application is configured to run in full-screen mode. In addition, the content hosted by the Silverlight plug-in is not isolated from the surrounding page in a same-site situation. When the Silverlight content comes from a different domain than the hosting HTML page, then it is indeed isolated from the page.

Silverlight comes with a browser interoperability layer that allows managed code to access the document object model (DOM) of the underlying page and register managed handlers for page-level events. At the same time, any JavaScript code running in the page can gain access to the XAML content of the plug-in and even make modifications. Finally, JavaScript code running in the page can also invoke managed functions as long as these functions are exposed properly. This month I'll discuss the browser interoperability layer of Silverlight 2 and the ways you can take advantage of it in your applications.

Configuring the Silverlight Control

As mentioned, the Silverlight plug-in is invoked through an <object> tag. However, this tag can be created for you by an ASP.NET control in ASP.NET pages. When you create a sample Silverlight application in Visual Studio 2008, the following markup for the Silverlight server control is automatically inserted in the test page:

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

The MinimumVersion attribute indicates the minimum version of the Silverlight runtime that is required to run the application specified in the Source attribute. Width and Height define the size of the Silverlight window with respect to the host page. As you can see, by default the Silverlight plug-in is configured to run in full-size mode.

Figure 1 lists the properties you can set to configure the Silverlight plug-in. The properties in Figure 1 refer to the Silverlight ASP.NET control that executes on the server side and spits out an <object> tag that may have differently named parameters or no equivalent parameters. For the purposes of this column, you should pay a lot of attention to the HtmlAccess property. (Note that this part of the Silverlight SDK changed a bit in the transition from Beta 1 to Beta 2.)

Figure 1 Properties of the Silverlight ASP.NET Server Control

Property Description
AutoUpgrade Indicates whether the Silverlight plug-in should automatically be upgraded. The default is false.
DefaultScriptType The default type of the client JavaScript object to create and associate with the Silverlight plug-in. By default, it is Sys.UI.Silverlight.Control.
EnableFrameRateCounter Indicates whether to display the current frame rate in the hosting browser's status bar. The default is true.
EnableRedrawRegions Indicates whether to show the areas of the Silverlight plug-in that are being redrawn for each frame. The default is true.
HtmlAccess Indicates whether to allow the Silverlight application to access the page DOM.
InitParameters Defines an optional set of user-defined initialization parameters.
MaxFrameRate Maximum number of frames to render per second for the Silverlight document.
MinimumVersion Minimum version of the plug-in that is required for the current application. The default is 1.0.
PluginBackground Indicates the plug-in background color.
PluginNotInstalledTemplate HTML markup to render if the Silverlight plug-in is not installed.
ScaleMode Indicates how the Silverlight plug-in fills the available space: none, zoom, or stretch. The default is none.
ScriptType The type of the client JavaScript object to create and associate with the Silverlight plug-in.
Source The URL for the XAML source file or the Extensible Ajax Platform (XAP) source package to download.
SplashScreenSource The URL of the splash screen document to render when the source document is loading.
Windowless Indicates whether the Silverlight plug-in is rendered directly in the browser's client area or within an aptly created window. The default is true.

The HtmlAccess property indicates the level of access to the underlying page DOM that the Silverlight application is allowed. The property accepts values from the HtmlAccess enumerated type. Values are listed in Figure 2 .

Figure 2 Values to Control Access to the Page DOM

Value Description
Disabled The application is not allowed access to the DOM of the underlying page.
Enabled The application is allowed access to the DOM of the underlying page.
SameDomain The application is allowed access to the DOM of the underlying page only if the page comes from the same domain of the Silverlight XAP application. This is the default setting.

Obviously, Enabled and Disabled are self-explanatory. The default setting is SameDomain, which doesn't inject any script in the page's markup. Note that a Silverlight application can be hosted in a page that is, in turn, hosted within a frame outside of its native domain. In this case, the Silverlight managed code is able to access the DOM of the host page in a cross-domain manner. Browsers have their own barriers to prevent cross-domain scripting, but they can't do much to stop the managed code within the Silverlight plug-in. Authors of Silverlight pages use HtmlAccess to control cross-domain access.

Accessing the DOM from Silverlight

Once access to the underlying page DOM has been granted, a Silverlight application can use the members on the static class Html­Page to accomplish its own tasks. Figure 3 lists properties and methods of the HtmlPage class.

Figure 3 Members of the HtmlPage Class

Member Description
BrowserInformation Gets general information about the browser, such as name, version, and operating system.
Document Provides access to the document object of the page.
IsEnabled Indicates whether access to the page DOM is allowed.
Plugin Gets a reference to the Silverlight object.
Window Provides access to the window object of the page.
RegisterCreateableType Registers a managed type as available for creation from JavaScript code.
RegisterScriptableObject Registers a managed object as scriptable by JavaScript code in the page.
UnregisterCreateableType Unregisters a type that was previously registered as createable using the RegisterCreteableType method.

The Document property returns a reference to an object of the managed type System.Windows.Browser.HtmlDocument, which represents the DOM of the underlying page. The Document property is strongly typed and exposes to Silverlight applications most of the features available to script in a weakly typed manner. Properties include the list of cookies, the query string, the body of the document, and the reference to the root of the DOM. The object also features a Boolean IsReady property to signal whether the browser's document has been fully loaded, which is paired with transposition of the DocumentReady event in the browser's DOM.

The HtmlDocument class also features methods such as Submit, AttachEvent, DetachEvent, GetElementById, and Create­Element. In addition, a couple of inherited methods—GetProperty and SetProperty—allow you to get and set attributes of the HTML elements from within managed code.

Full browser information is available through the Browser­Information property. Also, in this case, the property is of a managed type that wraps any user agent information that is available at the browser level. The following code snippet shows how to access user-agent data:

string info = HtmlPage.BrowserInformation.UserAgent;

The BrowserInformation object also exposes a Boolean property that indicates whether the browser supports cookies.

The following code shows how to retrieve a reference to a DOM element using managed code:

HtmlElement label1 = HtmlPage.Document.GetElementById("Label1");
label1.SetProperty("innerHTML", "Dino");

The GetElementById method takes an ID string and attempts to locate a corresponding element in the underlying DOM. The object returned is of type HtmlElement—a managed type acting as the wrapper for a reference to an underlying browser object.

HtmlElement features a set of properties that makes it look like an HTML element. It has methods such as AppendChild, Remove­Child, GetAttribute, RemoveAttribute, and SetAttribute. Properties are Id, Parent, TagName, and Children.

All HTML script objects you manipulate from within Silverlight code derive from the base ScriptObject class. This class defines a couple of methods—SetProperty and GetProperty—that all derived classes such as Html­Document inherit.

What's the difference between getting and setting attributes and properties? Attributes are always managed as strings; properties are managed as strongly typed values.

Inside the Browser Interoperability Layer

A graphical view of the Silverlight browser interoperability layer and access to the page DOM is shown in Figure 4 . Any requests made to any classes in the interoperability layer are resolved through an internal browser host service. Information is marshaled down to the browser's unmanaged environment and then back to Silverlight. Type differences are hidden and taken care of by the interoperability layer. DOM-level objects are wrapped as managed objects and served to the Silverlight code through a new managed interface—HtmlDocument, HtmlWindow, and the like. Let's focus on the GetElementById method.

fig041.gif

Figure 4 The HTML Bridge in Silverlight 2

As its first step, the method ensures that the code is being called on the Silverlight UI thread. If not, an exception is thrown. Next, a request is made to the underlying browser to get a reference to the specified DOM element. If the request is successful, the method gets an unmanaged reference for the DOM object for which it will create and return a managed wrapper.

Attaching Managed Code to DOM Events

A very nice result of the Silverlight and DOM interaction is the ability you're given to run managed code in response to DOM events. For example, when the user clicks on a button, you can execute C# code instead of JavaScript code. Here's how you can achieve that:

HtmlElement button1;
button1 = HtmlPage.Document.GetElementById("Button1");
button1.AttachEvent("click",
         new System.EventHandler(Button1_Click));

You first retrieve a managed reference to the button (or the DOM element) of interest. Next, you invoke the managed AttachEvent method to register a handler for the specific event. The really nice thing is that the handler is managed code whereas the event is triggered at the browser unmanaged level. For example, getting a GUID is nearly impossible in JavaScript. It becomes fairly easy, however, if you can rely on the power of managed code through Silverlight:

void Button1_Click(object sender, EventArgs e)
{
    // Get a new GUID
    Guid g = Guid.NewGuid(); 

    // Display the GUID in the page user interface 
    HtmlElement label1 = HtmlPage.Document.GetElementById("Label1");
    label1.SetProperty("innerHTML", g.ToString());
}

Needless to say, the managed code is a member of the Silverlight page codebehind class. The effect of the operation can be reflected both in the HTML of the host page (as in the preceding example) or directly in the Silverlight user interface—it is entirely up to you and your circumstances.

The attachment of the event is an operation that occurs through the browser interoperability layer and ends up calling the AttachEvent method on DOM objects. When the browser triggers the page-level event, a call is made back to Silverlight to execute the managed code.

When is such a feature helpful in the real world? Silverlight is a product designed to serve up a rich Web front end. You need Silverlight if you need managed code in the browser to do things that a managed language does better and faster than script. Examples of this are code-intensive operations where the speed of a compiled language beats script hands down, or operations that are not available in a functionally limited environment like the browser's. For sure, you don't strictly need Silverlight if all that you do is manipulate the DOM. So the ability to handle events in managed code is a very good feature to have, but it does require Silverlight. And you probably wouldn't want to engage Silverlight only for handling events.

In Silverlight 2, the HtmlWindow object provides the managed representation of the JavaScript window object. It stores a reference to the DOM object and allows you to drive it with a set of managed methods: Alert, Confirm, Prompt, Submit, Navigate, and even Eval. An instance of the HtmlWindow object is exposed to Silverlight developers through the Window property of the Html­Page class. The following shows how to display a browser's message box from Silverlight:

HtmlPage.Window.Alert("Hello, world");

Let's explore what happens under the hood of this simple piece of code. The model I'll examine repeats itself in almost all methods on the HtmlWindow class:

public void Alert(string message)
{
    HtmlPage.VerifyThread();
    if (message == null)
    {
        message = string.Empty;
    }
    this.Invoke("alert", 
                new object[] { message });
}

After the test on the UI thread, the method fixes the message string if it is null and proceeds to call the Invoke method on the base ScriptObject class. The Invoke method is the bridge between the managed world of Silverlight and the browser.

The method accepts two parameters, as you see here:

public virtual object Invoke(string name, params object[] args)

The first argument indicates the name of the method to invoke on the scriptable object stored in the current instance of Script­Object. The second argument is simply the list of arguments for the method to invoke.

The method is responsible for correctly marshaling types across the two different runtime environments. On the way to the browser, it transforms managed objects into JavaScript-compatible types; on the way back, it does the reverse.

There's another method on HtmlWindow that deserves some attention here—the CreateInstance method:

public ScriptObject CreateInstance(
    string typeName, 
    params object[] args)

The method allows you to create an instance of the specified JavaScript object. The type name parameter indicates the name of the JavaScript object to instantiate. Internally, the method prepares a dynamic JavaScript function that creates the specified object and then invokes the function from Silverlight:

ScriptObject xhr = HtmlPage.Window.CreateInstance("XMLHttpRequest");

The method CreateInstance is useful for obtaining a scriptable reference to a JavaScript object. Next, you script it using the Invoke method. These features are incredibly helpful when you really need to make synchronous calls from Silverlight 2. The problem is that synchronous calls are not permitted in Silverlight 2.

Synchronous Calls and Silverlight 2

Silverlight 2 supplies a variety of APIs to make calls to remote endpoints, but all of them must be asynchronous. You can start the call on a background thread, but you can't force the UI thread to sync up. If you block the UI thread on a sync object, you actually stop the UI thread indefinitely, and no command that resets the sync object can ever resume it. There's a big debate in the community about synchronous calls in Silverlight.

Nonetheless, synchronous calls from within the Silverlight API to a remote endpoint are simply not supported today because they are known and proven to have associated latency. Synchronous calls are an important feature in the browser environment, however. They're supported in all browsers that support XmlHttpRequest.

XmlHttpRequest is a browser object that enables AJAX scenarios. The object leverages the browser machinery to make a call to an endpoint within the same domain. By default, XmlHttpRequest operates asynchronously, and this is the way in which most AJAX frameworks use it. However, XmlHttpRequest can be quite easily configured to operate synchronously.

By creating and controlling an instance of XmlHttpRequest from Silverlight, you can arrange synchronous calls and fix all those particular scenarios where a synchronous call would make coding much easier. (Personally, I'm not a big fan of the async-only nature of Silverlight remote calls. Async calls are sufficient most of the time, but I think that, especially when you're adapting some existing front end to Silverlight, you may run into situations where a synchronous call would save you a lot of redesign. I'm the first to say that design is key, but if a consciously written synchronous call can save me hours or days of work, I'll definitely go for it.)

That said, the Silverlight team has good reasons to push the async-only approach because synchronous calls to remote endpoints could likely cause Silverlight applications to freeze the user interface, thus deteriorating the end user's experience with his browser and Web applications. Synchronous calls are technically possible, but the team does not support this natively in the platform in the interest of all applications and their consumers. Likewise, the team does not recommend resorting to a manually written synchronous remote call using XmlHttpRequest, as in Figure 5 .

Figure 5 Making Synchronous Calls from within Silverlight 2

private void Button1_Click(object sender, 
    System.Windows.RoutedEventArgs e)
{
    string url = "...";
    ScriptObject xhr = HtmlPage.Window.CreateInstance("XMLHttpRequest");
    xhr.Invoke("open", "POST", url, false);
    xhr.Invoke("setRequestHeader", "Content-Type", 
        "application/x-www-form-urlencoded");

    // Prepare the body as the endpoint expects it to be 
    string body = "...";
    xhr.Invoke("send", body);
    string response = (string) xhr.GetProperty("responseText");

    // Process the response and update the UI
    ProcessData(response)
}

Figure 5 shows how to use XmlHttpRequest to set up a synchronous same-domain call to a URL from within Silverlight 2. The key statement is when you invoke the open method on XmlHttp­Request. The Boolean argument indicates whether the call has to be asynchronous. If false, you instruct the object to proceed in a synchronous manner.

The drawback of this trick is that it leverages a low-level tool that just doesn't offer any facilities for converting strings to and from, say, JavaScript Object Notation (JSON) streams. If you employ this trick, you have to take care of any JSON serialization and deserialization using the DataContract­JsonSerializer class.

XAML, Managed Code, and JavaScript

JavaScript functions can gain access to the content of the Silverlight application and perform read and write operations. The content of the Silverlight application is the tree of XAML elements.

Anything in the XAML document that is characterized by a unique name—the x:Name attribute—can be accessed and scripted in JavaScript. The first step entails getting a DOM reference to the Silverlight plug-in. In an ASP.NET AJAX page, you would use the following code:

var plugin = $get("SilverlightControl1");

SilverlightControl1 is the ID of the Silverlight control or the ID you used for the <object> tag that points to the downloadable content. Next, you point to the actual XAML content using the content property. To locate specific XAML elements, you use the findName method:

// Retrieve the XAML element tagged with the name of TextBlock1
var xamlTree = $get("SilverlightControl1").content;
var textBlock1 = xamlTree.findName("TextBlock1");

// Modify the current content of the text block element
textBlock1.Text = "...";

If you use JavaScript code to drive the content of the XAML document, then it is recommended that you cache in your page any reference to XAML elements that you encounter along the way. This would save you from repeatedly traversing the XAML tree to find the same element over and over again.

You should notice that using JavaScript to access the content of the XAML document is a bit outdated for Silverlight 2. It remains a good option if you are targeting Silverlight 1.0 or, at least, if you are serving up Silverlight applications consisting of only XAML and script code. If you can use managed code to decide about the content to display, then it is hard to imagine any changes that you might want to make to the user interface using JavaScript instead of managed code.

In Silverlight 2, you can use JavaScript code to invoke managed code. This option addresses a scenario where you have an HTML-based page powered by some managed code that performs critical operations.

Your JavaScript code can script any managed object hosted in Silverlight 2 that has been previously registered as scriptable:

guidHelper = new GuidHelper();
HtmlPage.RegisterScriptableObject("GuidTools", guidHelper);

The method RegisterScriptableObject takes the informal name of the object being registered and a variable instance. The string being the first argument is the name that the caller JavaScript code will use to refer to the registered object.

With respect to the preceding code, the following shows what you do from JavaScript in order to invoke a method on the sample class GuidHelper:

// Invoke a method on a scriptable managed object
var guid = $get("Silverlight1").content.GuidTools.Generate();

// Update the user interface with the results
$get("Random1").innerHTML = guid;

The public name of the scriptable object—in this case, GuidTools—is used as a member to access the underlying object from JavaScript. The pseudo property, GuidTools, is exposed by the content property of the Silverlight plug-in. Let's have a look at the GuidHelper class:

public class GuidHelper
{
    [ScriptableMember]
    public string Generate()
    {
        Guid g = Guid.NewGuid();
        return g.ToString();
    }
}

The ScriptableMember attribute on public methods of a scriptable class indicates that the method can be invoked from JavaScript. If all public methods of a class are scriptable, you can use the Scriptable­Type attribute on the class to automatically extend the scriptability attribute to all public methods.

An object registered as scriptable is instantiated through managed code and an existing instance is passed down to JavaScript. Not all managed types can be directly instantiated in JavaScript.

Before you can create an instance of a managed class from JavaScript, you first need to register the type you want as available for creation from JavaScript code:

HtmlPage.RegisterCreateableType("StockPicker", typeof(Samples.Order));

The first argument indicates the alias for the type to be used from within JavaScript. Here's how you create an instance of the Samples.StockPicker class:

var plugin = $get("Silverlight1");
var type = "Samples.StockPicker";  
var inst = plugin.content.services.createObject(type);

You retrieve the Silverlight plug-in, access the content.services property and then invoke the method createObject. The argument to createObject is the string with the name of the managed type to instantiate.

A scriptable type that has complex and custom types in the signatures of its methods also works as a factory for its types. For example, imagine a managed type Customer with the following method:

void Add(Order order);

What about the Order type? Should it be declared createable? Not necessarily. If you omit the call to RegisterCreateableType, then you can only use as the factory a scriptable type that has a scriptable member that requires that type. In other words, if Customer is registered as scriptable and method Add is scriptable, then you can instantiate Order from JavaScript without declaring the type creatable. The following code would work:

var customer = $get("Silverlight1").content.Customer;
var order = customer.createManagedObject("Order");
order.ID = 1;
o.OrderDate = new Date();
customer.Add(order);

Managed types are marshaled down to JavaScript wrapped as browser objects. The wrapper browser object contains a table of valid methods to call from JavaScript. The list includes all methods declared as scriptable, plus createManagedObject. Invocations on methods are then resolved by invoking the corresponding method in the managed code.

Cross-Domain Access and Silverlight Plug-Ins

A Silverlight application that exposes a public managed API is subject to cross-domain access. If cross-domain access represents a security hazard, or just an unwanted feature, you should take your countermeasures.

When a page with a Silverlight plug-in is hosted in a frame, it is possible that the host page lives in a different domain than the Silverlight application. This means that the JavaScript code in the host page (cross-domain) is able to gain access through the frame to the Silverlight plug-in and script any public-managed object.

Cross-domain access to the Silverlight content may be disabled, fully enabled, or limited to scripting. It is disabled by default. You can control cross-domain access, as in Figure 6 , by using the External­CallersFromCrossDomain attribute in the Deployment node of the Silverlight application manifest file. The manifest file is the file that is generated by Visual Studio 2008 when you compile a Silverlight project.

Figure 6 A Sample Manifest File for a Silverlight 2 App

<Deployment xmlns="https://schemas.microsoft.com/client/2007/deployment" 
            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
            EntryPointAssembly="BrowserFun" 
            EntryPointType="BrowserFun.App" 
            RuntimeVersion="2.0.30523.6"
            ExternalCallersFromCrossDomain="FullAccess">
    <Deployment.Parts>
        <AssemblyPart x:Name="BrowserFun" 
                  Source="BrowserFun.dll" />
        <AssemblyPart x:Name="System.Windows.Controls.Extended" 
                  Source="System.Windows.Controls.Extended.dll" />
    </Deployment.Parts>
</Deployment>

When you load a page in the browser that contains one or more Silverlight plug-ins, you still get one instance of the CLR per browser process. However, each running instance of the Silverlight plug-in gets its own AppDomain. Nothing is being shared between plug-ins and communication is possible, but only through code—and with the help of a little trick.

The trick consists essentially of using the browser interoperability layer as an intermediary. One plug-in exposes a managed interface and the other connects to it and invokes methods.

So one plug-in defines a few scriptable members, like so:

public partial class Page : UserControl
{
  public Page()
  {
    InitializeComponent();
    HtmlPage.RegisterScriptableObject(
        "Action", new ActionPageCommand());
  }
}

The ActionPageCommand class contains all methods that the plug-in exposes for external callers. Here's an example:

[ScriptableType]
public class ActionPageCommand
{
  public int GetRandomNumber()
  {
     Random rnd = new Random();
     return rnd.Next(0, 100);
  }
}

Scriptable members on a plug-in are directly visible to JavaScript code in the page. In Figure 7 , you see a JavaScript wrapper for the publicly available interface of the Silverlight plug-in. Any plug-in interested in the interface of the other plug-in can create an instance of the JavaScript wrapper in the figure:

ScriptObject so = HtmlPage.Window.CreateInstance("ActionBar");
object result = so.Invoke("invokeGetRandomNumber");

Figure 7 Exposure to Another Silverlight Plug-In

<script type="text/javascript">
var ActionBar = function()
{}

function invokeGetRandomNumber$Impl()
{
    var plugin2Services = $get("Silverlight2").content;
    var results = plugin2Services.Action.GetRandomNumber();
    return results;    
}

ActionBar.prototype = 
{
    invokeGetRandomNumber: invokeGetRandomNumber$Impl
}
</script>

Is there a more direct way to invoke a Silverlight plug-in from another one? Yes, you can opt for the following:

HtmlElement plugin = HtmlPage.Document.GetElementById("Silverlight2");
var content = (ScriptObject) plugin.GetProperty("content");
var action = (ScriptObject) content.GetProperty("Action");
action.Invoke("GetRandomNumber");

The final effect is the same. This way, however, you're making more round-trips between Silverlight and the underlying browser.

In a Nutshell

The Silverlight Base Class Library includes facilities to connect to the browser and the host page and to read information and access the DOM. If you have a Silverlight-centric application, you probably don't need to do much with the host HTML page. However, if yours is a mixed solution with both ASP.NET AJAX and Silverlight, then the need to execute managed code from JavaScript or invoke JavaScript objects from within managed code is a real one.

The browser interoperability layer (also known as the HTML bridge) contains a lot of functionality to enable communication between the managed world of Silverlight and the interpreted world of JavaScript. Communication requires marshaling of types and objects across the layers. At the highest level of abstraction, you find a public API and documentation to explain the few things you need to know to invoke managed code from JavaScript and JavaScript objects from managed code.

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 .