WPF列表框&更改Hashcode的项

本文关键字:Hashcode 的项 更改 列表 WPF | 更新日期: 2023-09-27 18:15:45

我有一个ListBox绑定到一个项目集合,该项目具有用于生成GetHashCode()结果的ID。添加新项目时,它的ID为0,直到它第一次保存到数据库中。这导致我的ListBox抱怨;我相信原因是因为当一个项目第一次被ListBox使用时,它被存储在一个不期望哈希码改变的内部Dictionary中。

我可以通过从集合中删除未保存的项目来修复这个问题(I 必须在此阶段通知UI将其从字典中删除),保存到DB,并将其添加回集合。这是混乱的,我并不总是能够从我的Save(BusinessObject obj)方法访问集合。有人有解决这个问题的替代方案吗?

编辑回复Blam的回答:

我正在使用MVVM,所以我修改了代码来使用绑定。若要重现该问题,请单击添加,选择项目,单击保存,重复,然后尝试进行选择。我认为这表明ListBox仍然在其内部Dictionary中保留旧的哈希码,因此出现了冲突键错误。

<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++;
        }
    }
}
经过一些测试,我发现了一些事情:
  • 更改ListBox中项目的哈希码似乎可以正常工作。

  • ListBox break中更改选定项的哈希码
    它的功能。

当选择(单选择或多选择模式)时,IList ListBox.SelectedItems被更新。添加到选择项中的项被添加到SelectedItems中,不再包含在选择项中的项被删除。

如果项目的哈希码在被选中时发生了变化,则无法将其从SelectedItems中删除。即使手动调用SelectedItems.Remove(item), SelectedItems.Clear()并将SelectedIndex设置为-1也没有效果,并且该项目仍保留在IList中。这会导致在下一次选择它之后抛出异常,因为我相信它再次添加到SelectedItems

WPF列表框&更改Hashcode的项

谁有解决这个问题的替代方案?

对象的哈希码在对象生命周期内不能更改。你不应该使用可变数据来计算哈希码。

我没有想到我的回答会引起这样的讨论。这里有一些详细的解释,也许会对op有帮助。

让我们看一下代码中定义的一些可变实体类型,它覆盖了GetHashCode,当然还有Equals。相等性基于Id相等性:

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);
    }
}

在代码的某个地方,您已经创建了几个这种类型的实例:

        // 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 };

这是一些外部代码,使用Dictionary<Mutable, string>来实现内部目的:

        // 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]);

还是你的代码。假设您已经更改了key1中的Id。哈希码也改变了:

        // let's change the hashcode of key1:
        key1.Id = 4;

外部代码。这里,它尝试通过key1:

来定位一些数据。
Console.WriteLine(dictionary[key1]); // ooops! key1 was not found in dictionary

当然,您可以设计可变类型,它覆盖GetHashCodeEquals,并在可变数据上计算哈希码。但你不应该这样做,真的(除非你明确知道自己在做什么)。

不能保证任何外部代码不会在内部使用Dictionary<TKey, TValue>HashSet<T>

我怀疑你的代码的问题是它没有覆盖Equals

ListBox使用Equals来查找一个项目,所以如果有多个Equals返回true,那么它匹配多个项目,只是简单地搞砸了。
ListBox中的项必须基于Equals唯一。
如果您尝试将ListBox绑定到列表Int32或列表字符串并重复任何值,则它具有相同的问题。

当你说抱怨。它是怎么抱怨的?

在下面这个简单的例子中,ListView没有在改变GetHashCode的情况下达到收支平衡。

你执行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; }
    }
}