Progressive Enhancement with Silverlight
I’ve been working with Yahoo’s YUI ajax libraries recently and I’ve been impressed (if frustrated) by their DataTable feature. What I especially liked about it was the idea to combine Progressive Enhancement (i.e. the ability to spruce up HTML by injecting more code and styles via javascript) with re-using the data inside the HTML table. Although I’m sure everyone wants to use nice REST/Web services to fetch their data there is still vast amounts of legacy code out there that is just pumping out HTML tables that have the potential for a good make-over. Now Ajax is fun and all, but for .NET developers Silverlight is the promised land since it uses almost exactly the same skill set. So my theory was, "why can’t I use Silverlight to enhance a HTML table and scrape the data from the HTML rather than a web service?". So that’s exactly what I did, please take look at the picture (I really should get a proper blog) to see what this all looks like.
The Start, boring HTML table
Here is a boring HTML table, no fancy bits, no resizing, no row selectors, no dragging, no cursor keyboard navigation, etc, etc.
<table>
<thead> <tr> <th>Surname <th>Forename <th class="bool">Available </th></tr>
<tbody> <tr> <td>Bassett <td>Berty <td> <tr> <td>Shore <td>Suzanne <td> <td> <input type="checkbox…
Fairly standard stuff, you should note that the table has an Id and the header for ‘available’ has the class ‘bool’, we’ll use those later. I put this Html into the SilverlightTest.aspx page you get when you create a new project.
How to pass arguments into Silverlight?
In order for Silverlight to work its magic over the Html table it first needs to know which table it should be working on, but how? Initially I decorated my classes with ScriptableMember and then registering the class with the browser using;
TableEnhancement tableEnhancement = new TableEnhancement();
HtmlPage.RegisterScriptableObject("SLTableEnhancement", tableEnhancement);
But it started to get muddy since I had to use clunky techniques in order to correctly discover which instance of the user control was running, etc. So I ended up simply passing the table’s id in Silverlights InitParameters (thanks to Mike Taulty for the hint);
<asp:Silverlight ID="Xaml1" runat="server" InitParameters="tableId=MyTable"…
Silverlight gathers that information in the Application, I chose to store it away in a public property;
private void Application_Startup(object sender, StartupEventArgs e)
{
string tableId = e.InitParams["tableId"];
this.TargetTableId = tableId;
this.RootVisual = new Page();
…
So now Silverlight knows the id of the element it should target.
How can Silverlight examine Html Elements?
This is quite easy too, Silverlight has access to the Dom;
HtmlDocument doc = HtmlPage.Document;
HtmlElement tableElement = doc.GetElementById(tableId);
…
Although this looks very easy the problem is that HtmlElement is as fine grain as it gets. So I’m grabbing a Html table which has lots of interesting properties and collections whereas HtmlElement boils down to GetProperty, GetAttribute and Children. Ok, so with a bit of to and fro-ing you can get the correct properties but I’d certainly like to see a few helper wrappers here.
How to create/configure the Silverlight DataGrid to represent the HTML table?
As with all grid technologies it’s really easy to get quite an advanced grid by simply binding to a source. But I wanted a little more control of the headers. So I set AutoGenerateColumns="False" and created the headers dynamically, be warned this is early prototype code so I’m making quite a few assumptions about the state of the HTML 😉
HtmlElement header = tableElement.GetProperty("tHead") as HtmlElement;
HtmlElementCollection headerRows = header.Children;
HtmlElementCollection headerCols = headerRows[0].Children;
HtmlElementCollection rows = tableElement.GetProperty("rows") as HtmlElementCollection;
Dictionary<int, DataGridColumn> columnHeader = new Dictionary<int, DataGridColumn>();
Dictionary<string, object> data = new Dictionary<string, object>();
HtmlElement trHeader = rows[0];
for (int i = 0; i < trHeader.Children.Count; i++)
{
HtmlElement td = trHeader.Children[i];
string headerText = ((string)td.GetProperty("innerText")).Trim();
string cssClass = td.GetAttribute("className");
if (cssClass != null && cssClass == "bool")
{
columnHeader.Add(i, new DataGridCheckBoxColumn
{
Header = headerText,
DisplayMemberBinding = new Binding(headerText)
});
}
else
{
columnHeader.Add(i, new DataGridTextColumn
{
Header = headerText,
DisplayMemberBinding = new Binding(headerText)
});
}
}
…
Ah, now you can see the role of the bool class, when a header is marked as bool I will create a checkbox column. You can probably guess that eventually I will pass that dictionary to the user control with the data grid so I’ll show that;
public override void CreateTable(Dictionary<int,DataGridColumn>headers, IEnumerable dataSource)
{
for(int i=0;i<headers.Count;i++)
{
DataGridColumn dataGridColumn = headers[i];
this.myDataGrid.Columns.Add(dataGridColumn
);
}
this.myDataGrid.ItemsSource = dataSource;
}
How to create a data source from the values in the HTML Table?
As you can see from the previous code snippet the data grid wants an IEnumerable data source. But my data is inside the rather nasty table hierarchy, so how do you create classes on the fly…well good old TypeBuilder that’s how. Now I must confess that I struck lucky here as while I was looking for exactly signature of a data source I happened across http://blog.bodurov.com/How-to-bind-Silverlight-DataGrid-from-IEnumerable-of-IDictionary. I used the sample there to create the classes on the fly, you have to watch that regEx – especially for spaces in your data. I also have to check the memory implications of on the fly creation cause I’ve been bitten by that before. But I’m pleased to say that it worked well.
How to replace the HTML table?
Standard DOM techniques work well for deleting the HTML table, a quick .Parent.RemoveChild worked fine. So everything worked? Well not quite. Silverlight takes a fraction of time to download after the document onload event fires. This means you can see the HTML table before it gets replaced. The simple answer to this is to style it hidden, if the Silverlight loads then it will be removed, if Silverlight doesn’t load then style it back. The problem was knowing if the user has Silverlight or not. The official line is to register the onPlugInError handler which fires when you don’t have Silverlight or just check to see of the Silverlight object is null. Indeed if you disable the Silverlight add-in in IE then it works fine. The problem is with other "unsupported" browsers, such as Opera, Chrome and Windows Safari. Although they don’t run Silverlight they do quite happily load the control. You can right-click and get all the properties of the Silverlight control…however although the light is on no-one is home. Nothing will run, no events will fire but the object is instantiated so the browser cheerfully reports nothing is wrong. It’s a real problem. Fortunately for my example I could simply show the element in the OnLoad and Silverlight will either run and delete it or not run and leave it there. I added a little timer to greatly reduce the opportunity of seeing the HTML version before the Silverlight kicks in;
<script type="text/javascript">
var timerId;
function showTable() {
clearTimeout(timerId);
var silverlightControl = document.getElementById("Xaml1");
// if the silverlight control hasn’t been loaded then…
if (silverlightControl == null) {
// show the original table
document.getElementById("MyTable").style.visibility = "visible";
// hide the download silverlight banner
document.getElementById("SLHost").style.display = "none";
return;
}
// just cause we’re here doesn’t mean Silverlight is active
// since other browsers still load the ActiveX face-plate
var tableElement = document.getElementById("MyTable");
if (tableElement != null) {
// I know the Silverligtht control deletes this element,
// so if it’s still here then show it, if I’m too quick for SL
// then it will still delete it any second now…
document.getElementById("MyTable").style.visibility = "visible";
}
}
function useSilverlight() {
// very quick timer to give everything a chance to settle down and
// process the message queue
timerId = setTimeout("showTable()", 5);
}
</script>