There are quite a few discussions and examples on using DynamicObject to create wrappers for XML documents already out here. However I found most of them lacking. Here’s my take on this.
This is largely based on this example: Dynamic in C# 4.0: Creating Wrappers with DynamicObject (you’ll probably need to read this first to understand what I’m talking about here as mine is an extended version of this).
I made some important additions like handling attributes and dealing with collections of elements. My goal here was to keep the fluency of working with XML this way, not to cover every base. It’s not a perfect abstraction by any means nor is it a complete one. But it works for me.
For accessing XML attributes, I went with an indexer syntax like a dictionary class:
dynamicXMLObject[“AttributeName”] = “AttributeValue”
Note that is you set a value to null it removes the attribute. Setting a value for an existing attribute replaced it. This all happens in the TrySetIndex() and TryGetIndex() methods of this wrapper class and it pretty self-explanatory.
For dealing with collections of elements, you just access the underlying XElement collection method of choice:
dynamic elements = dynamicXMLObject.Elements()
This was possible in the orignal example but I added the ability to return wrapped collections of DynamicXMLNode. When you call any underlying method that returns an IEnumerable<XElement> collection, I instead return an IEnumberable<DynamicXMLNode> collection . This bit of magic is probably the most interesting part as I detect the return type for any underlying method called and substitute in the wrapped collection as necessary. This allow all the members of XElement that return collections of XElement to return collections of DynamicXMLNode instead, transparently. Note that in your dynamic code, you’ll need to cast this collection to IEnumerable<dynamic> to access all the handy Linq extension methods, etc…
Along those same lines, I also detect and convert underlying method parameters so that you can call XElement.Add(), etc… and pass in DynamicXMLNode. When you do I instead substitute the inner wrapped XElement as necessary. This allows for transparent use of DynamicXMLNode in most cases.
Both of these detections and substitution happen in TryInvokeMember(). The combination of these two substitutions allow any of the underlying methods of the wrapped XElement to transparently work with the DynamicXMLNode wrapper class.
I also cloned some of the useful static XElement methods, like Load() and Parse(). I would have liked to handle this dynamically like other method calls but it seems you can’t trap static method calls with DynamicObject. Oh well.
The only hitch is that for XElement methods that expect an XName object like Elements() or Descendants() you’ll have to cast any strings you use to an XName in your dynamic code. Normally with static typing this is not necessary as it happens behind the scene via an implicit cast done for you. However with dynamic code the compiler can’t do this for you.
I suppose that I could snoop the parameters to all the methods of XElement and do this cast automatically, but I’ll leave that for the next person to figure out.
public class DynamicXMLNode : DynamicObject
{
private XElement node;
public DynamicXMLNode(XElement node)
{
this.node = node;
}
public DynamicXMLNode(String name)
{
node = new XElement(name);
}
public static DynamicXMLNode Load(string uri)
{
return new DynamicXMLNode(XElement.Load(uri));
}
public static DynamicXMLNode Load(Stream stream)
{
return new DynamicXMLNode(XElement.Load(stream));
}
public static DynamicXMLNode Load(TextReader textReader)
{
return new DynamicXMLNode(XElement.Load(textReader));
}
public static DynamicXMLNode Load(XmlReader reader)
{
return new DynamicXMLNode(XElement.Load(reader));
}
public static DynamicXMLNode Load(Stream stream, LoadOptions options)
{
return new DynamicXMLNode(XElement.Load(stream, options));
}
public static DynamicXMLNode Load(string uri, LoadOptions options)
{
return new DynamicXMLNode(XElement.Load(uri, options));
}
public static DynamicXMLNode Load(TextReader textReader, LoadOptions options)
{
return new DynamicXMLNode(XElement.Load(textReader, options));
}
public static DynamicXMLNode Load(XmlReader reader, LoadOptions options)
{
return new DynamicXMLNode(XElement.Load(reader, options));
}
public static DynamicXMLNode Parse(string text)
{
return new DynamicXMLNode(XElement.Parse(text));
}
public static DynamicXMLNode Parse(string text, LoadOptions options)
{
return new DynamicXMLNode(XElement.Parse(text, options));
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
XElement setNode = node.Element(binder.Name);
if (setNode != null)
{
setNode.SetValue(value);
}
else
{
if (value.GetType() == typeof(DynamicXMLNode))
{
node.Add(new XElement(binder.Name));
}
else
{
node.Add(new XElement(binder.Name, value));
}
}
return true;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
XElement getNode = node.Element(binder.Name);
if (getNode != null)
{
result = new DynamicXMLNode(getNode);
return true;
}
else
{
result = null;
return false;
}
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
Type xmlType = typeof(XElement);
try
{
// unwrap parameters: if the parameters are DynamicXMLNode then pass in the inner XElement node
List<object> newargs = null;
var argtypes = args.Select(x => x.GetType()); // because GetTypeArray is not supported in Silverlight
if (argtypes.Contains(typeof(DynamicXMLNode)) || argtypes.Contains(typeof(DynamicXMLNode[])))
{
newargs = new List<object>();
foreach (var arg in args)
{
if (arg.GetType() == typeof(DynamicXMLNode))
{
newargs.Add(((DynamicXMLNode)arg).node);
}
else if (arg.GetType() == typeof(DynamicXMLNode[]))
{
// unwrap array of DynamicXMLNode
newargs.Add(((DynamicXMLNode[])arg).Select(x => ((DynamicXMLNode)x).node));
}
else
{
newargs.Add(arg);
}
}
}
result = xmlType.InvokeMember(
binder.Name,
System.Reflection.BindingFlags.InvokeMethod |
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Instance,
null, node, newargs == null ? args : newargs.ToArray());
// wrap return value: if the results are an IEnumerable<XElement>, then return a wrapped collection of DynamicXMLNode
if (result != null && typeof(IEnumerable<XElement>).IsAssignableFrom(result.GetType()))
{
result = ((IEnumerable<XElement>)result).Select(x => new DynamicXMLNode(x));
}
// Note: we could wrap single XElement too but nothing returns that
return true;
}
catch
{
result = null;
return false;
}
}
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
{
if (indexes[0].GetType() == typeof(String))
{
node.SetAttributeValue((string)indexes[0], value);
return true;
}
else
{
return false;
}
}
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
if (indexes[0].GetType() == typeof(String))
{
XAttribute attr = node.Attribute((string)indexes[0]);
if (attr != null)
{
result = attr.Value;
return true;
}
else
{
result = null;
return false;
}
}
else
{
result = null;
return false;
}
}
public override bool TryConvert(ConvertBinder binder, out object result)
{
if(binder.Type == typeof(XElement))
{
result = node;
}
else if(binder.Type == typeof(String))
{
result = node.Value;
}
else
{
result = false;
return false;
}
return true;
}
}