Black Lotus
Black Lotus

Reputation: 2397

DataGrid unable to deselected modified item

I have a WPF application with a DataGrid. This DataGridis bound to an ObservableCollection that contains a bunch of models. The selection mode of this DataGrid is set to Extended.

When ever a item is currently selected, and then receives an update (something of the selected item changes during a refresh for example) and then the user attempts to select another item, it does not deselect the previous item.

The OnselectionChanged fires, but does not contain the previouse item, and there seems to be no way to deselect it.

The code that runs this (clean wpf application using .net core 3.1)

The DataGrid, that (as you can see) doesn't store any of the selected items either.

            <DataGrid Grid.Row="1"
                  x:Name="ItemsDataGrid"
                  SelectionMode="Extended"
                  ItemsSource="{Binding Items}">
            <DataGrid.Resources>
                <Style TargetType="{x:Type DataGridCell}">
                    <Style.Triggers>
                        <Trigger Property="DataGridCell.IsSelected" Value="True">
                            <Setter Property="Background" Value="Red" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </DataGrid.Resources>
        </DataGrid>

The items, which is a simple Observable Collection

private ObservableCollection<Window> _items = new ObservableCollection<Window>();

public ObservableCollection<Window> Items
{
    get => _items;
    set => SetProperty(ref _items, value);
 }

And the code that changes the item, it just grabs the first item, and adds a new Dimension

 private void ChangeItem()
    {
        var rand = new Random();

        Items[0].Dimensions = new Rect(rand.Next(1, 100), rand.Next(1, 100), rand.Next(1, 100), rand.Next(1, 100));
    }

And on request, here the code that adds the items to the ListBox

private void Button_Click(object sender, RoutedEventArgs e)
    {
        SelectedItemsListBox.Items.Clear();

        foreach(var item in ItemsDataGrid.SelectedItems)
        {
            SelectedItemsListBox.Items.Add(item);
        }

        SelectedItemsListBox.Items.Refresh();
    }

Here the application is just opend, and the "Refresh" button has been clicked to show 3 items No Selected Items

Here the first item has been selected, the "Change Item" button has been clicked that modified the Dimensions, also note how this item now shows it's description below, this is a listbox that displays all currently selected items and was refreshed (including a listbox.clear) after clicking "Display selected Items".

enter image description here

Here the 3th item has been clicked, selecting it, and should in turn also deselect the previously selected item, but as the listbox shows, it is still selected.

enter image description here

I already trimmed the issue down to this point, it ended up not being my selection method, it wasn't the MultiSelector i used, nothing of the UI frameworks i used, this barebone solution still has the issue putting me at a loss for words and out of idea's as to wat it might be.

Upvotes: 1

Views: 171

Answers (2)

EldHasp
EldHasp

Reputation: 7918

You are confused between selecting a cell and selecting an item (row) of DataGrid. Replace DataGrid and, I think, you yourself will understand the cause of your problem:

    <DataGrid Grid.Row="1"
              x:Name="ItemsDataGrid"
              SelectionMode="Extended"
              ItemsSource="{Binding ItemsCollection}">
        <DataGrid.ItemContainerStyle>
            <Style TargetType="DataGridRow">
                <Style.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter Property="Background" Value="Red" />
                    </Trigger>
                </Style.Triggers>
            </Style>
        </DataGrid.ItemContainerStyle>
    </DataGrid>

If @BionicCode correctly written about implementing IEquatable in a collection item, then remove it.
While this is not the cause of the problem, it can lead to others.
The ItemsControl (the DataGrid is derived from it) does not guarantee correct operation when the equal items are present in the collection.
Equality of elements is determined by the Equals() and GetHashCode() methods. These methods are also used in many others in the internal implementation of DependecyObject.
Therefore, it is advisable to use a reference type for data intended for WPF and not change the default Equals() and GetHashCode().
If you need to compare by value, you must either use an immutable implementation (possibly a struct) or create methods similar to Equals() and GetHashCode(), but them with other names.

You can also complement the ListBox with a binding to SelectedItems and it will display the selected in real time:

    <ListBox Grid.Row="2" ItemsSource="{Binding SelectedItems, ElementName=ItemsDataGrid}"/>

Upvotes: 0

BionicCode
BionicCode

Reputation: 28988

The behavior you are observing is related to your GetHashCode implementation.

From your posted class definition (on Pastebin), I was able to learn that your data items are implementing IEquatable and in this context also override object.GetHashCode.

You implementation computes the hash code based on mutable fields!
This should be generally avoided as it can lead to unexpected behavior (like you are experiencing right now).

"In general, for mutable reference types, you should override GetHashCode() only if:

  • You can compute the hash code from fields that are not mutable; or
  • You can ensure that the hash code of a mutable object does not change while the object is contained in a collection that relies on its hash code."

(Microsoft Docs: Notes to Inheritors)

The problem with mutable fields is that they can change while the object is used in a hash based collection.
If the field used for the hash code computation changes, then the hash code would change too and as the result the stored value of the original hash key would be lost.

Now you must know that DataGrid and Selector in general uses a hash table to store the selected items to improve lookup performance. Since your type implements IEquatable, the DataGrid is tempted to use the value returned by GetHashCode as key, because it assumes an overridden implementation.
This implementation of GetHashCode is checked by the DataGrid for reliability before used, but obviously this reliability check is not taking the mutability of the fields used for computation into account. Of course, this would require reflection. It seems quite reasonable to avoid reflection and just test the result of GetHashCode being constant after consecutive calls.

With this in mind we are now able to explain the behavior:

  • An item is selected and stored in a selected items hash table
  • DataGrid uses the item itself as key for the hash based selected items backing collection to improve lookup speed
  • Since the items implement IEquatable the hash table happily calls GetHashCode on the selected item to get an object hash as key for the value (the selected item)
  • You now modify the item by editing a cell of the DataGrid, which also results in an undesired change of the computed hash code
  • Next you select a different item. The DataGrid now tries to remove the previous selected item(s) from the selected items collection. But since the item's hash code has changed, the lookup won't return any item. Therefore the old and deselected item(s) remains in the selected items collection.

The solution is to follow the guidelines and avoid overriding object.GetHashCode, because in your case:

  • You cannot compute the hash code from fields that are not mutable (read-only) AND
  • You cannot ensure that the hash code of a mutable object does not change while the object is contained in a collection that relies on its hash code.

So removing/fixing the GetHashCode override will fix the issue.


Implementation improvements

Add a SelectedItems collection of type ObservableCollection as the binding source for the ListBox and add a DataGrid.SelectionChanged event handler:

View model

private ObservableCollection<Window> _items = new ObservableCollection<Window>();
public ObservableCollection<Window> Items
{
    get => _items;
    set => SetProperty(ref _items, value);
 }

private ObservableCollection<Window> _selectedItems = new ObservableCollection<Window>();
public ObservableCollection<Window> SelectedItems
{
    get => _selectedItems ;
    set => SetProperty(ref _selectedItems, value);
}

View (code-behind)

private void OnDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
{
  var viewModel = this.DataContext as ViewModel;
  foreach (Window addedItem in e.AddedItems.Cast<Window>())
  {
    viewModel.SelectedItems.Add(addedItem);
  }
  foreach (Window removedItem in e.RemovedItems.Cast<Window>())
  {
    viewModel.SelectedItems.Remove(removedItem);
  }
}

View:

<DataGrid SelectionMode="Extended"
          SelectionChanged="OnDataGridSelectionChanged">
   ...
</DataGrid>

<ListBox ItemsSource="{Binding SelectedItems}" />

Upvotes: 1

Related Questions