1

I am building a WPF MVVM application.

What I have:

I have a DataTable, binded to a DataGrid as such:

<DataGrid
       Name="Map"
       AutoGenerateColumns="True"
       AutoGeneratingColumn="Map_AutoGeneratingColumn"
       IsReadOnly="True"
       ItemsSource="{Binding MapDataTable}" />

Here is how I am creating the data table in the ViewModel:

ObservableCollection<ObservableCollection<MapModel>> MapDataList { get; set; }

private void CreateDataTable(IEnumerable<MapModel> mapDataItems)
{
    MapDataTable.Clear();

    DataTable dataTable = new DataTable("MapDataTable");
    dataTable.Columns.Add("First", typeof(string));
    dataTable.Columns.Add("Second", typeof(string)); //typeof might need to be changed to ObservableCollection<SomeModel> (still works) or ComboBox

    var mapData = new ObservableCollection<MapModel>();

    foreach (var item in mapDataItems)
    {
         mapData.Add(item);
    }

    MapDataList.Add(mapData);

    foreach (MapModel item in mapDataItems)
    {
         DataRow dataRow = dataTable.NewRow();

         dataRow[0] = item.Name;
         dataRow[1] = item.SomeValues; //mistake could be here

         dataTable.Rows.Add(dataRow);
     }

     MapDataTable = dataTable;
 }

MapModel.cs:

public class MapModel
{
    public string Name { get; set; }

    public ObservableCollection<SomeModel> SomeValues { get; set; } = new ObservableCollection<SomeModel>
    {
        new SomeModel(),
        new SomeModel()
    };
}

SomeModel.cs:

public class SomeModel
{
    public string Name { get; set; }

    public double Value { get; set; }

    public SomeModel()
    {
        Name = "test";
        Value = 0;
    }
}

Map_AutoGeneratingColumn in the xaml.cs:

if (e.PropertyName == "Second")
{
      var templateColumn = new DataGridTemplateColumn
      {
            Header = e.PropertyName,
            CellTemplate = (sender as FrameworkElement).FindResource("SecondCellTemplate") as DataTemplate,
            CellEditingTemplate = (sender as FrameworkElement).FindResource("SecondCellEditingTemplate") as DataTemplate
      };

      e.Column = templateColumn;
}

In the View:

<Grid.Resources>
    <DataTemplate x:Key="SecondCellTemplate">
       <TextBlock Text="{Binding Path=???, Mode=OneWay}" />
    </DataTemplate>
    <DataTemplate x:Key="SecondCellEditingTemplate">
       <ComboBox
           DisplayMemberPath="Name"
           ItemsSource="{Binding Path=SomeValues, Mode=OneWay}" //wrong binding
           SelectedValue="{Binding Path=???, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
           SelectedValuePath="Name" />
    </DataTemplate>
</Grid.Resources>

What I want to achieve:

I want the SomeValues' names to be displayed in the ComboBox of the DataGridTemplateColumn.

How do I achieve this?

EDIT:

From this comment (https://stackoverflow.com/a/35212223/17198402) I understand how to bind SomeValues IF the columns were hardcoded and not autogenerated, but I still can't wrap my head around how to do it when they are autogenerated. Setting the ItemsSource in the code-behind is still a mystery to me. As far as I understand, DataGridTemplateColumn doesn't inherit the DataContext, unlike DataGridTextColumn.

EDIT 2:

I was able to bind to a property in my ViewModel this way:

ViewModel.cs

public ObservableCollection<SomeModel> SomeValues { get; set; } = new ObservableCollection<SomeModel>
{
      new SomeModel(),
      new SomeModel()
};

xaml:

<Grid.Resources>
    <DataTemplate x:Key="SecondColumnTemplate" DataType="DataGridCell">
         <ComboBox 
             DisplayMemberPath="Name" 
             ItemsSource="{Binding Path=DataContext.SomeValues, RelativeSource={RelativeSource AncestorType=Page}}" />
    </DataTemplate>
</Grid.Resources>

AutoGeneratingColumn:

 var templateColumn = new DataGridTemplateColumn
 {
       Header = e.PropertyName,
       CellTemplate = (sender as FrameworkElement).FindResource("SecondColumnTemplate") as DataTemplate
 };

 e.Column = templateColumn;

This successfully binds the new property in the ViewModel, but of course that's not what I want. How do I bind to the current item.SomeValues ?

Melissa
  • 135
  • 1
  • 7
  • Why are you binding to DataContext.MapDataList? "DataContext." is unnecessary, since DataContext is, by definition, the context your binding is used in. Also, MapDataList doesn't seem to be defined anywhere. Did you mean MapDataTable instead? Apart from that, binding issues are relatively easy to debug if you are using Visual Studio, just enable binding error output and look at the VS output log. – Shrimperator Oct 26 '21 at 08:58
  • Sorry, I forgot to include `MapDataList`, it should be included now. I will try to find the binding mistake with the output log. – Melissa Oct 26 '21 at 09:08
  • I tried it even with a test property, (a List), but apparently in the xaml.cs Style, it can't bind to anything - it says that it is not found when I try to bind to a property in the ViewModel. – Melissa Oct 26 '21 at 09:32
  • @Melissa: Why are you binding to an `ObservableCollection>` instead of an `ObservableCollection`? – mm8 Oct 26 '21 at 14:33
  • I have to, it is a requirement. Basically, when I add a new column (dynamically), I read mapDataItems from another source and add a new column for Second, containing a ComboBox of Second’s values. First is always the same. So basically you get the following columns when you add one - First | Second | Second 2 (for example). I don’t know how to explain it. – Melissa Oct 26 '21 at 14:42
  • and also, this newly read mapDataItems is added to MapDataList. – Melissa Oct 26 '21 at 14:47
  • You must bind to the cell values by referencing the column by its name. The DataContext of the columns template is the row. First, the column's type must be changed to `IEnumerable`. Second, since the column of interest is named `Second`, the binding on the ComboBox must look like this: ``. Same path applies for the TextBlock. – BionicCode Nov 08 '21 at 19:02
  • Specifying the Binding.Mode is redundant as the mode is OneWay by default. – BionicCode Nov 08 '21 at 19:05

1 Answers1

1

There are some things wrong with your binding:

<Setter Property="ItemsSource" Value="{Binding DataContext.MapDataList.SomeValues, RelativeSource={RelativeSource AncestorType=Page}}" />

First of all, DataContext. is unnecessary whichever way you look at it. DataContext is, by definition, the path WPF bindings look at to resolve their binding by default. If you don't set the DataContext, it is null. Now you can either set the DataContext to something other than null, or you can specify a path for the binding. You're mixing up different ways of binding data here. DataContext. shouldn't be part of your binding in any case, but if you use RelativeSource you'll have to specify the Path of the binding and set it to the property you want to bind to.

The first answer to this question here is a great explanation of DataContext and how to use RelativeSource: How do I use WPF bindings with RelativeSource?

All that aside, your binding cannot work: MapDataList is an ObservableCollection, and the elements of that collection have a list called "SomeValues". If you want to access SomeValues, you're going to have to specify an index of your collection at some point. After all: which element of the collection should the binding go to?

You seem to be mixing up a lot of different systems here.

I recommend you read through these questions and their answers thoroughly to get an idea of how to do this properly:

How to bind collection to WPF:DataGridComboBoxColumn

Binding a WPF ComboBox to a custom list

Shrimperator
  • 560
  • 3
  • 13
  • I guess that I might have to specify an index, but how do `DataGridTextColumns` need no index? The binding is quite simple - `Binding = new Binding(e.PropertyName)` in the AutoGeneratingColumn event. – Melissa Oct 27 '21 at 09:47
  • DataGrid columns are special that way when you use ItemsSource. They automatically resolve the binding to the element associated with the row created for said element. If you set the ItemsSource of a DataGrid to a list with 5 elements of type MyType, where MyType has a property "MyProperty", it will create 5 rows in that DataGrid, and in the DataGrid column you can immediately bind to MyProperty within the definition of that column. Why is that? Because the DataContext of the column will automatically be set to the specified element. – Shrimperator Oct 27 '21 at 10:24
  • 1
    In practice you don't want to bind to a specific index in a collection, I just wanted you to understand why your binding can never actually work: you give the binding an ObservableCollection and then try to bind to a property of an element inside said collection - that logically cannot work: which element would it resolve to? What you can do instead is properly use the ItemsSource of the DataGrid, so it automatically resolves the binding to the correct element. I recommend you look at some DataGrid examples to learn how to use it, and get to know WPF data binding first. – Shrimperator Oct 27 '21 at 10:28