WPF DataGrid with ComboBox bound to a different list of values on each row

Raya Chorbadzhiyska 21 Reputation points
2021-11-01T07:12:08.763+00:00

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)

    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 each item's SomeValues' names to be displayed in the ComboBox of the DataGridTemplateColumn on the corresponding row.

How do I achieve this?

Developer technologies Windows Presentation Foundation
0 comments No comments
{count} votes

Accepted answer
  1. Peter Fleischer (former MVP) 19,341 Reputation points
    2021-11-03T05:06:08.133+00:00

    Hi Raya,
    try following demo:

    XAML

    <Window x:Class="WpfApp1.Window077"  
            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:WpfApp077"  
            mc:Ignorable="d"  
            Title="211102_RayaChorbadzhiyska-6265" Height="450" Width="800">  
      <Window.DataContext>  
        <local:ViewModel/>  
      </Window.DataContext>  
      <Grid>  
        <Grid.RowDefinitions>  
          <RowDefinition Height="auto"/>  
          <RowDefinition/>  
        </Grid.RowDefinitions>  
        <ComboBox ItemsSource="{Binding SelectCols}"   
                  DisplayMemberPath="Desc"  
                  SelectedValue="{Binding ColCount}"  
                  SelectedValuePath="ID"/>  
        <DataGrid Grid.Row="1"  
                  CanUserAddRows="False"  
                  AutoGenerateColumns="False"  
                  ItemsSource="{Binding View}"  
                  local:DataGridExtension.Columns="{Binding Columns}"/>  
      </Grid>  
    </Window>  
    

    Code:

    using System.Collections.Generic;  
    using System.Collections.ObjectModel;  
    using System.Collections.Specialized;  
    using System.ComponentModel;  
    using System.Runtime.CompilerServices;  
    using System.Windows;  
    using System.Windows.Controls;  
    using System.Windows.Data;  
      
    namespace WpfApp1  
    {  
      public partial class Window077 : Window  
      {  
        public Window077() => InitializeComponent();  
      }  
    }  
      
    namespace WpfApp077  
    {  
      public class ViewModel : INotifyPropertyChanged  
      {  
        public ViewModel() => cvs.Source = GetData();  
      
        #region ComboBox to select ColumnCount in DataGrid  
        public int MaxColumnCount = 5;  
        private int _colCount = 1;  
        public int ColCount { get => this._colCount; set { this._colCount = value; OnPropertyChanged(nameof(Columns)); } }  
        public List<IDHelper> SelectCols  
        {  
          get  
          {  
            List<IDHelper> l = new List<IDHelper>();  
            for (int i = 0; i < MaxColumnCount; i++) l.Add(new IDHelper()  
            {  
              ID = i,  
              Desc = $"{ i + 1 }"  
            });  
            return l;  
          }  
        }  
        #endregion  
      
        #region View of data in DataGrid  
        public ICollectionView View { get => cvs.View; }  
        private CollectionViewSource cvs = new CollectionViewSource();  
      
        // create demo data  
        private ObservableCollection<MapModel> GetData()  
        {  
          ObservableCollection<MapModel> data = new ObservableCollection<MapModel>();  
          for (int i = 0; i < 10; i++)  
          {  
            // Array of FKNames  
            string[] arr1 = new string[MaxColumnCount];  
            // Array of SomeModels  
            ObservableCollection<SomeModel>[] arr2 = new ObservableCollection<SomeModel>[MaxColumnCount];  
            for (int k = 0; k < MaxColumnCount; k++) // for columns in DataGrid  
            {  
              // collection in array of SameModels  
              arr2[k] = new ObservableCollection<SomeModel>();  
              for (int l = 0; l < 5; l++) arr2[k].Add(new SomeModel() { ID = l, Name = $"FKName {l} {k} {i}" });  
              // first element  
              arr1[k] = arr2[k][0].Name;  
            }  
            //  
            data.Add(new MapModel(arr1, arr2) { Name = $"Name {i}" });  
          }  
          return data;  
        }  
        #endregion  
      
        #region Columns in DataGrid  
        public ObservableCollection<DataGridColumn> Columns { get => GetColumns(); }  
        private ObservableCollection<DataGridColumn> GetColumns()  
        {  
          ObservableCollection<DataGridColumn> cols = new ObservableCollection<DataGridColumn>();  
          cols.Add(new DataGridTextColumn() { Header = "First", Width = 100, Binding = new Binding("Name") });  
          for (int i = 0; i <= ColCount; i++)  
          {  
            DataGridTemplateColumn col = new DataGridTemplateColumn() { Header = $"Second {i + 1}", Width = 100 };  
            // SecondCellTemplate  
            DataTemplate dt1 = new DataTemplate();  
            FrameworkElementFactory tb = new FrameworkElementFactory(typeof(TextBlock));  
            tb.SetBinding(TextBlock.TextProperty, new Binding($"FKName[{i}]") { Mode = BindingMode.OneWay });  
            dt1.VisualTree = tb;  
            col.CellTemplate = dt1;  
            // SecondCellEditingTemplate  
            DataTemplate dt2 = new DataTemplate();  
            FrameworkElementFactory cb = new FrameworkElementFactory(typeof(ComboBox));  
            cb.SetValue(ComboBox.DisplayMemberPathProperty, "Name");  
            cb.SetBinding(ComboBox.ItemsSourceProperty, new Binding($"SomeValues[{i}]") { Mode = BindingMode.OneWay });  
            cb.SetBinding(ComboBox.SelectedValueProperty, new Binding($"FKName[{i}]") { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });  
            cb.SetValue(ComboBox.SelectedValuePathProperty, "Name");  
            dt2.VisualTree = cb;  
            col.CellEditingTemplate = dt2;  
            //  
            cols.Add(col);  
          }  
          return cols;  
        }  
        #endregion  
      
        #region OnPropertyChanged  
        public event PropertyChangedEventHandler PropertyChanged;  
        private void OnPropertyChanged([CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));  
        #endregion  
      }  
      
      public class MapModel  
      {  
        public string Name { get; set; }  
        public ArrayIndexer<int> FK { get; } // Name of selected SomeValue  
        public ArrayIndexer<string> FKName { get; } // Name of selected SomeValue  
        public ArrayIndexer<ObservableCollection<SomeModel>> SomeValues { get; }  
        public MapModel(string[] FKNameArray, ObservableCollection<SomeModel>[] SomeValuesArray)  
        {  
          this.FKNameArr = FKNameArray;  
          this.SomeValuesArr = SomeValuesArray;  
          this.FKName = new ArrayIndexer<string>(FKNameArr);  
          this.SomeValues = new ArrayIndexer<ObservableCollection<SomeModel>>(SomeValuesArr);  
        }  
        string[] FKNameArr;  
        ObservableCollection<SomeModel>[] SomeValuesArr;  
      }  
      
      public class SomeModel  
      {  
        public int ID { get; set; }  
        public string Name { get; set; }  
      }  
      
      public class IDHelper  
      {  
        public int ID { get; set; }  
        public string Desc { get; set; }  
      }  
      
      public class ArrayIndexer<T>  
      {  
        private T[] arr;  
        public ArrayIndexer(T[] arr) => this.arr = arr;  
        public T this[int index] { get => arr[index]; set { arr[index] = value; } }  
        public int Length { get => arr.Length; }  
      }  
      
      public static class DataGridExtension  
      {  
        public static ObservableCollection<DataGridColumn> GetColumns(DependencyObject obj) => (ObservableCollection<DataGridColumn>)obj.GetValue(ColumnsProperty);  
        public static void SetColumns(DependencyObject obj, ObservableCollection<DataGridColumn> value) => obj.SetValue(ColumnsProperty, value);  
      
        public static readonly DependencyProperty ColumnsProperty =  
               DependencyProperty.RegisterAttached("Columns",  
               typeof(ObservableCollection<DataGridColumn>),  
               typeof(DataGridExtension),  
               new UIPropertyMetadata(new ObservableCollection<DataGridColumn>(),  
               OnDataGridColumnsPropertyChanged));  
      
        private static void OnDataGridColumnsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
        {  
          if (d.GetType() == typeof(DataGrid))  
          {  
            DataGrid myGrid = d as DataGrid;  
            ObservableCollection<DataGridColumn> Columns = (ObservableCollection<DataGridColumn>)e.NewValue;  
            if (Columns != null)  
            {  
              myGrid.Columns.Clear();  
              if (Columns != null && Columns.Count > 0)  
                foreach (DataGridColumn dataGridColumn in Columns) myGrid.Columns.Add(dataGridColumn);  
              Columns.CollectionChanged += delegate (object sender, NotifyCollectionChangedEventArgs args)  
              {  
                if (args.NewItems != null) foreach (DataGridColumn column in args.NewItems) myGrid.Columns.Add(column);  
                if (args.OldItems != null) foreach (DataGridColumn column in args.OldItems) myGrid.Columns.Remove(column);  
              };  
            }  
          }  
        }  
      }  
    }  
    

    Result:

    146032-x.gif

    1 person found this answer helpful.
    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. Peter Fleischer (former MVP) 19,341 Reputation points
    2021-11-01T21:04:30.047+00:00

    Hi Raya,
    try following demo:

    XAML:

    <Window x:Class="WpfApp1.Window076"  
            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:WpfApp076"  
            mc:Ignorable="d"  
            Title="Window076" Height="450" Width="800">  
      <Window.DataContext>  
        <local:ViewModel/>  
      </Window.DataContext>  
      <Grid>  
        <Grid.Resources>  
          <DataTemplate x:Key="SecondCellTemplate">  
            <TextBlock Text="{Binding Path=FKName, Mode=OneWay}" />  
          </DataTemplate>  
          <DataTemplate x:Key="SecondCellEditingTemplate">  
            <ComboBox DisplayMemberPath="Name"    
                      ItemsSource="{Binding Path=Second, Mode=OneWay}"       
                      SelectedValue="{Binding Path=FKName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"       
                      SelectedValuePath="Name" />  
          </DataTemplate>  
        </Grid.Resources>  
        <DataGrid AutoGenerateColumns="False"  
                  ItemsSource="{Binding MapDataTable}">  
          <DataGrid.Columns>  
            <DataGridTextColumn Header="First" Binding="{Binding First}" Width="100"/>  
            <DataGridTemplateColumn Header="Second" Width="150"  
                                    CellTemplate="{StaticResource SecondCellTemplate}"  
                                    CellEditingTemplate="{StaticResource SecondCellEditingTemplate}"/>  
          </DataGrid.Columns>  
        </DataGrid>  
      </Grid>  
    </Window>  
    

    Code:

    using System.Collections.Generic;  
    using System.Collections.ObjectModel;  
    using System.Data;  
    using System.Windows;  
      
    namespace WpfApp076  
    {  
      public class ViewModel  
      {  
        public ViewModel()  
        {  
          ObservableCollection<MapModel> l = new ObservableCollection<MapModel>();  
          for (int i = 1; i < 10; i++)  
          {  
            MapModel mm = new MapModel() { Name = $"Name {i}" };  
            for (int k = 0; k < 3; k++) mm.SomeValues.Add(new SomeModel() { Name = $"Val {i} {k}" });  
            l.Add(mm);  
          }  
          CreateDataTable(l);  
        }  
        public DataTable MapDataTable { get; set; } = new DataTable();  
        ObservableCollection<ObservableCollection<MapModel>> MapDataList { get; set; } = new ObservableCollection<ObservableCollection<MapModel>>();  
        private void CreateDataTable(IEnumerable<MapModel> mapDataItems)  
        {  
          MapDataTable.Clear();  
          DataTable dataTable = new DataTable("MapDataTable");  
          dataTable.Columns.Add("First", typeof(string));  
          dataTable.Columns.Add("Second", typeof(object));  
          dataTable.Columns.Add("FKName", typeof(string));  
          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;  
            dataRow[2] = item.SomeValues[0].Name;  
            dataTable.Rows.Add(dataRow);  
          }  
          MapDataTable = dataTable;  
        }  
      }  
      
      public class MapModel  
      {  
        public string Name { get; set; }  
        public string FKName { get; set; } // Name of selected SomeValue  
        public ObservableCollection<SomeModel> SomeValues { get; set; } = new ObservableCollection<SomeModel>();  
      }  
      
      public class SomeModel  
      {  
        public string Name { get; set; }  
        public double Value { get; set; }  
      }  
    }  
    

    Result:

    145604-x.gif

    1 person found this answer helpful.

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.