Aaron Summernite
Aaron Summernite

Reputation: 365

XDocument binding Elements and Attributes

I have an XDocument like this one set as a DataContext of my Window:

Class MainWindow
    Public Sub New()
        InitializeComponent()
        Me.DataContext = <?xml version="1.0" encoding="utf-8"?>
                         <Sketch Format="A4" Author="Aaron" Created="..." Test="Value">
                             <Item Kind="Line" X1="50" Y1="50" X2="150" Y2="150">
                                 <Item Kind="Rect" X="10" Y="10" Width="30" Height="30"/>
                             </Item>
                             <Item Kind="Line" X1="250" Y1="250" X2="250" Y2="50">
                                 <Item Kind="Ellipse" X="10" Y="10" Width="30" Height="30"/>
                             </Item>
                             <Test Param="Value"/>
                         </Sketch>
    End Sub
End Class

Now in my frontend I test couple of different binding paths. All of them works with Elements, Element, Attribute, but Attributes doesn't seem to work for me. I consider it rather odd, because Elements is IEnumerable<XElement> and Attributes is IEnumerable<XAttribute> -- exactly the same kind of collection and everything.

<Window Height="320" Title="Main Window" Width="640" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="MainWindow">
    <UniformGrid Columns="3">
        <StackPanel>
            <Label Foreground="DimGray">Root.Elements.Count</Label>
            <Label Content="{Binding Path=Root.Elements.Count, FallbackValue=Loading…}"/>
            <Label Foreground="DimGray">Root.Attributes.Count</Label>
            <Label Content="{Binding Path=Root.Attributes.Count, FallbackValue=Loading…}"/>
            <Label Foreground="DimGray">Root.Element[Test]</Label>
            <Label Content="{Binding Path=Root.Element[Test], FallbackValue=Loading…}"/>
            <Label Foreground="DimGray">Root.Attribute[Test]</Label>
            <Label Content="{Binding Path=Root.Attribute[Test], FallbackValue=Loading…}"/>
        </StackPanel>
        <StackPanel>
            <Label Foreground="DimGray">Root.Elements</Label>
            <ListBox ItemsSource="{Binding Root.Elements}"/>
            <Label Foreground="DimGray">Root.Attributes</Label>
            <ListBox ItemsSource="{Binding Root.Attributes}"/>
        </StackPanel>
        <StackPanel>
            <TreeView ItemsSource="{Binding Root.Elements}">
                <TreeView.ItemTemplate>
                    <HierarchicalDataTemplate ItemsSource="{Binding Elements}">
                        <Label Content="{Binding Name}"/>
                    </HierarchicalDataTemplate>
                </TreeView.ItemTemplate>
            </TreeView>
        </StackPanel>
    </UniformGrid>
</Window>

Do you have any idea why everything binds correctly except Attributes? Any help is appreciated. I think is has (maybe) got something to do with a fact, that Element and Elements are inherited from XContainer, but this doesn't explain why XElements very own Attribute works...

Thanks in advance! Aaron

Upvotes: 1

Views: 1523

Answers (3)

svick
svick

Reputation: 244878

There is no property Attributes on XElement (only method Attributes() that can't be used directly in binding), so it's not surprising the binding doesn't work.

But there is also no property Elements, so why does that work? It's because LINQ to XML objects have special “dynamic properties” specifically for use in WPF, see LINQ to XML Dynamic Properties. There is a dynamic property Elements on XElement, but no Attributes.

There's still one thing I don't understand though: The Elements dynamic property is documented to work only in the form elem.Elements[elementName]. So it's still surprising to me that your code works.

If you want to know about any workarounds, I can't think of any, except for invoking the Attributes() method using <ObjectDataProvider>.

Upvotes: 2

DjScribbles
DjScribbles

Reputation: 118

A quick portable workaround for this issue is to run the XElement through a converter that returns its Attributes results. Then you can simply bind to the element.

I also filter out namespaces below, easily removable.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Xml.Linq;

namespace FSW.Core.Utility
{
    [ValueConversion(typeof(XElement), typeof(IEnumerable<XAttribute>))]
    public class XElementToXAttributesConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var element = value as XElement;
            return element?.Attributes().Where(x=>x.Name.LocalName != "xmlns");
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }

    }
}

Upvotes: 0

user487779
user487779

Reputation: 630

Svick is correct with his response. The reason why Elements works as you have discovered is that the custom CustomTypeDescriptor for XElement ( determined by the presence of the TypeDescriptionProviderAttribute on XElement ) provides a custom PropertyDescriptor with name Elements which returns an IEnumerable<XElement>. If this is followed in the binding path by an indexer then what is returned is XContainer.Elements(XName) otherwise will be XContainer.Elements(). The reason why Attributes does not work is that there is no such dynamic property descriptor provided.

The code below provides this missing functionality ( and also a Nodes property ) in a similar manner to the dynamic Elements property.

 //Add this code in App start up    
 TypeDescriptor.AddProvider(new XElementAdditionalDynamicPropertiesTypeDescriptionProvider(),
 typeof(XElement));

The classes below provide the functionality and this code is similar to how Elements work.

public class XDeferredAxis : IEnumerable<XAttribute>
{
    internal XElement element;
    private Func<XElement, XName, IEnumerable<XAttribute>> func;
    private XName name;

    public IEnumerator<XAttribute> GetEnumerator()
    {
        return this.func(this.element, this.name).GetEnumerator();
    }

    public XDeferredAxis(Func<XElement, XName, IEnumerable<XAttribute>> func, XElement element, XName name)
    {
        if (func == null)
        {
            throw new ArgumentNullException("func");
        }
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }
        this.func = func;
        this.element = element;
        this.name = name;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

public class XElementNodesPropertyDescriptor : PropertyDescriptor
{
    private XElement element;
    private bool childRemoved;
    public XElementNodesPropertyDescriptor() : base("Nodes", null)
    {

    }
    public override void AddValueChanged(object component, EventHandler handler)
    {
        bool flag = base.GetValueChangedHandler(component) != null;
        base.AddValueChanged(component, handler);
        if (!flag)
        {
            XElement local = component as XElement;
            if ((local != null) && (base.GetValueChangedHandler(component) != null))
            {
                element = local;
                local.Changing += new EventHandler<XObjectChangeEventArgs>(this.OnChanging);
                local.Changed += new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
            }
        }
    }

    private void OnChanging(object sender, XObjectChangeEventArgs e)
    {
        childRemoved = false;
        if (e.ObjectChange == XObjectChange.Remove)
        {
            XObject senderNode = (XObject)sender;
            if (senderNode.Parent == element)
            {
                childRemoved = true;
            }
        }
    }

    private void OnChanged(object sender, XObjectChangeEventArgs e)
    {
        XObject senderNode = (XObject)sender;
        switch (e.ObjectChange)
        {
            case XObjectChange.Add:
            case XObjectChange.Value:
            case XObjectChange.Name:
                if (senderNode.Parent == element)
                {
                    this.OnValueChanged(element, EventArgs.Empty);
                }
                break;
            case XObjectChange.Remove:
                if (childRemoved)
                {
                    this.OnValueChanged(element, EventArgs.Empty);
                }
                break;

        }
    }
    public override void RemoveValueChanged(object component, EventHandler handler)
    {
        base.RemoveValueChanged(component, handler);
        XElement local = component as XElement;
        if ((local != null) && (base.GetValueChangedHandler(component) == null))
        {
            local.Changed -= new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
        }
    }

    public override bool SupportsChangeEvents
    {
        get
        {
            return true;
        }
    }
    public override Type ComponentType
    {
        get
        {
            return typeof(XElement);
        }
    }

    public override bool IsReadOnly
    {
        get
        {
            return true;
        }
    }

    public override Type PropertyType
    {
        get
        {
            return typeof(IEnumerable<XNode>);
        }
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override object GetValue(object component)
    {
        var nodes= (component as XElement).Nodes();
        return nodes;
    }

    public override void ResetValue(object component)
    {

    }

    public override void SetValue(object component, object value)
    {

    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
}

public class XElementAttributesPropertyDescriptor : PropertyDescriptor
{
    private XDeferredAxis value;
    private bool removalIsOwnAttribute;
    public XElementAttributesPropertyDescriptor() : base("Attributes", null) {

    }
    public override void AddValueChanged(object component, EventHandler handler)
    {
        bool flag = base.GetValueChangedHandler(component) != null;
        base.AddValueChanged(component, handler);
        if (!flag)
        {
            XElement local = component as XElement;
            if ((local != null) && (base.GetValueChangedHandler(component) != null))
            {
                local.Changing += new EventHandler<XObjectChangeEventArgs>(this.OnChanging);            
                local.Changed += new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
            }
        }
    }

    private void OnChanging(object sender, XObjectChangeEventArgs e)
    {
        removalIsOwnAttribute = false;
        if (e.ObjectChange == XObjectChange.Remove)
        {
            var xAttribute = sender as XAttribute;
            if (xAttribute != null && xAttribute.Parent == value.element)
            {
                removalIsOwnAttribute = true;
            }
        }
    }

    private void OnChanged(object sender, XObjectChangeEventArgs e)
    {
        var changeRequired = false;
        var xAttribute = sender as XAttribute;

        if (xAttribute != null)
        {
            switch (e.ObjectChange)
            {
                case XObjectChange.Name:
                case XObjectChange.Add:
                    if (xAttribute.Parent == value.element)
                    {
                        changeRequired = true;
                    }
                    break;
                case XObjectChange.Remove:
                    changeRequired = removalIsOwnAttribute;
                    break;
            }
            if (changeRequired)
            {
                this.OnValueChanged(value.element, EventArgs.Empty);
            }
        }
    }
    public override void RemoveValueChanged(object component, EventHandler handler)
    {
        base.RemoveValueChanged(component, handler);
        XElement local = component as XElement;
        if ((local != null) && (base.GetValueChangedHandler(component) == null))
        {
            local.Changed -= new EventHandler<XObjectChangeEventArgs>(this.OnChanged);
        }
    }

    public override bool SupportsChangeEvents
    {
        get
        {
            return true;
        }
    }
    public override Type ComponentType
    {
        get
        {
            return typeof(XElement);
        }
    }

    public override bool IsReadOnly
    {
        get
        {
            return true;
        }
    }

    public override Type PropertyType
    {
        get
        {
            return typeof(IEnumerable<XAttribute>);
        }
    }

    public override bool CanResetValue(object component)
    {
        return false;
    }

    public override object GetValue(object component)
    {
        return (object)(this.value = new  XDeferredAxis((Func<XElement, XName, IEnumerable<XAttribute>>)((e, n) =>
        {
            if (!(n != (XName)null))
                return e.Attributes();
            return e.Attributes(n);
        }), component as XElement, (XName)null));
    }

    public override void ResetValue(object component)
    {

    }

    public override void SetValue(object component, object value)
    {

    }

    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }
}

public class XElementAdditionalDynamicPropertiesTypeDescriptionProvider: TypeDescriptionProvider
{
    public XElementAdditionalDynamicPropertiesTypeDescriptionProvider() : this(TypeDescriptor.GetProvider(typeof(XElement))) { }

    protected XElementAdditionalDynamicPropertiesTypeDescriptionProvider(TypeDescriptionProvider parent) : base(parent) { }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        var baseTypeDescriptor= base.GetTypeDescriptor(objectType, instance);
        return new XElementAdditionalDynamicPropertiesTypeDescriptor(baseTypeDescriptor);
    }
}

public class XElementAdditionalDynamicPropertiesTypeDescriptor : CustomTypeDescriptor
{
    public XElementAdditionalDynamicPropertiesTypeDescriptor(ICustomTypeDescriptor original) : base(original) { }
    public override PropertyDescriptorCollection GetProperties()
    {
        return GetProperties(null);
    }
    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        PropertyDescriptorCollection descriptors = new PropertyDescriptorCollection(null);
        if (attributes == null)
        {
            descriptors.Add(new XElementAttributesPropertyDescriptor());
            descriptors.Add(new XElementNodesPropertyDescriptor());
        }


        foreach (PropertyDescriptor pd in base.GetProperties(attributes))
        {
            descriptors.Add(pd);
        }
        return descriptors;
    }
}

Upvotes: 0

Related Questions