I like the HTML DOM interoperability in Silverlight, I spent quite a lot of time in my UK REMIX talk (up here http://www.microsoft.com/uk/remix08/agenda.aspx on day 2) demonstrating various things that we can do with it and it’s very, very rich and powerful. Like it. Like it. Like it ๐
However, it doesn’t half look like the HTML DOM!
I know that’s a daft thing to say, that’s what it’s meant to look like so you’d expect nothing else but recent advances in API’s such as LINQ to XML have left me not wanting to do that kind of DOM based programming any more.
Here’s a “for instance”.
Imagine that I’m in the .NET world and I create a brand new Silverlight project and I’ve created this “UI” in XAML;
<UserControl x:Class="SilverlightApplication5.Page" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > <Grid x:Name="LayoutRoot" Background="Gray"> <TextBox x:Name="txtPerson" MinWidth="192" FontSize="36" Margin="20" Text="Not Set" HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" /> </Grid> </UserControl>
So, very simple. Now imagine that I’ve created this class here;
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public static List<Person> GetDummyData() { return (new List<Person>() { new Person() { FirstName = "Mike", LastName = "Taulty" }, new Person() { FirstName = "Mike", LastName = "Ormond" } }); } }
Now imagine that I’ve changed my hosting page for the Silverlight control to look something like this;
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head> <title>SilverlightApplication5</title> <style type="text/css"> html, body { height: 100%; overflow: auto; } body { padding: 0; margin: 0; } #silverlightControlHost { height: 50%; } #myDiv { height: 50%; background-color:Purple; } </style> </head> <body> <div id="silverlightControlHost"> <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%"> <param name="source" value="ClientBin/SilverlightApplication5.xap"/> </object> </div> <div id="myDiv"> </div> </body> </html>
So, what have I done there? Well, I’ve just gone and divided the real estate 50:50 between the Silverlight control and a div called myDiv so that it ends up looking like this;
Now, say I want to create an HTML table inside that purple DIV which contains the people data that I return from my GetDummyData function and say I want a button which will copy the text into the Silverlight textbox. That might look like this;
public partial class Page : UserControl { public Page() { InitializeComponent(); this.Loaded += OnLoaded; } void OnLoaded(object sender, RoutedEventArgs e) { HtmlElement myDiv = HtmlPage.Document.GetElementById("myDiv"); HtmlElement table = HtmlPage.Document.CreateElement("table"); HtmlElement tbody = HtmlPage.Document.CreateElement("tbody"); foreach (Person p in Person.GetDummyData()) { HtmlElement tr = HtmlPage.Document.CreateElement("tr"); HtmlElement td = HtmlPage.Document.CreateElement("td"); td.SetAttribute("innerText", p.FirstName); tr.AppendChild(td); td = HtmlPage.Document.CreateElement("td"); td.SetAttribute("innerText", p.LastName); tr.AppendChild(td); td = HtmlPage.Document.CreateElement("td"); HtmlElement button = HtmlPage.Document.CreateElement("input"); button.SetAttribute("type", "button"); button.SetAttribute("value", "Select"); // Bit of a sneaky use of closures here Person current = p; button.AttachEvent("onclick", (object s, EventArgs a) => { txtPerson.Text = string.Format("{0} {1}", current.FirstName, current.LastName); }); td.AppendChild(button); tr.AppendChild(td); tbody.AppendChild(tr); } table.AppendChild(tbody); myDiv.AppendChild(table); } }
and that all works fine but, wow, it’s a lot of code for what I was trying to achieve.
When I used to go out and demonstrate LINQ to XML I would show this kind of example against the XML DOM with the XmlDocument class as an example of the problem that LINQ to XML was trying to solve ๐
I didn’t really want to write this much code so I wrote some helper classes that try and make ( a small, incomplete part of working with HTML interop ) look a bit more like LINQ to XML. Here’s the same functionality when I use my own classes;
void OnLoaded(object sender, RoutedEventArgs e) { HtmlElement myDiv = HtmlPage.Document.GetElementById("myDiv"); myDiv.Add( new HElement("table", new HElement("tbody", from p in Person.GetDummyData() select new HElement("tr", new HElement("td", new HAttribute("innerText", p.FirstName)), new HElement("td", new HAttribute("innerText", p.LastName)), new HElement("td", new HElement("input", new HAttribute("type", "button"), new HAttribute("value", "select"), new HHandler("onclick", (s, a) => { txtPerson.Text = string.Format("{0} {1}", p.FirstName, p.LastName); }))))))); }
It’s just “2 lines of code” and, in fact, I could easily make it one line of code. That’s a bit of a glib thing to say but I do find that style of code far easier to write than what I wrote initially.
I imagine I could still make that a lot better ( e.g. removing those string names for elements and attributes would be a start ) and there’s perhaps a better way to factor all of this but I can certainly write that piece of code using my own HElement, HAttribute, HStyleAttribute, HHandler (ahem, bad name!) a lot more easily than I can write it using the built-in HtmlDocument class.
Here’s the supporting classes ( note – I wrote these in about 10-15 minutes so they aren’t meant to be complete or working or anything like that ).
Firstly, simple classes that make it possible to add an element/attribute/style attribute on a single line of code;
public class HAttribute { public HAttribute() { } public HAttribute(string name, string value) { this.Name = name; this.Value = value; } public string Name { get; set; } public string Value { get; set; } } public class HStyleAttribute : HAttribute { } public class HHandler { public HHandler() { } public HHandler(string eventName, EventHandler<HtmlEventArgs> handler) { EventName = eventName; Handler = handler; } public string EventName { get; set; } public EventHandler<HtmlEventArgs> Handler { get; set; } }
and then the HElement class itself which does a tiny bit more work;
public class HElement { public HElement(HtmlElement parent, string elementType, params object[] items) : this(elementType, items) { parent.AppendChild(element); } public HElement(string elementType, params object[] items) { element = HtmlPage.Document.CreateElement(elementType); AddItems(items); } void AddItems(object[] items) { foreach (object o in items) { if (o.GetType() == typeof(HElement)) { AddSubElement((HElement)o); } else if (o.GetType() == typeof(HAttribute)) { AddAttribute((HAttribute)o); } else if (o.GetType() == typeof(HStyleAttribute)) { AddStyleAttribute((HStyleAttribute)o); } else if (o.GetType() == typeof(HHandler)) { AddHandler((HHandler)o); } else { IEnumerable<HElement> elementList = o as IEnumerable<HElement>; if (elementList != null) { foreach (HElement el in elementList) { AddSubElement(el); } } else { IEnumerable<HAttribute> attrList = o as IEnumerable<HAttribute>; if (attrList != null) { foreach (HAttribute attr in attrList) { AddAttribute(attr); } } else { IEnumerable<HStyleAttribute> styleList = o as IEnumerable<HStyleAttribute>; if (styleList != null) { foreach (HStyleAttribute style in styleList) { AddStyleAttribute(style); } } else { throw new InvalidOperationException("Parameter type not supported"); } } } } } } private void AddHandler(HHandler handler) { element.AttachEvent(handler.EventName, handler.Handler); } private void AddStyleAttribute(HStyleAttribute styleAttribute) { element.SetStyleAttribute(styleAttribute.Name, styleAttribute.Value); } private void AddAttribute(HAttribute attribute) { element.SetAttribute(attribute.Name, attribute.Value); } private void AddSubElement(HElement subElement) { element.AppendChild(subElement); } public static implicit operator HtmlElement(HElement el) { return (el.element); } HtmlElement element; }
and then finally I added a couple of ( very similar looking – perhaps one should call the other? ) extension methods to HtmlElement;
public static class HtmlElementExtensions { public static void Add(this HtmlElement parent, IEnumerable<HElement> elements) { foreach (HElement item in elements) { parent.AppendChild(item); } } public static void Add(this HtmlElement parent, params HElement[] elements) { foreach (HElement item in elements) { parent.AppendChild(item); } } }
I find that a lot easier than what comes in the box but, naturally, it’s just wrapping whereas what’s in the box is providing the functionality in the first place.
Here’s the project file for download – maybe someone could take something like this and make a nice CodePlex library out of it that does a proper job?