Mike Taulty's Blog
Bits and Bytes from Microsoft UK
Databinding XML in Silverlight 2
Mike Taulty's Blog

Mike's Badges

Follow on Twitter
View mike's profile on slideshare
Add to Technorati Favorites
CW Blog Awards

I passed across some code to Daniel just last night as he was struggling to get LINQ to XML to read some XML data from a web site ( actually, a web service ) for him.

One of the things I miss in Silverlight ( from WPF ) is the ability to naturally bind to XML data and this discussion with Daniel prompted to wonder what I could do about writing classes that make XML data binding more declarative.

Approach 1 ( not going to work )

My first instinct was to see if I could implement my own markup extension so that in markup I could write something like;

<ListBox ItemsSource="{XmlBinding Source=foo.xml, Path=/foo/item}"/>

and then maybe in the template for that ListBox I could use something like;

<TextBlock Text="{XmlBinding Path=@value}"/>

or something along those lines. However, there's a number of things that would stop me being able to do that;

Silverlight doesn't support custom markup extensions like {XmlBinding} above so, naturally, that's really "game over" for this idea.

There's also a couple of other reasons why I'd struggle.

Firstly, Silverlight has LINQ to XML as its XML API ( which is great as I love that API ) but it doesn't seem to pick up a feature of the API from the full .NET Framework V3.5 version which is the part of the API that lives in System.Xml.XPath and allows you to take an XElement and make it effectively IXPathNavigable by calling CreateNavigator() on it.

In the full V3.5 Framework, this provides a link between the "LINQ to XML" approach and the "XML DOM" approach represented by XmlDocument, XPathNavigator and so on and it makes sense that this doesn't exist in Silverlight because the DOM approach (in the XmlDocument sense) isn't there.

The second reason why I struggle is to do with when and where XML is loaded from. In Silverlight if you're doing;

XElement.Load("myFile.xml");

then the myFile.xml is going to be loaded from your application package (i.e. the XAP).

That's fine but it won't work if your file is on a web server somewhere and you need to download it. However, you can easily achieve that by using WebClient to download the file asynchronously and when the resultant Stream comes back you can use XmlReader.Create( stream ) and then use XElement.Load() on the XmlReader.

That all adds some complexity to the simplistic pseudo-syntax I used above because at the time of binding we might not have yet download the XML if we're doing it asynchronously so it gets a bit tricky.

Approach 2 ( equally, not going to work )

Giving up on the first approach, I wondered if I could come up with something a little like this;

<ListBox>

    <ListBox.ItemsSource>

      <custom:XmlDataSource Uri="foo.xml" ElementPath="/foo/item"/>

    </ListBox.ItemsSource>

</ListBox>

and then maybe use it with a data template that looks something like this ( assuming that the item element has an attribute on it called @value )

<TextBlock Text="{Binding Value}"/>

the problem I've then got is that I need to make sure that whatever object I bind this to has a property called Value on it. That's fine for one specific example :-) but it's not very general purpose because it's a bit tricky to construct a .NET type at runtime which has a property with an arbitrary name on it as you might choose to pick out any attribute or element content in that Binding statement.

Now, I might have tried to solve that problem with anonymous types but Silverlight doesn't support binding to anonymous types.

Approach 3 ( doesn't work either )

A modification on the second approach. Given that I'm going to find it difficult to generate a real .NET type with dynamically named properties to support binding, maybe I can be more generic and end up with something like;

<TextBlock Text="{Binding Attributes['value']}"/>

that is, define a class that has array-based properties on it such as Attributes and Elements and then just allow the binding syntax to index in to those properties thereby reaching into the underlying XML.

However, binding syntax does not allow you to use things like [] within the expressions and doesn't look to support binding to arrays in this way.

Getting Stuck

By now, I'm getting a little stuck.

I pondered dynamic dispatch and maybe going down the DLR route to see how that helped but I didn't do it.

Moving On

I started to think about whether there's enough of Reflection in Silverlight to dynamically build a class that implements the properties that I'm being asked for.

That is, returning to approach 2 above and trying to get something like;

<ListBox>

  <ListBox.ItemsSource>

    <custom:XmlDataSource Uri="foo.xml" SelectPath="/foo/item">

        <custom:XmlDataSource.ValuePath Attribute="Name"/>

        <custom:XmlDataSource.ValuePath Attribute="Title"/>

        <custom:XmlDataSource.ValuePath Element="Age"/>

    </custom:XmlDataSource>

  </ListBox.ItemsSource>

</ListBox>

That would mean that when I encounter one of these I can look at all the names that are being asked for "up front" and then maybe dynamically create a type that actually has those properties on it to pacify databinding meaning that I could then have;

<TextBlock Text="{Binding Age}"/>

and that might work.

I worked on this for a little while and managed to get my XAML into this kind of state;

<UserControl
  x:Class="SilverlightApplication17.Page"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="400"
  Height="300"
  xmlns:local="clr-namespace:SilverlightApplication17">
  <Grid
    x:Name="LayoutRoot"
    Background="White">
    <ListBox>
      <ListBox.ItemsSource>
        <local:XmlDataSource
          XmlSource="test.xml"
          ItemPath="/root/child/grandchild">
          <local:XmlDataSource.ValuePaths>
            <local:XmlValuePath
              Attribute="attr1" />
            <local:XmlValuePath
              Attribute="attr2" />
            <local:XmlValuePath
              Attribute="attr3" />
            <local:XmlValuePath
              Element="el1" />
          </local:XmlDataSource.ValuePaths>
        </local:XmlDataSource>
      </ListBox.ItemsSource>
      <ListBox.ItemTemplate>
        <DataTemplate>
          <StackPanel
            Orientation="Horizontal">
            <TextBlock
              Text="{Binding attr1}" />
            <TextBlock
              Text="{Binding attr2}" />
            <TextBlock
              Text="{Binding attr3}" />
            <TextBlock
              Text="{Binding el1}" />            
          </StackPanel>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>
</UserControl>

And that is in use against an XML file which looks like this ( the only XML file I've tested so far ) ;

<?xml version="1.0" encoding="utf-8" ?>
<root>
  <child>
    <grandchild attr1="A"
                attr2="B"
                attr3="C">
      <el1>One</el1>
    </grandchild>
  </child>
  <child>
    <grandchild attr1="D"
                attr2="E"
                attr3="F">
      <el1>Two</el1>
    </grandchild>    
  </child>
  <child>
    <grandchild attr1="G"
                attr2="H"
                attr3="I">
      <el1>Three</el1>
    </grandchild>    
  </child>
</root>

and then there's a bunch of supporting code, specifically the XmlDataSource class;

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Markup;
using System.Xml.Linq;

namespace SilverlightApplication17
{
  public class XmlValuePath
  {
    public string Attribute { get; set; }
    public string Element { get; set; }

    public string Name
    {
      get
      {
        return (string.IsNullOrEmpty(Attribute) ? Element : Attribute);
      }
    }
  }
  [ContentProperty("ValuePaths")]
  public class XmlDataSource : IEnumerable
  {
    public XmlDataSource()
    {
      valuePaths = new List<XmlValuePath>();
    }
    public string XmlSource { get; set; }
    public string ItemPath { get; set; }

    public List<XmlValuePath> ValuePaths
    {
      get
      {
        return (valuePaths);
      }
      set
      {
        valuePaths = value;
      }
    }
    public IEnumerator GetEnumerator()
    {
      XElement xml = XElement.Load(XmlSource);

      IEnumerable<XElement> matchingElements =
        MatchXElements(xml);

      List<Object> objects = new List<object>();

      Type dynamicType = ReflectionHelper.BuildTypeForValuePaths(ValuePaths);

      foreach (XElement element in matchingElements)
      {
        object entry = Activator.CreateInstance(dynamicType);

        foreach (XmlValuePath path in ValuePaths)
        {
          string value = string.IsNullOrEmpty(path.Attribute) ? 
            element.Element(path.Element).Value : element.Attribute(path.Attribute).Value;

          ReflectionHelper.SetPropertyValue(entry, path.Name, value);          
        }
        objects.Add(entry);
      }
      return (objects.GetEnumerator());
    }
    private IEnumerable<XElement> MatchXElements(XElement xml)
    {
      string[] paths = ItemPath.TrimStart('/').Split('/');

      IEnumerable<XElement> elements = null;

      if (xml.Name == paths[0])
      {
        elements = new List<XElement>() { xml };
      }
      for (int i = 1; i < paths.Length; i++)
      {
        elements =
          (from e in elements.Elements()
           where e.Name == paths[i]
           select e).ToList();
      }
      return (elements);
    }
    private List<XmlValuePath> valuePaths;
  }
}

which is backed by this ReflectionHelper class;

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace SilverlightApplication17
{
  public static class ReflectionHelper
  {
    public static void SetPropertyValue(object targetObject, string property, string value)
    {
      PropertyInfo pi = targetObject.GetType().GetProperty(property);

      if (pi == null)
      {
        throw new ArgumentException(
          "Can not find the object property to set - binding/XmlDataSource mismatch?");
      }
      pi.SetValue(targetObject, value, null);
    }
    public static Type BuildTypeForValuePaths(List<XmlValuePath> paths)
    {
      if ((paths == null) || (paths.Count == 0))
      {
        throw new ArgumentException("Paths null or empty");
      }
      foreach (XmlValuePath path in paths)
      {
        if (string.IsNullOrEmpty(path.Attribute) && (string.IsNullOrEmpty(path.Element)))
        {
          throw new ArgumentException("No attribute or element specified");
        }
        if (!string.IsNullOrEmpty(path.Attribute) && (!string.IsNullOrEmpty(path.Element)))
        {
          throw new ArgumentException("Both attribute and element specified - invalid");
        }
      }
      
      // TODO: Better way to name these types than this...
      string typeName = string.Format("BoundType_{0}", paths.GetHashCode());

      Type type = DynamicModule.GetType(typeName);

      if (type == null)
      {
        TypeBuilder tb =
          DynamicModule.DefineType("Foo", TypeAttributes.Class | TypeAttributes.Public, typeof(object));

        tb.DefineDefaultConstructor(MethodAttributes.Public);

        foreach (XmlValuePath path in paths)
        {
          DefineStringProperty(tb, path.Name);
        }
        type = tb.CreateType();
      }
      return (type);
    }
    private static string BackingFieldName(string propName)
    {
      return(string.Format("_{0}", propName));
    }
    private static void DefineStringProperty(TypeBuilder typeBuilder, string propName)
    {
      // CREDIT: Code taken from MSDN sample :-)
      FieldBuilder fieldBuilder = typeBuilder.DefineField(BackingFieldName(propName),
        typeof(string), FieldAttributes.Private);

      PropertyBuilder propBuilder = typeBuilder.DefineProperty(propName, 
        PropertyAttributes.HasDefault, typeof(string), null);

      MethodAttributes getSetAttr =
          MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;

      MethodBuilder methGetBuilder = typeBuilder.DefineMethod(string.Format("get_{0}", propName), 
        getSetAttr, typeof(string), Type.EmptyTypes);

      ILGenerator propGetIL = methGetBuilder.GetILGenerator();

      propGetIL.Emit(OpCodes.Ldarg_0);
      propGetIL.Emit(OpCodes.Ldfld, fieldBuilder);
      propGetIL.Emit(OpCodes.Ret);

      // Define the "set" accessor method for CustomerName.
      MethodBuilder methSetBuilder = typeBuilder.DefineMethod(
        string.Format("set_{0}", propName), getSetAttr, null, new Type[] { typeof(string) });

      ILGenerator propSetIL = methSetBuilder.GetILGenerator();

      propSetIL.Emit(OpCodes.Ldarg_0);
      propSetIL.Emit(OpCodes.Ldarg_1);
      propSetIL.Emit(OpCodes.Stfld, fieldBuilder);
      propSetIL.Emit(OpCodes.Ret);

      // Last, we must map the two methods created above to our PropertyBuilder to 
      // their corresponding behaviors, "get" and "set" respectively. 
      propBuilder.SetGetMethod(methGetBuilder);
      propBuilder.SetSetMethod(methSetBuilder);
    }
    private static ModuleBuilder DynamicModule
    {
      get
      {
        if (dynamicModule == null)
        {
          dynamicModule = DynamicAssembly.DefineDynamicModule("XmlDataSourceModule");
        }
        return (dynamicModule);
      }
    }
    private static AssemblyBuilder DynamicAssembly
    {
      get
      {
        if (dynamicAssembly == null)
        {
          dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
            new AssemblyName("XmlDataSource"), AssemblyBuilderAccess.Run);
        }
        return (dynamicAssembly);
      }
    }
    static ModuleBuilder dynamicModule;
    static AssemblyBuilder dynamicAssembly;
  }
}

Now, naturally, these are nowhere near as good as they could be and will be riddled with bugs as I was just playing around here but it's a start towards having something that's a bit easier than having to write code every time I want to consume some XML.

But It's Still Synchronous

Even with my hacked together code in place, I still have the "problem" that I can only load XML that's defined in my project itself, I can't also load it from ( say ) the site of origin at runtime.

I changed my XmlDataSource to have a property on it XmlItems which can then be data bound to as an ItemsSource for my ListBox and I can then try and load the XML from the XAP and if that fails I can try and fall back to using WebClient and fire a property changed notification when I finally get around to loading the XML.

My XAML then ended up looking like this;

<UserControl
  x:Class="SilverlightApplication17.Page"
  xmlns="http://schemas.microsoft.com/client/2007"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Width="400"
  Height="300"
  xmlns:local="clr-namespace:SilverlightApplication17">
  <Grid
    x:Name="LayoutRoot"
    Background="White">
    <Grid.Resources>
      <local:XmlDataSource
        x:Key="xmlData"
        XmlSource="test.xml"
        ItemPath="/root/child/grandchild">
        <local:XmlDataSource.ValuePaths>
          <local:XmlValuePath
            Attribute="attr1" />
          <local:XmlValuePath
            Attribute="attr2" />
          <local:XmlValuePath
            Attribute="attr3" />
          <local:XmlValuePath
            Element="el1" />
        </local:XmlDataSource.ValuePaths>
      </local:XmlDataSource>
    </Grid.Resources>
    <ListBox
      DataContext="{StaticResource xmlData}"
      ItemsSource="{Binding XmlItems}">
    <ListBox.ItemTemplate>
        <DataTemplate>
          <StackPanel
            Orientation="Horizontal">
            <TextBlock
              Text="{Binding attr1}" />
            <TextBlock
              Text="{Binding attr2}" />
            <TextBlock
              Text="{Binding attr3}" />
            <TextBlock
              Text="{Binding el1}" />            
          </StackPanel>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>
</UserControl>

That is, I've moved my XmlDataSource into a resource and then just bound my ListBox to it and the ItemsSource to its XmlItems property. Then I've changed my XmlDataSource class;

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Markup;
using System.Xml.Linq;
using System.ComponentModel;
using System.Net;
using System.Xml;

namespace SilverlightApplication17
{
  public class XmlValuePath
  {
    public string Attribute { get; set; }
    public string Element { get; set; }

    public string Name
    {
      get
      {
        return (string.IsNullOrEmpty(Attribute) ? Element : Attribute);
      }
    }
  }
  [ContentProperty("ValuePaths")]
  public class XmlDataSource : IEnumerable, INotifyPropertyChanged
  {
    public XmlDataSource()
    {
      valuePaths = new List<XmlValuePath>();
    }
    public string XmlSource { get; set; }
    public string ItemPath { get; set; }

    public List<XmlValuePath> ValuePaths
    {
      get
      {
        return (valuePaths);
      }
      set
      {
        valuePaths = value;
      }
    }
    public IEnumerable XmlItems
    {
      get
      {
        IEnumerable enumerable = null;

        if (xml == null)
        {
          LoadXml();
        }
        else
        {
          // TODO: Not sure returning the same object each time here
          // is a good idea although I might get away with it because
          // my XML doesn't change.
          enumerable = this;
        }
        return (enumerable);
      }
    }
    public IEnumerator GetEnumerator()
    {
      List<object> objects = new List<object>();

      if (xml != null)
      {
        IEnumerable<XElement> matchingElements = MatchXElements(xml);

        Type dynamicType = ReflectionHelper.BuildTypeForValuePaths(ValuePaths);

        foreach (XElement element in matchingElements)
        {
          object entry = Activator.CreateInstance(dynamicType);

          foreach (XmlValuePath path in ValuePaths)
          {
            string value = string.IsNullOrEmpty(path.Attribute) ?
              element.Element(path.Element).Value : element.Attribute(path.Attribute).Value;

            ReflectionHelper.SetPropertyValue(entry, path.Name, value);
          }
          objects.Add(entry);
        }
      }
      return (objects.GetEnumerator());
    }
    private IEnumerable<XElement> MatchXElements(XElement xml)
    {
      string[] paths = ItemPath.TrimStart('/').Split('/');

      IEnumerable<XElement> elements = null;

      if (xml.Name == paths[0])
      {
        elements = new List<XElement>() { xml };
      }
      for (int i = 1; i < paths.Length; i++)
      {
        elements =
          (from e in elements.Elements()
           where e.Name == paths[i]
           select e).ToList();
      }
      return (elements);
    }
    private void LoadXml()
    {
      try
      {
        xml = XElement.Load(XmlSource);
        FirePropertyChanged("XmlItems");
      }
      catch
      {
      }
      if (xml == null)
      {
        WebClient client = new WebClient();
        client.OpenReadCompleted += OnAsyncReadCompleted;
        client.OpenReadAsync(new Uri(XmlSource, UriKind.RelativeOrAbsolute));
      }
    }
    void OnAsyncReadCompleted(object sender, OpenReadCompletedEventArgs e)
    {
      if (e.Error == null)
      {
        xml = XElement.Load(XmlReader.Create(e.Result));
        FirePropertyChanged("XmlItems");
      }
    }
    private void FirePropertyChanged(string property)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this,
          new PropertyChangedEventArgs(property));
      }
    }
    private List<XmlValuePath> valuePaths;
    private XElement xml;
    public event PropertyChangedEventHandler PropertyChanged;
  }
}

And, finally, that seems to "work" ( in the sense of having tried it once and being very suspicious about large parts of it :-) ) in the sense that I can put my test.xml into my XAP or I can put my test.xml file onto the web server at the site of origin and either way this code finds it and loads up my ListBox from it.

That's It ( For Now )

This all started because I was thinking about it in the car on the way home today ( 4 hours gives you a lot of time for thinking :-) ) and I just wanted to hack some things together to experiment with it. I've uploaded the project file here for you to download if you want the code - bear in mind that it's not production code and comes with all the usual caveats.


Posted Fri, Apr 18 2008 11:41 AM by mtaulty
Filed under:

Comments

game server wrote game server
on Fri, Apr 18 2008 7:32 PM
Rob Relyea - Xamlified wrote WPF/Silverlight/XAML Web Links - 2004/04/18
on Fri, Apr 18 2008 8:36 PM
&amp;#160; WPF Apps Jaime Rodriguez&amp;#39;s list of WPF in Line-of-Business case studies CodePlex Project:
Rob Relyea - Xamlified wrote WPF/Silverlight/XAML Web Links - 2008/04/18
on Fri, Apr 18 2008 9:01 PM
WPF Apps Jaime Rodriguez&amp;#39;s list of WPF in Line-of-Business case studies CodePlex Project: Slick Code
used car parts wrote used car parts
on Mon, Apr 21 2008 12:20 AM
Databinding XML in Silverlight 2 wrote Databinding XML in Silverlight 2
on Mon, Apr 21 2008 11:33 PM
Mike Taulty's Blog discussed different approaches on how to make XML data binding more declarative in Silverlight. Read on his blog what are the pros and cons for each approach and choose the best that fits you. He also shows why some approaches that
Hot Topics wrote Databinding XML in Silverlight 2.0
on Sat, Apr 26 2008 10:53 AM
Q: What do you get when you bring the data side of Mike Taulty&amp;#39;s brain together with the design side
IT Ramblings wrote DataBinding XML in SilverLight v2
on Mon, Feb 9 2009 7:47 AM

DataBinding XML in SilverLight v2

(C) Mike Taulty, 2009. All rights reserved. The information in this weblog is provided "AS IS" with no warranties, and confers no rights. This weblog does not represent the thoughts, intentions, plans or strategies of my employer. It is solely my opinion. Inappropriate comments will be deleted at the authors discretion. All code samples are provided "AS IS" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.
Powered by Community Server (Non-Commercial Edition), by Telligent Systems