I have a ListBox
bound to a collection of items that have an ID that is used to generate the result of GetHashCode()
. When a new item is added, it has ID 0 until it is first saved to our database. This is causing my ListBox
to complain; I believe the reason is because when an item is first used by a ListBox
it is stored in an internal Dictionary
that does not expect the hashcode to change.
I can fix this by removing the unsaved item from the collection (I must notify the UI at this stage to remove it from the dictionary), save to the DB, and add it back to the collection. This is messy and I do not always have access to the collection from my Save(BusinessObject obj)
method. Does anyone have an alternative solution to this problem?
EDIT In respose to Blam's answer:
I am using MVVM so I have modified the code to use bindings. To reproduce the problem click add, select the item, click save, repeat, then try to make a selection. I think this demonstrates that the ListBox
is still holding onto the old hashcodes in it's internal Dictionary
, hence the conflicting keys error.
<Window x:Class="ListBoxHashCode.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button Click="Button_Click_Add" Content="Add"/>
<Button Click="Button_Click_Save" Content="Save Selected"/>
</StackPanel>
<ListBox Grid.Row="1" ItemsSource="{Binding List}" DisplayMemberPath="ID" SelectedItem="{Binding Selected}"/>
</Grid>
</Window>
public partial class MainWindow : Window {
public ObservableCollection<ListItem> List { get; private set; }
public ListItem Selected { get; set; }
private Int32 saveId;
public MainWindow() {
this.DataContext = this;
this.List = new ObservableCollection<ListItem>();
this.saveId = 100;
InitializeComponent();
}
private void Button_Click_Add(object sender, RoutedEventArgs e) {
this.List.Add(new ListItem(0));
}
private void Button_Click_Save(object sender, RoutedEventArgs e) {
if (Selected != null && Selected.ID == 0) {
Selected.ID = saveId;
saveId++;
}
}
}
EDIT 2 After some testing, I have discovered a few things:
Changing the hash code of an item in a ListBox
seems to work ok.
Changing the hash code of the selected item in a ListBox
breaks
it's functionality.
When a selection made (single or multiple selection mode) the IList ListBox.SelectedItems
is updated. Items that are added to the selection are added to SelectedItems
and items that are no longer included in the selection are removed.
If the hash code of an item is changed while it is selected, there is no way to remove it from SelectedItems
. Even manuallty calling SelectedItems.Remove(item)
, SelectedItems.Clear()
and setting SelectedIndex
to -1 all have no effect, and the item remains in the IList
. This causes exceptions to be thrown after the next time it is selected as I believe it is once again added to SelectedItems
.
Does anyone have an alternative solution to this problem?
Hash code of the object must not be changed during the object life-time. You shouldn't use mutable data for hash code calculation.
Update.
I didn't expect, that my answer will cause such discussion. Here's some detail explanation, may be it will help OP.
Let's look at some mutable entity type defined in your code, which overrides GetHashCode
and, of course, Equals
. The equality is based on Id
equality:
class Mutable : IEquatable<Mutable>
{
public int Id { get; set; }
public override int GetHashCode()
{
return Id.GetHashCode();
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var mutable = obj as Mutable;
if (mutable == null)
{
return false;
}
return this.Equals(mutable);
}
public bool Equals(Mutable other)
{
return Id.Equals(other.Id);
}
}
Somewhere in your code you've created several instances of this type:
// here's some mutable entities with hash-code, calculated using mutable data:
var key1 = new Mutable { Id = 1 };
var key2 = new Mutable { Id = 2 };
var key3 = new Mutable { Id = 3 };
And this is some external code, which uses Dictionary<Mutable, string>
for its internal purposes:
// let's use them as a key for the dictionary:
var dictionary = new Dictionary<Mutable, string>
{
{ key1, "John" },
{ key2, "Mary" },
{ key3, "Peter" }
};
// everything is ok, all of the keys are located properly:
Console.WriteLine(dictionary[key1]);
Console.WriteLine(dictionary[key2]);
Console.WriteLine(dictionary[key3]);
Again, your code. Suppose, you've changed Id
of key1
. The hash-code was changed too:
// let's change the hashcode of key1:
key1.Id = 4;
And again, external code. Here it tries to locate some data by key1
:
Console.WriteLine(dictionary[key1]); // ooops! key1 was not found in dictionary
Of course, you can design mutable types, which overrides GetHashCode
and Equals
, and calculate hash-code on mutable data. But you shouldn't do that, really (except the cases, when you definitely know, what are you doing).
You can't guarantee, that any external code won't use Dictionary<TKey, TValue>
or HashSet<T>
internally.
I suspect the problem with your code is that it did not override Equals
.
ListBox
uses Equals
to find an item so if the are multiple Equals that return true then it matches multiple items and just plain messes up.
Items in the ListBox
must be unique based on Equals.
If you try and bind a ListBox
to List Int32 or List string and repeat any of the values then it has the same problem.
When you say complain. How does it complain?
In this simple example below ListView
did not break even with a change to the GetHashCode
.
Did you implement INotifyPropertyChanged
?
<Window x:Class="ListViewGetHashCode.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Button Click="Button_Click" Content="Button"/>
<Button Click="Button_Click2" Content="Add"/>
<Button Grid.Row="0" Click="Button_Click_Save" Content="Save"/>
</StackPanel>
<ListBox Grid.Row="1" ItemsSource="{Binding BindingList}" DisplayMemberPath="ID" SelectedItem="{Binding Selected}" VirtualizingStackPanel.VirtualizationMode="Standard"/>
<!--<ListBox Grid.Row="1" x:Name="lbHash" ItemsSource="{Binding}" DisplayMemberPath="ID"/>-->
</Grid>
</Window>
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace ListViewGetHashCode
{
public partial class MainWindow : Window
{
ObservableCollection<ListItem> li = new ObservableCollection<ListItem>();
private Int32 saveId = 100;
private Int32 tempId = -1;
public MainWindow()
{
this.DataContext = this;
for (Int32 i = 1; i < saveId; i++) li.Add(new ListItem(i));
InitializeComponent();
}
public ObservableCollection<ListItem> BindingList { get { return li; } }
public ListItem Selected { get; set; }
private void Button_Click(object sender, RoutedEventArgs e)
{
Int32 counter = 0;
foreach (ListItem l in li)
{
l.ID = -l.ID;
counter++;
if (counter > 100) break;
}
}
private void Button_Click2(object sender, RoutedEventArgs e)
{
//li.Add(new ListItem(0)); // this is where it breaks as items were not unique
li.Add(new ListItem(tempId));
tempId--;
}
private void Button_Click_Save(object sender, RoutedEventArgs e)
{
if (Selected != null && Selected.ID <= 0)
{
Selected.ID = saveId;
saveId++;
}
}
}
public class ListItem : Object, INotifyPropertyChanged
{
private Int32 id;
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public Int32 ID
{
get
{
return (id < 0) ? 0 : id;
//if you want users to see 0 and not the temp id
//internally much use id
//return id;
}
set
{
if (id == value) return;
id = value;
NotifyPropertyChanged("ID");
}
}
public override bool Equals(object obj)
{
if (obj is ListItem)
{
ListItem comp = (ListItem)obj;
return (comp.id == this.id);
}
else return false;
}
public bool Equals(ListItem comp)
{
return (comp.id == this.id);
}
public override int GetHashCode()
{
System.Diagnostics.Debug.WriteLine("GetHashCode " + id.ToString());
return id;
//can even return 0 as the hash for negative but it will only slow
//things downs
//if (id > 0) return id;
//else return 0;
}
public ListItem(Int32 ID) { id = ID; }
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With