0

I am working on an MVVM WPF DataGrid application. I have a DataGrid and a multiselect CheckBox dropdown menu above it. Whenever I select an option in the menu, I want to add a column in the DataGrid. Is there any way to do that?

enter image description here

The code behind the ComboBox Item Template looks like this:

<ComboBox.ItemTemplate>
        <DataTemplate>
               <CheckBox
                     VerticalAlignment="Center"
                     ClickMode="Press"
                     Content="{Binding Position}"
                     IsChecked="{Binding IsSelected}" />
        </DataTemplate>
</ComboBox.ItemTemplate>
Raya
  • 148
  • 1
  • 10
  • 1
    You can use a DataTable as data source and manipulate it. – BionicCode Sep 21 '21 at 09:10
  • @BionicCode Can I do it all from the ViewModel? As I am getting the data from a REST API to the ViewModel. – Raya Sep 24 '21 at 13:21
  • Usually you call the RESTful API from your Model. The View Model requests the data from the Model. It depends on your design if it makes sense that your Model chooses to store the response in a DataTable data structure. But in general it is not a problem to have the View Model to convert the data received from the Model in order to present it to the View. That's one responsibility of the View Model. If you don't think the DataTable is the preferred data structure to handle the data in your Model, you can let the View Model perform the data conversion. – BionicCode Sep 24 '21 at 19:18
  • @BionicCode So it is possible to add a column from the ViewModel? Because in my case, I don’t think that I can use a DataTable. – Raya Sep 25 '21 at 07:11
  • Yes, you can. If adding a column is triggered by the View then you do it in the View Model. If the column is added e.g., remotely, then you would already get the exptended table. I'm not sure why you can't use a DataTable. If you have data that you can display in a table, then DataTable can hold your data, of course. That's why I wonder why you can't use a DataTable as data source. – BionicCode Sep 25 '21 at 08:36
  • @BionicCode Do basically I can declare and fill the DataTable in the ViewModel and bind it as data source? I thought that I can only use it in the xaml.cs file, not the ViewModel. I’m sorry for asking so many questions, I’ve just been struggling with this for a few days and my head is a mess. – Raya Sep 25 '21 at 13:29
  • 1
    You seem to have some critically wrong perceptions. First of all DataTable is a simple class that implements a plain table data structure - it structures data in rows and columns. It is a container for data. Data is used in the Model (persistence, apply business logic), in the View Model (get Data from the Model, apply conversion logic, act as data source for the View, exchange Data via data binding) and the View (present data, apply presentation logic e.g. wrapping/mapping data to thei UI containers). Data is present everywhere. – BionicCode Sep 25 '21 at 13:46
  • 1
    DataTable can be used everywhere like you would use other data structures like collections everwhere too. A ObservableCollection is a data structure as is DataTable. You can expose a property of type DataTable to the View as binding source. DataGrid.ItemsSource can handle both: IEnumerable and DataTable or to be more precise ICollectionView and DataView. I can asure you that having a DataTable as bindng source is perfectly fine. There is no constraint of what type a property must be in order to be a valid binding source. You can even bind to XML data. – BionicCode Sep 25 '21 at 13:46
  • 1
    If having your data stored in a DataTable structure in your Model does not make sense, then use the View Model to convert the Model data objects to a DataTable. Then bind your DataGrid directly to this table. This is very clean MVVM. – BionicCode Sep 25 '21 at 13:46
  • @BionicCode Thank you very much for explaining it so well! Everything is clear to me now :) – Raya Sep 25 '21 at 13:51
  • 1
    You are welcome. Glad I could help you. – BionicCode Sep 25 '21 at 13:56
  • 1
    Sorry, my posted example was incomplete. i have updated the answer to provide the missing pieces. – BionicCode Sep 25 '21 at 15:10
  • Thank you again for putting such effort helping a newbie! It’s highly appreciated. – Raya Sep 25 '21 at 15:13

2 Answers2

1

I have recently faced something similar in a C# uwp application. Here's what worked for me.


Firstly, I created a list to keep track of all checked checkboxes:

private List<CheckBox> checkedCheckboxes = new List<CheckBox>();

Then, I created the checkboxes and linked them to the same events like so (You probably have code for this part already):

foreach (foo blah in random)
        {
            var checkbox = new CheckBox(); //creating new checkbox
            checkbox.Content = blah.name; //setting content
            checkbox.Name = $"chk_{blah.name}"; //naming it
            checkbox.Tag = "blah"; //adding the tag

            checkbox.Checked += CheckBox_Checked; //adding the checked event
            checkbox.Unchecked += CheckBox_Unchecked; //adding the unchecked event

            ClassCheckboxes.Add(checkbox);
        }

For the "CheckBox_Checked" event I did the following:

private void CheckBox_Checked(object sender, RoutedEventArgs e)
    {
        checkedCheckboxes.Add((CheckBox)sender);
        //Here you can put some code to update your datagrid
    }

And for the "CheckBox_Unchecked" event I did the following:

private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
    {
        checkedCheckboxes.Remove((CheckBox)sender);
        //Here you can put some code to update your datagrid
    }

To add a new column to your datagrid, you can refer to this answer. There are a few nice strategies there that may work for you. Here's the highest voted one for your convenience:

DataGridTextColumn textColumn = new DataGridTextColumn(); 
textColumn.Header = "First Name"; 
textColumn.Binding = new Binding("FirstName"); 
dataGrid.Columns.Add(textColumn);

Sorry if I've done something wrong here, this is my first time posting an answer so go easy on me :)

  • 2
    In WPF, almost every solution that modifies the view instead of the data is a bad solution - especially in an MVVM context. Finding yourself creating controls and data bindings in C# should ring some bells. The clean solution would be to modify the data source. Adjust specialized view columns if necessary by handly the `DataGrid.AutoGeneratingColumn` event: [Microsoft Docs: How to: Customize Auto-Generated Columns in the DataGrid Control](https://learn.microsoft.com/en-us/windows/communitytoolkit/controls/datagrid_guidance/customize_autogenerated_columns) – BionicCode Sep 21 '21 at 10:55
  • @BionicCode Unfortunately, I cannot use Auto-Generated Columns, as I am binding the DataGrid to a CollectionView of an ObservableCollection of type classA (for example), but I don't want to add a column for every property of that class. I am manually binding which properties I want to create columns for. – Raya Sep 21 '21 at 14:48
  • 1
    That's what the afore-mentioned events are for. You can assign column templates or cancel the generation for columns you want to ignore. The provided link from my previous comment shows you how to do it. To add columns you simply would add a new column to the DataTable which you should use as data source. – BionicCode Sep 21 '21 at 15:27
1

This is a simple example how to use a DataTable as data source for a DataGrid.

ViewModel.cs

class VeiwModel : INotifyPropertyChanged
{
  // Let this property set() raise the INotifyPropertyChanged.PropertyChanged event
  // if its value/instance will change after instantiation
  public DataTable TableData { get; set; }

  public ViewModel()
  {
    var changedTable = new DataTable();
    AddColumn<int>("ID", changedTable);
    AddColumn<string>("Username", changedTable);
    AddColumn<string>("Mail", changedTable);

    // Add column values in order of their columns
    AddRow(changedTable, 1, "Me", "me@mail.com");

    // Update the DataGrid with changes
    this.TableData = changedTable; 
  }

  // Appends a new column. 
  // Use 'columnIndex' parameter to assign an other column index than the last
  private void AddColumn<TData>(string columnName, DataTable targetDataTable, int columnIndex = -1)
  {
    DataColumn newColumn = new DataColumn(columnName, typeof(TData));

    targetDataTable.Columns.Add(newColumn);
    if (columnIndex > -1)
    {
      newColumn.SetOrdinal(columnIndex);
    }

    int newColumnIndex = targetDataTable.Columns.IndexOf(newColumn);

    // Initialize existing rows with default value for the new column
    foreach (DataRow row in targetDataTable.Rows)
    {
      row[newColumnIndex] = default(TData);
    }
  }

  private void AddRow(DataTable targetDataTable, params object[] columnValues)
  {
    DataRow rowModelWithCurrentColumns = targetDataTable.NewRow();
    targetDataTable.Rows.Add(rowModelWithCurrentColumns);

    for (int columnIndex = 0; columnIndex < targetDataTable.Columns.Count; columnIndex++)
    {
      rowModelWithCurrentColumns[columnIndex] = columnValues[columnIndex];
    }
  }
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DataContex>

  <DataGrid ItemsSource="{Binding TableData}" />
</Window>
BionicCode
  • 1
  • 4
  • 28
  • 44