3
votes

I'm struggling with a confluence of problems.

  1. I have a dynamic data set which I manually assemble into a DataTable.
  2. I have to auto generate the columns as the data is not static.
  3. I need to bind the ItemsSource of a combo box to an Observable collection defined in each cell.

Although I thought it would be easy, the ComboBox cannot see the DataItem in the DataView, rather it tries to bind to the DataView directly.

I've put together a sample project here:

https://github.com/5flags/DataGridBindingIssue

Now, it's obviously contrived to demonstrate the issue. I can't change the data structure at this point, so any solution must be done in the XAML.

To see the problems, use Snoop (or equivalent) to see the binding errors on the ComboBoxes.

The DataGrid is set up like so:

<DataGrid AutoGenerateColumns="True" AutoGeneratingColumn="DataGrid_OnAutoGeneratingColumn" CanUserAddRows="False" x:Name="TheDataGrid" ItemsSource="{Binding Data}">
    <DataGrid.Resources>
        <DataTemplate x:Key="dataItemCellTemplate">
            <ComboBox SelectedValue="{Binding Path=SelectedOption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                  ItemsSource="{Binding Options}"/>
        </DataTemplate>
    </DataGrid.Resources>
</DataGrid>

And the event handler for the autogeneration is:

private void DataGrid_OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    if (e.PropertyType == typeof(string))
    {
        var col = new DataGridTextColumn {Binding = new Binding(e.PropertyName), Header = e.PropertyName};
        e.Column = col;
    }
    else if (e.PropertyType == typeof(DataItem))
    {
        var col = new DataGridTemplateColumn
        {
            CellTemplate = (DataTemplate) TheDataGrid.FindResource("dataItemCellTemplate"),
            CellEditingTemplate = (DataTemplate)TheDataGrid.FindResource("dataItemCellTemplate"),
            Header = e.PropertyName
        };
        e.Column = col;
    }
}

The binding error on the combo is:

System.Windows.Data Error: 40 : BindingExpression path error: 'Options' property not found on 'object' ''DataRowView' (HashCode=22264221)'. BindingExpression:Path=Options; DataItem='DataRowView' (HashCode=22264221); target element is 'ComboBox' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')

System.Windows.Data Error: 40 : BindingExpression path error: 'SelectedOption' property not found on 'object' ''DataRowView' (HashCode=22264221)'. BindingExpression:Path=SelectedOption; DataItem='DataRowView' (HashCode=22264221); target element is 'ComboBox' (Name=''); target property is 'SelectedValue' (type 'Object')

3
Can you please just show your data binding XAML?Sheridan
Edited the question to include more info.RBT
Can you please confirm that you have one column named Options and one named SelectedOption in your DataTable?Sheridan
The DataItem in the cell has SelectedOption and Options on it.RBT
What do you mean by 'DataItem'? I thought that you said that you're using a DataTable... do you mean DataRow? And what makes you think that the Binding is reaching the DataView instead? What errors do you have in the Output window in Visual Studio?Sheridan

3 Answers

3
votes

Dusan's answer set me on the right track. Because I don't know the column names until runtime, I have to create the data template at runtime too. It's actually not difficult.

private DataTemplate GetDataTemplate(string columnName)
{
    string xaml = "<DataTemplate><ComboBox SelectedValue=\"{Binding Path=[" + columnName +
                  "].SelectedEnumeratedElementItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}\"" +
                  " ItemsSource=\"{Binding Path=[" + columnName +
                  "].Items}\" DisplayMemberPath=\"Name\"/></DataTemplate>";

    var sr = new MemoryStream(Encoding.ASCII.GetBytes(xaml));
    var pc = new ParserContext();
    pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
    pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
    var datatemplate = (DataTemplate)XamlReader.Load(sr, pc);

    return datatemplate;
}
2
votes

Modify your XAML to:

<DataGrid AutoGenerateColumns="True" AutoGeneratingColumn="DataGrid_OnAutoGeneratingColumn" CanUserAddRows="False" x:Name="TheDataGrid" ItemsSource="{Binding Data}">
    <DataGrid.Resources>
        <DataTemplate x:Key="dataItemCellTemplate">
            <ComboBox ItemsSource="{Binding [Option].Options}" SelectedValue="{Binding [Option].SelectedOption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </DataGrid.Resources>
</DataGrid>

Where [Option] refers to the column of DataView in which you store your custom DataItem objects.

2
votes

I dont know how to do this with DataTemplate from XAML resources, but it work fine for me with DataTemplate created in code.

private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    System.Windows.Controls.DataGridTemplateColumn templateColumn = new System.Windows.Controls.DataGridTemplateColumn();
    templateColumn.Header = e.PropertyName;

    DataTemplate template = new DataTemplate();
    FrameworkElementFactory factory = new FrameworkElementFactory(typeof(StackPanel));
    template.VisualTree = factory;
    FrameworkElementFactory childFactory = new FrameworkElementFactory(typeof(TextBox));
    childFactory.SetBinding(TextBox.TextProperty, new Binding(e.PropertyName));
    factory.AppendChild(childFactory);

    templateColumn.CellEditingTemplate = template;

    template = new DataTemplate();
    factory = new FrameworkElementFactory(typeof(StackPanel));
    template.VisualTree = factory;
    childFactory = new FrameworkElementFactory(typeof(TextBlock));
    childFactory.SetBinding(TextBlock.TextProperty, new Binding(e.PropertyName));
    factory.AppendChild(childFactory);
    templateColumn.CellTemplate = template;

    e.Column = templateColumn;
}