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.