Reputation: 2397
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
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".
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.
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
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
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:
DataGrid
uses the item itself as key for the hash based selected items backing collection to improve lookup speedIEquatable
the hash table happily calls GetHashCode
on the selected item to get an object hash as key for the value (the selected item)DataGrid
, which also results in an undesired change of the computed hash codeDataGrid
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:
So removing/fixing the GetHashCode
override will fix the issue.
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