1

I have a WPF application with a DataGrid, menus and buttons. When rows in the DataGrid are selected, buttons and menu items are activated, to enable deleting data from a database.

Partial XAML for this main window:

<Button ToolTip="Delete Record" Command="{Binding DeleteCommand}" Name="button_delete" IsEnabled="False"/>
<MenuItem>
    <MenuItem Header="Delete" IsEnabled="False" Name="menuItem_delete" Command="{Binding DeleteCommand}"/>
</MenuItem>

<DataGrid Name="BooksDataGrid" ItemsSource="{Binding BooksList}" SelectionChanged="dataGrid_selectionChanged">
    <DataGrid.Columns>
         <DataGridTextColumn Header="Title" Binding="{Binding title_long}"/>
         <DataGridTextColumn Header="ISBN" Binding="{Binding isbn}"/>
    </DataGrid.Columns>
</DataGrid>

The DeleteCommand is to be defined within the class that is the DataContext for the main window above. Partial code for this class is as follows:

sealed class BookViewModel
{
    public ObservableCollection<IBook> Books { get; private set; }

    // load data command code

    // delete record command code
    // ...
    public void deleteAction(IEnumerable<string> isbnList)
    {
        // delete data from database
        // this already works
    }
}

There is already a command implemented to load data from the database. That was implemented in a very similar fashion to the answer to the following question: How to bind WPF button to a command in ViewModelBase?

What is to be achieved:

  1. When items in the DataGrid are selected, the UI elements for the delete command are activated if one or more items are selected. This is already achieved with the following event handler, in the codebehind for the main window:
private void dataGrid_selectionChanged(object sender, SelectionChangedEventArgs args)
{
    // this works

    // if nothing is selected, disable delete button and menu item
    if (BooksDataGrid.SelectedItems.Count == 0)
    {
         button_deleteBook.IsEnabled = false;
         menuItem_deleteBook.IsEnabled = false;
    }
    else
    {
        // delete command can now be executed, as shown in the binding in XAML
        button_deleteBook.IsEnabled = true;
        menuItem_deleteBook.IsEnabled = true;
    }
}
  1. The delete command to be implemented. What is not clear so far, is how to pass parameters to a command implemented in the ViewModel (DataContext for the View). I am new to WPF and trying to understand how commands work. Specifically, this command should take a parameter of IEnumerable<string>, or perhaps a collection of string. I have already completed and tested the deleteAction method. The string objects are to be the values in the "ISBN" column of the selected rows of the DataGrid.
BionicCode
  • 1
  • 4
  • 28
  • 44
Al2110
  • 566
  • 9
  • 25
  • You can bind the `CommandParameter` to the `DataGrid.SelectedItem` property, which returns the currently selected row data model. – BionicCode Jun 14 '20 at 13:38

2 Answers2

1

You've wandered into one of the tricky bits of wpf / mvvm in that what you would ideally want to use cannot be bound. Or at least not straight out the box.

If you wanted just a single item select and delete then you could just bind selecteditem to a property in your window viewmodel. The command could use the IBook object that gives to do a delete.

Since you want multiple selection and deletion that's a complication because you can't bind the entire list of selecteditems. This is not a bindable dependency property.

There are a number of ways round that.

You could sub class the datagrid and extend.

Or

You could use a behavior. What these allow you to do is encapsulate a chunk of event orientated code and add an attached dependency property to store the data. This itself is then bindable. I recommend you read up on behaviors generally and google a bit to take a look at examples. Binding selecteditems is a fairly common requirement and you should get a number of hits. Here's one though.

Select multiple items from a DataGrid in an MVVM WPF project

You end up with a List of observablecollection if IBook you can work with in your viewmodel.

I recommend observablecollection and you can subscribe to the collectionchanged event in the viewmodel so you can check the count. Use 0 to return false for CanExecute of your command and 1+ true.

https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.icommand.canexecute?view=netcore-3.1#System_Windows_Input_ICommand_CanExecute_System_Object_

Your iBook doesn't sound like it's going to be a viewmodel. It should be. Pretty much anything you're binding which isn't marked explicitly as OneTime should be a viewmodel that implements inotifypropertychanged. This is because there's a long existing bug which can give memory leaks otherwise. Don't worry about whether your viewmodel is going to leak or not. Just always use a viewmodel and build a base viewmodel implements inpc so you can easily inherit everything from that.

Andy
  • 11,864
  • 2
  • 17
  • 20
  • I think I would be better off just sending one item at a time. Could you show a pattern to use for passing a `string` parameter to a command implemented in the ViewModel? – Al2110 Jun 15 '20 at 12:16
0

The simplest solution is to use the build-in DataGrid.DeleteCommand.

DataGrid already supports row/cell deletion by default. Usually there is no need to create something sophisticated on your own. I would consider this waste of time.

The DataGrid exposes a static DataGrid.DeleteCommand, which is a routed command. DataGrid listens to this command using CommandManager.RegisterClassCommandBinding.
In addition to the DataGrid.DeleteCommand, there is support for a DataGrid.BeginEditCommand (Key.F2), DataGrid.CommitEditCommand, DataGrid.CancelEditCommand(Key.Escape), DataGrid.SelectAllCommand and ApplicationCommands.Copy.

You can always press CRTL + A to select all rows and then press DEL to delete the selected rows.

If you want to add mouse input controlled delete, you should add a delete button to each row. This is the most intuitive and established table design.
Doing so, to delete a single row the user don't need to execute four operations: first navigate to the target row, then second execute select row command on the row and then third navigate to the delete button and finally press the delete button. The user can now directly press the row's delete button.
This removes two operations in order to accomplish the goal (delete a row). In terms of user experience (UX) a goal must be achievable using as less as possible user operations and as less as possible mouse movement.
Multiselect delete is still possible. User has to select the target rows and then simply press a random delete button. DataGrid does the rest e.g. enable/disable the delete button.

The following example adds a delete Button to each row using DataGridTemplateColumn. The example assumes a table of two columns, which are auto-generated. By setting the DataGridTemplateColumn.DisplayIndex of the third delete button column to 2, positions this column rightmost.
You can use the DataGrid.FrozenColumnCount property to prevent the delete column from scrolling i.e. pin the column(s), which would require to position the delete column leftmost.

<DataGrid AutoGenerateColumns="True">
  <DataGrid.Columns>
    <DataGridTemplateColumn DisplayIndex="2">
      <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
          <Button Content="X" 
                  Command="{x:Static DataGrid.DeleteCommand}" />
        </DataTemplate>
      </DataGridTemplateColumn.CellTemplate>
    </DataGridTemplateColumn>
  </DataGrid.Columns>
</DataGrid>
BionicCode
  • 1
  • 4
  • 28
  • 44