Search code examples
c#xmlxamldata-bindingdatagrid

What is the C# equivalent of this XAML code binding XML elements to a DataGrid?


(Edited: My actual problem turns out to be about setting the ItemsSource correctly, not the bindings for each DataGridTextColumn!)

Description of Problem

I am struggling with a specific data binding task where I want to bind XML data (using LINQ, parsed as XElement) to a WPF DataGrid (not a DataGridView) so that it can be edited by the user. I think the very core problem it probably boils down to is this:

What is the equivalent in C# code for the following XAML statement?

<DataGrid x:Name="dtaGrid" ItemsSource="{Binding Path=Elements[track]}"/>

I thought, it should be:

dtaGrid.ItemsSource = xml.Elements("track");

Unfortunately, the C# statement doesn't work as expected: While the data is being displayed in the DataGrid, a System.InvalidOperationException ("EditItem is not allowed for this view") occurs once the user double-clicks a DataGrid cell to edit its content. Using the XAML variant, the data is both shown and editable without error, and the changes are reflected in the XML source.

Since I don't know the actual XML file's structure at design time, I want to dynamically set the ItemSource at runtime in code behind (and thus be able to change the path used for binding).


Working Example

Here is a working example (with ItemsSource binding being done in XAML). Sorry for long code quotes, I just thought it might help clarify the problem better in context.

MainWindow.xaml (note how the DataGrid's ItemsSource is explicitly bound here - I need to do be able to change this binding at runtime in code behind):

<Window x:Class="linq_xml.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:linq_xml" mc:Ignorable="d"
        Title="MainWindow" Width="1000" Height="700" >

    <Grid Margin="8">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <DataGrid x:Name="dtaGrid" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" 
                  ItemsSource="{Binding Path=Elements[track]}" AutoGenerateColumns="False"/>

        <Button x:Name="btn_Save" Grid.Row="1" Grid.Column="0" 
                Width="100" HorizontalAlignment="Left" Margin="0 8 0 0" 
                Content="Save XML" Click="Btn_Save_Click"/>
    </Grid>
</Window>

MainWindow.xaml.cs (note the uncommented ItemsSource statement):

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Xml.Linq;

namespace linq_xml
{
    public partial class MainWindow : Window
    {
        private XElement xml;
        private readonly string filepath = @"D:\SynologyDrive\Dev\C#\linq-xml\XML-Beispiele\random.xml";

        public MainWindow()
        {
            InitializeComponent();

            xml = XElement.Load(filepath); // load xml file
            dtaGrid.DataContext = xml; // set LINQ to XML as data context

            /* If the following line is used rather than the ItemsSource being bound done in XAML, 
             * it doesn't work as expected: Once the user tries to edit a cell at runtime,
             * a System.InvalidOperationException ("EditItem is not allowed for this view") occurs. */
          // dtaGrid.ItemsSource = xml.Elements("track");

            List<DataGridTextColumn> columns = new List<DataGridTextColumn>();
            columns.Add(new DataGridTextColumn());
            columns[^1].Header = "Artist";
            columns[^1].Binding = new Binding("Element[artist_name].Value");

            columns.Add(new DataGridTextColumn());
            columns[^1].Header = "Album";
            columns[^1].Binding = new Binding("Element[album_name].Value");

            columns.Add(new DataGridTextColumn());
            columns[^1].Header = "Duration";
            columns[^1].Binding = new Binding("Element[duration].Value");

            foreach (DataGridTextColumn c in columns)
            {
                dtaGrid.Columns.Add(c);
            }                        
        }

        private void Btn_Save_Click(object sender, RoutedEventArgs e)
        {
            xml.Save(filepath);
        }
    }
}

example.xml:

<?xml version="1.0" encoding="utf-8"?>
<data>
  <track>
    <id>1337</id>
    <name>Wonderful World</name>
    <duration>128</duration>
    <artist_id>13</artist_id>
    <artist_name>Trumpet</artist_name>
    <album_id>22</album_id>
    <album_name>Nice People</album_name>
  </track>
  <track>
    <id>4711</id>
    <name>Colorful World</name>
    <duration>256</duration>
    <artist_id>1</artist_id>
    <artist_name>Pink</artist_name>
    <album_id>11</album_id>
    <album_name>I like the blues</album_name>
  </track>
  <track>
    <id>0815</id>
    <name>World</name>
    <duration>512</duration>
    <artist_id>9</artist_id>
    <artist_name>CNN</artist_name>
    <album_id>33</album_id>
    <album_name>My Finger Is On The Button</album_name>
  </track>
</data>

Solution

  • Unfortunately, the C# statement doesn't work as expected: While the data is being displayed in the DataGrid, a System.InvalidOperationException ("EditItem is not allowed for this view") occurs once the user double-clicks a DataGrid cell to edit its content.

    That exception is telling you that the bound data source is read-only. You aren't allowed to edit the item, because WPF doesn't have any way to copy your edit back into the source.

    And if you look at the XElement.Elements() method, it's easy to see why. That method returns an IEnumerable<XElement>. The IEnumerable<T> interface is read-only. It just produces values. It provides no mechanism to modify the original source of the values. So, of course the DataGrid can't modify the elements.

    But! (you will exclaim :) ) Why does it work when you provide the exact same data source in the XAML? Well, because WPF is working hard to try to make sure you don't have to. If you were to run the program, break in the debugger at a convenient time (like, when your "Save XML" button is clicked), you can take a look at what the dtaGrid.ItemsSource property is set to, and you'll find it's not an instance of IEnumerable<XElement>. Instead, it's this other type, ReadOnlyObservableCollection<T>.

    WPF has, on your behalf, copied the results of the IEnumerable<XElement> object into a new collection, where the elements can be modified.

    Interestingly, you'll note that this is ReadOnlyObservableCollection<T> (or more precisely, ReadOnlyObservableCollection<object>). There is also a related type, ObservableCollection<T>. Why WPF uses the read-only version I'm not sure…probably some sort of compromise meant to balance convenience and/or performance and potential for messing the data. In any case, that's what it does. It's interesting, because it means that while you can edit individual cells in the grid, you can't delete entire rows. Cells can be updated without modifying the collection itself, but deleting entire rows can't be.

    This all brings me to the fix for your code, which is very simple: bind to a collection type that is appropriate to your needs. If you want exactly the behavior seen when you bind via XAML, you can create the read-only version of the collection:

    dtaGrid.ItemsSource = new ReadOnlyObservableCollection<XElement>(
        new ObservableCollection<XElement>(xml.Elements("track")));
    

    (The read-only collection can only be initialized with an instance of the regular write-able version.)

    On the other hand, if you would like the user to also be able to delete or insert rows, you can use the write-able version of the collection (i.e. just do it without the read-only wrapper):

    dtaGrid.ItemsSource = new ObservableCollection<XElement>(xml.Elements("track"));
    

    That addresses the specific question you asked. I hope it was worth the walk. :) But there's more…

    Since I don't know the actual XML file's structure at design time, I want to dynamically set the ItemSource at runtime in code behind (and thus be able to change the path used for binding).

    You should commit yourself to invest the effort to learn the MVVM pattern in WPF. There are a lot of reasonable variations on the theme, and I personally don't always necessarily adhere to it strictly myself. Taken literally, it can result in a lot of repeated effort, adding a "view model" layer between your UI and the business logic. This effort is often not worthwhile in very simple programs, where the business logic model objects can adequately serve as view model objects.

    But regardless, the basic ideas behind MVVM are sound and, more importantly, WPF is designed specifically with it in mind. Which means any time you're not "doing it the MVVM way", you're fighting the framework. It's a steep learning curve, but I assure you it's worth it when you get to the summit (or at least the lookout point halfway up, where I figure I am right now :) ).

    In the context of your example, that means you would ideally have a view model data structure that has properties representing the XML (so you can set the property and let a binding in the XAML copy the reference to ItemsSource), but also a collection-type property that contains the information needed to configure the columns according to the run-time needs. Ideally, you'll never create a UI object (like DataGridTextColumn) in code-behind. Instead, you'll let WPF do the hard work of translating your simple business logic represented as a view model, to the UI data structures needed for display.

    Connecting this to the original issue, you can see that you can make the same sort of decisions involved in the original fix, but in your view model instead, providing a read-only collection or write-able collection, depending on how you want the grid to behave.

    Either way, eventually you should aim to implement your program in a way that does not require manually setting any of this in the UI code-behind, but rather using view models for all of your actual state, and using the XAML binding syntax to connect your view models to the UI.