Silverlight and Xml Binding

I’ll start by saying that this is just a sketchy idea around data-binding Silverlight to XML data.

I’ve had other sketchy ideas on this topic in the past.

I was listening to Ian talk about WPF 4.0 at the UK TechDays event last Thursday and one of the things he mentioned was around the ability of WPF 4.0 to data-bind to objects implementing dynamic dispatch.

I was immediately drawn to wondering whether this might be used to data-bind Silverlight UI to XML until I realised that Silverlight 4 doesn’t have the same ability to data-bind to dynamic objects.

However, Silverlight 4 does have a couple of small features that might combine to do something interesting in this area, namely;

  • the ability to data-bind to string indexers
  • support for XPath

and so I wondered whether I might be able to combine those two things together to do something “interesting” in terms of data-binding Silverlight UI to XML.

Imagine that I’ve got this little XML file ( called test.xml ) on my site of origin;

<?xml version="1.0" encoding="utf-8" ?>
<root>
  <child value="1"/>
  <child value="2"/>
  <child value="3"/>
  <child value="4"/>
  <child value="5"/>
</root>

and then maybe I can set up a little UI like this to data-bind;

 <UserControl.Resources>
    <local:XmlDataSource 
      x:Key="myXmlDataSource"
      DownloadUri="test.xml" />
  </UserControl.Resources>
  <Grid
    x:Name="LayoutRoot">
    <Viewbox>
      <TextBlock
        Text="{Binding 
          Source={StaticResource myXmlDataSource},
          Path=Xml.[sum(descendant-or-self::child/attribute::value)]}" />
    </Viewbox>
  </Grid>

and so you’ll notice that I am using my own class XmlDataSource which is just something that offers 3 properties;

  • DownloadUri – a URI as above where we can do an async download of an XML file ( using WebClient )
  • ResourceUri – a content or resource URI where we can do a sync load of an XML resource packaged either as content in the XAP or as a resource in an assembly in the XAP
  • Xml – the property where the XML ultimately shows up after loading

You’d then notice that my binding on the TextBlock uses the Xml property on the XmlDataSource and applies a string indexer to it.

It’s the string indexer that’s the unusual thing here – it contains an XPath and that’s being applied to produce the value bound to here.

Hopefully it’s obvious that I could never make this particular binding into a two-way binding but I took a stab at two-way binding in particular cases so for instance with a different UI like this one ( against the same XML file );

 <UserControl.Resources>
    <local:XmlDataSource 
      x:Key="myXmlDataSource"
      DownloadUri="test.xml" />
  </UserControl.Resources>
  <Grid
    x:Name="LayoutRoot">
    <ListBox
      ItemsSource="{Binding 
      Source={StaticResource myXmlDataSource},
      Path=Xml.[descendant-or-self::child/attribute::value]}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBox
            Text="{Binding Path=[string()],Mode=TwoWay}" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>

and then I can achieve a 2-way binding that pokes values back into the underlying XML but I’ve not got any property change notification for the case where the XML value is updated.

I deliberately structured what I wrote a particular way so that the Text property actually bound to a function on an XAttribute whereas I could have done it something like this;

 <ListBox
      ItemsSource="{Binding 
      Source={StaticResource myXmlDataSource},
      Path=Xml.[descendant-or-self::child]}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBox
            Text="{Binding Path=[string(attribute::value)],Mode=TwoWay}" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>

and then the code that I’ve written would fall apart because it doesn’t “know” how to get back to the relevant element/attribute once it has evaluated the string(attribute::value) expression whereas the previous structure where I just called string() gave it a “context” of an XAttribute that it could get back to later and set values against.

If you’ve not encountered it yet then the new XPath support in Silverlight is in ( not surprisingly ) System.Xml.XPath and I used it to write this little ( sketchy ) class;

  public class BindableXNode
  {
    XPathNavigator navigator;
    IXmlNamespaceResolver resolver;

    public BindableXNode(XNode node, IXmlNamespaceResolver resolver = null)
    {
      this.navigator = node.CreateNavigator();
      this.resolver = resolver;
    }
    private BindableXNode(XPathNavigator navigator, IXmlNamespaceResolver resolver)
    {
      this.navigator = navigator.Clone();
      this.resolver = resolver;
    }
    public object this[string indexer]
    {
      get
      {
        object result = this.navigator.Evaluate(indexer, this.resolver);

        if ((result is bool) || (result is string) || (result is double))
        {
          return (result);
        }
        else
        {
          XPathNodeIterator iterator = (XPathNodeIterator)result;

          List<BindableXNode> items = new List<BindableXNode>();

          while (iterator.MoveNext())
          {
            items.Add(new BindableXNode(iterator.Current,
              this.resolver));
          }
          return (items);
        }
      }
      set
      {
        string stringValue = value.ToString();

        if (this.navigator.UnderlyingObject is XElement)
        {
          XElement xElement = (XElement)this.navigator.UnderlyingObject;
          xElement.Value = stringValue;
        }
        else if (this.navigator.UnderlyingObject is XAttribute)
        {
          XAttribute xAttribute = (XAttribute)this.navigator.UnderlyingObject;
          xAttribute.Value = stringValue;
        }
        else
        {
          throw new NotSupportedException();
        }
      }
    }
  }

and so the idea is that I can “walk up” to this class with an XNode and create a “BindableXNode”. By the way – XNode.CreateNavigator() is an extension method that comes with the XPath support.

That means I can write code such as;

      XElement xElement = new XElement(
        "sequence",
        from i in Enumerable.Range(1, 10)
        select
          new XElement("number",
            new XAttribute("value", i),
            new XAttribute("square", i * i)));

      BindableXNode bindableNode = new BindableXNode(xElement);

      double sum = 
        (double)bindableNode["sum(descendant-or-self::number/attribute::square)"];

and the BindableXNode does the work of turning the XPath into a double for me via a string indexer.

Then I need a class that can do the loading work for me so I wrote this little XmlDataSource;

  [ContentProperty("NamespaceMappings")]
  public class XmlDataSource : PropertyChangeNotification
  {
    public XmlDataSource()
    {
      this.NamespaceMappings = new List<XmlNamespaceMapping>();
    }
    public List<XmlNamespaceMapping> NamespaceMappings { get; set; }

    public Uri ResourceUri
    {
      get
      {
        return (_ResourceUri);
      }
      set
      {
        _ResourceUri = value;
        Validate();
        base.RaisePropertyChanged("ResourceUri");
        LoadXml();
      }
    }
    Uri _ResourceUri;

    public Uri DownloadUri
    {
      get
      {
        return (_DownloadUri);
      }
      set
      {
        _DownloadUri = value;
        Validate();
        base.RaisePropertyChanged("DownloadUri");
        LoadXml();
      }
    }
    Uri _DownloadUri;

    public BindableXNode Xml
    {
      get
      {
        return (_Xml);
      }
      internal set
      {
        _Xml = value;
        base.RaisePropertyChanged("Xml");
      }
    }
    BindableXNode _Xml;

    void Validate()
    {
      if ((this.DownloadUri != null) && (this.ResourceUri != null))
      {
        throw new InvalidOperationException(
          "Cannot set both the resource and the download Uri");
      }
    }
    void LoadXml()
    {
      if (this.ResourceUri != null)
      {
        using (XmlReader reader = XmlReader.Create(
          this.ResourceUri.ToString()))
        {
          ReadXml(reader);
          reader.Close();
        }
      }
      else if (this.DownloadUri != null)
      {
        WebClient webClient = new WebClient();

        webClient.OpenReadCompleted += (s, e) =>
          {
            if (e.Error == null)
            {
              using (XmlReader reader = XmlReader.Create(e.Result))
              {
                ReadXml(reader);
                reader.Close();
              }
            }
          };

        webClient.OpenReadAsync(this.DownloadUri);
      }
    }
    void ReadXml(XmlReader reader)
    {
      XElement xElement = XElement.Load(reader);

      if (this.NamespaceMappings.Count > 0)
      {
        XmlNamespaceManager manager = new XmlNamespaceManager(
          new NameTable());

        foreach (var item in this.NamespaceMappings)
        {
          manager.AddNamespace(item.Prefix, item.Namespace);
        }
        this.Xml = new BindableXNode(xElement, manager);
      }
      else
      {
        this.Xml = new BindableXNode(xElement);
      }
    }
  }

which really just “knows” how to grab some XML via [content/resource] ( by setting the ResourceUri property ) or via a download using WebClient ( by setting the DownloadUri property ).

It then surfaces the XML that it’s loaded via its Xml property and that Xml property is a BindableXNode so that I can then use the string indexer to evaluate XPaths.

There’s then just a little helper class to manage the set up of the XML namespace mappings;

  public class XmlNamespaceMapping
  {
    public string Prefix { get; set; }
    public string Namespace { get; set; }
  }

As I said at the start – it’s just a simple sketch rather than something concrete but I wonder if the idea has merit? I figured I’d build it into a little Twitter example and so included that below along with the project source code. To call Twitter I think you have to run elevated and out of browser due to a lack of cross domain policy but I put together a simple UI that does a declaratively bound search as in;

<UserControl
  x:Class="SilverlightApplication3.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:xmlc="clr-namespace:SilverlightApplication3.XmlClasses"
  mc:Ignorable="d"
  d:DesignHeight="300"
  d:DesignWidth="400">
  <UserControl.Resources>
    <xmlc:XmlDataSource
      x:Key="myXmlDataSource"
      DownloadUri="http://search.twitter.com/search.atom?q=mtaulty">
      <xmlc:XmlNamespaceMapping
        Prefix="atom"
        Namespace="http://www.w3.org/2005/Atom" />
    </xmlc:XmlDataSource>
  </UserControl.Resources>
  <Grid
    x:Name="LayoutRoot">
    <ListBox
      ItemsSource="{Binding 
        Source={StaticResource myXmlDataSource},
        Path=Xml.[descendant-or-self::atom:entry]}">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <StackPanel>
            <TextBlock
              Text="{Binding Path=[string(child::atom:title)]}" />
            <TextBlock
              Text="{Binding Path=[string(child::atom:published)]}" />
          </StackPanel>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>
</UserControl>

as I say – just a sketch but here’s the project bits for download ( VS 2010 RC ) – I might take it further over time…