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

Answer accepted by question author
  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' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.