24 May 2010

WPF: One-time, One-way and Two-way Data Binding a DataGrid to LINQ to XML

In my previous post I described my findings on how to bind a DataGrid to a collection object. Now I will examine another common scenario - binding a DataGrid to LINQ to XML a.k.a XLinq.

Basic data binding to XLinq

The goal is to bind the DataGrid to a collection of XElement items returned from XLinq. For a one-time binding, the obvious method works:

Source XML file:
<charsets>
    <charset name="COI" value="1-907" />
    <charset name="f1" value="dsldh" />
</charsets>

MainWindows.xaml.cs:
public XDocument Doc...
...
this.charsetGrid.DataContext = Doc.Root.Elements("charset");

MainWindow.xaml:
<Window x:class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        title="MyApp" height="478" width="644" x:name="mainWindow">
  <DockPanel name="dockPanel1">
    <DataGrid Name="charsetGrid" IsReadOnly="True">
      <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Path=Attribute[name].Value}" Header="Name" />
        <DataGridTextColumn Binding="{Binding Path=Attribute[value].Value}" Header="Value" Width="*" />
      </DataGrid.Columns>
    </DataGrid>
  </DockPanel>
</Window>

One-way and Two-way data binding to XLinq

There are some major gotchas when you are trying to get one-way and two-way data binding to work with XLinq. Here is the list of my findings:

1. The binding to the actual XElement collection has to be done from within XAML. Failing to do so results in a cryptic run-time error when entering edit mode.
2. Property changes of the bound XElements function properly out of the box.
3. The XElement collection does not support two-way changes (presumably because it does not have to map to a single node in the XML tree).
4. You will need to use the Elements and Attribute dynamic properties as the binding paths.
5. You will need to manually implement modifications to the collection structure (i.e. adding and removing items). Again, presumably, this is because the collection does not represent a node in the XML tree.

Here is the code that implements a two-way binding between a DataGrid and an XLinq XML document.

MainWindow.xaml.cs:
public partial class MainWindow : Window
{
    public string FileName  { get; set; }
    public XDocument Doc { get; set; }
    ...
    private void LoadData()
    {
        ...
        // Load the XML file and set the context for the binding
        Doc = XDocument.Load(FileName);
        this.charsetGrid.DataContext = Doc.Root.Element("charsets");
    }

    private void AddCharset_Click(object sender, RoutedEventArgs e)
    {
        // Manually add an XElement at the right position in the XML tree.
        // Make sure teh XElement is fully created (i.e. it has all attributes
        // and elements that are bound to columns in the DataGrid, or you will
        // have run-time problems.
        XElement elem = new XElement("charset",
            new XAttribute("name", string.Empty),
            new XAttribute("value", string.Empty),
        Doc.Root.Element("charsets").Add(elem);
    }

    private void RemoveCharsets_Click(object sender, RoutedEventArgs e)
    {
        // Delete the XElement selected in the DataGrid
        XElement elem = (XElement)charsetGrid.SelectedItem;
        elem.Remove();
    }
}

MainWindow.xaml:
<Window x:class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        title="MyApp" height="478" width="644" x:name="mainWindow">
  <DockPanel name="dockPanel1">
    <DataGrid Name="charsetGrid" IsReadOnly="False" ItemsSource="{Binding Path=Elements}">
      <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Path=Attribute[name].Value}" Header="Name" />
        <DataGridTextColumn Binding="{Binding Path=Attribute[value].Value}" Header="Value" Width="*" />
      </DataGrid.Columns>
    </DataGrid>
  </DockPanel>
</Window>

Note how in line 6 of the XAML we bind the ItemsSource to Elements in XAML instead of code.

No comments: