Accessing Web Page Elements in Silverlight

Remote Control

Remote Control

Introduction

Lately I’ve been working on a web application based on Silverlight 2.  The application uses a traditional web login system based on a cryptographically signed cookie.  In early development,  users logged in on an HTML page,  which would load a Silverlight application on successful login.  Users who didn’t have Silverlight installed would be asked to install it after logging in,  rather than before.

Although it’s (sometimes) possible to determine what plug-ins a user has installed using Javascript,  the methods are dependent on the specific browser and the plug-ins.  We went for a simple and effective method:  make the login form a Silverlight application,  so that users would be prompted to install Silverlight before logging in.

Our solutionn  was to make the Silverlight application a drop-in replacement for the original HTML form.  The Silverlight application controls a hidden HTML form:  when a user hits the “Log In” buttonin the Silverlight application,  the application inserts the appropriate information into the HTML form and submits it.  This article describes the technique in detail.

The HTML Form

The HTML form is straightforward — the main thing that’s funky about it is that all of the controls are invisible,  since we don’t want users to see them or interact with them directly:

[01] <form id="loginForm" method="post" action="">
[02]     <div class="loginError" id="loginError">
[03]        <%= Server.HtmlEncode(errorCondition) %>
[04]     </div>
[05]     <input type="hidden" name="username" id="usernameField"
[06]        value="<%= Server.HtmlEncode(username) %>" />
[07]     <input type="hidden" name="password" id="passwordField"  />
[08] </form>

The CSS file for the form contains the following directive:

[09] .loginError { display:none }

to prevent the error message from being visible on the HTML page.

Executing Javascript In Silverlight 2

A Silverlight 2 application can execute Javascript using the HtmlPage.Window.Eval() method;  HtmlPage.Window is static,  so you can use it anywhere,  so long as you’re running in the UI thread.  Our simple application doesn’t do communication and doesn’t launch new threads,  so we don’t need to worry about threads.  We add a simple wrapper method to help the rest of the code flow off the tips of our fingers

[10] using System.Windows.Browser
[11] ...
[12] namespace MyApplication {
[13] public partial class Page : UserControl {
[14]      ...
[15]        Object JsEval(string jsCode) {
[16]            return HtmlPage.Window.Eval(jsCode);
[17]        }

Note that Visual Studio doesn’t put the using in by default,  so you’ll need to add it.  The application is really simple,   with just two event handlers and a little XAML to define the interface,  so it’s all in a single class,  the Page.xaml and Page.xaml.cs files created when I made the project in VIsual Studio.

Note that JsEval returns an Object,  so you can look at the return value of a javascript evaluation.  Numbers are returned as doubles and strings are returned as strings,  which can be quite useful.  References to HTML elements are returned as instances of the HtmlElement class.  HtmlElement has some useful methods,  such as GetAttribute() and SetAttribute(),  but I’ve found that I get more reliable results by writing snippets of Javascript code.

Finding HTML Elements

One practical is problem is how to find the HTML Elements on the page that you’d like to work with.  I’ve been spoiled by the $() function in Prototype and JQuery,  so I like to access HTML elements by id.  There isn’t a standard method to do this in all browser,  so I slipped the following snippet of Javascript into the document:

[18]    function returnObjById(id) {
[19]        if (document.getElementById)
[20]            var returnVar = document.getElementById(id);
[21]        else if (document.all)
[22]            var returnVar = document.all[id];
[23]        else if (document.layers)
[24]            var returnVar = document.layers[id];
[25]        return returnVar;
[26]    }

(code snippet courtesy of NetLobo)

I wrote a few convenience methods in C# inside my Page class:

[27]   String IdFetchScript(string elementId) {
[28]        return String.Format("returnObjById('{0}')", elementId);
[29]   }
[30]
[31]   HtmlElement FetchHtmlElement(string elementId) {
[32]        return (HtmlElement)JsEval(IdFetchScript(elementId));
[33]   }

Manipulating HTML Elements

At this point you can do anything that can be done in Javascript.  That said,  all I need is a few methods to get information in and out of the form:

[34]       string GetTextInElement(string elementId) {
[35]           HtmlElement e = FetchHtmlElement(elementId);
[36]           if (e == null)
[37]               return "";
[38]
[39]           return (string)JsEval(IdFetchScript(elementId) + ".innerHTML");
[40]       }
[41]
[42]       void Set(string fieldId, string value) {
[43]            HtmlElement e = FetchHtmlElement(fieldId);
[44]            e.SetAttribute("value", value);
[45]       }
[46]
[47]       string GetFormFieldValue(string fieldId) {
[48]            return (string) JsEval(IdFetchScript(fieldId)+".value");
[49]       }
[50]
[51]       void SubmitForm(string formId) {
[52]            JsEval(IdFetchScript(formId) + ".submit()");
[53]       }

Isolating the Javascript into a set of helper methods helps make the code maintainable:  these methods are usable by a C#er who isn’t a Javascript expert — if we discover problems with cross-browser compatibility,  we can fix them in a single place.

Putting it all together

A little bit of code in the constructor loads form information into the application:

[54]        public Page() {
[55]            ...
[56]            LoginButton.Click += LoginButton_Click;
[57]            UsernameInput.Text = GetFormFieldValue("usernameField");
[58]            ErrorMessage.Text = GetTextInElement("loginError");
[59]         }

The LoginButton_Click event handler populates the invisible HTML form and submits it:

[60]      void LoginButton_Click(object sender, RoutedEventArgs e) {
[61]           SetFormFieldValue("usernameField", UsernameInput.Text);
[62]           SetFormFieldValue("passwordField", PasswordInput.Password);
[63]           SubmitForm("loginForm");
[64]       }

Conclusion

Although Silverlight 2 is capable of direct http communication with a web server,  sometimes it’s convenient for a Silverlight application to directly manipulate HTML forms.   This article presents sample source code that simplifies that task.

(Thanks skippy for the remote control image.)