Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF ListBox & Items with Changing Hashcode

Tags:

c#

mvvm

wpf

listbox

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.

like image 744
Coder1095 Avatar asked May 28 '13 10:05

Coder1095


2 Answers

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.

like image 157
Dennis Avatar answered Oct 19 '22 20:10

Dennis


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; }
    }
}
like image 22
paparazzo Avatar answered Oct 19 '22 18:10

paparazzo