【问题标题】:ObservableDictionary not properly implementating INotifyCollectionChanged [duplicate]ObservableDictionary 未正确实现 INotifyCollectionChanged [重复]
【发布时间】:2020-05-08 16:09:48
【问题描述】:

为了将字典绑定到listbox,我使用了observable dictionary,它已由某人实现。

当我在 InitializeComponent() 之前初始化 ObservableDictionary 时,Dic[0] = "three"; 正在抛出 System.ArgumentOutOfRangeException

我尝试调试但没有成功的这个实现有问题。 有人可以指导我或指出这个实现有什么问题吗?

OnCollectionChanged(NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item));替换为OnCollectionChanged();(在ObservableDictionary之前InitializeComponent()初始化的情况下) 似乎解决了异常问题。这是真正的解决方案吗?

TestWindow.xaml

<Window x:Class="Wpf.TestWindow" x:Name="win"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    DataContext="{Binding ElementName=win}"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" Title="TestWindow" Height="450" Width="800">

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <StackPanel Orientation="Vertical">
        <ListBox MinHeight="100" MinWidth="100"
            DataContext="{Binding ElementName=win}"
            ItemsSource="{Binding Path=Dic}"
            SelectedValuePath="Key"                         
            DisplayMemberPath="Value"/>
        <Button Content="modify" MinWidth="100" Click="Button_Click"/>
    </StackPanel>
</Grid>

TestWindow.cs

using System.Collections.ObjectModel;
using System.Windows;    
namespace Wpf
{   
    public partial class TestWindow : Window, INotifyPropertyChanged
    {
        private ObservableDictionary<int, string> dic;
        public ObservableDictionary<int, string> Dic {
            get => dic;
            set
            {
                dic = value;
                PropertyChanged(this, new PropertyChangedEventArgs(nameof(Dic)));
            }
        }

        public TestWindow()
        {
            InitializeComponent();
            Dic = new ObservableDictionary<int, string>() { {0,"Zero" }, {1, "one" }, };
            Dic.Add(2, "two");    
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Dic[0] = "three";
            Dic.Remove(1);
        }
    }
}

ObservableDictionary.cs

using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;
using System.Collections.Specialized;

namespace System.Collections.ObjectModel
{
    public class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
    {
        private const string CountString = "Count";
        private const string IndexerName = "Item[]";
        private const string KeysName = "Keys";
        private const string ValuesName = "Values";

        private IDictionary<TKey, TValue> _Dictionary;
        protected IDictionary<TKey, TValue> Dictionary
        {
            get { return _Dictionary; }
        }

        #region Constructors
        public ObservableDictionary()
        {
            _Dictionary = new Dictionary<TKey, TValue>();
        }
        public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
        {
            _Dictionary = new Dictionary<TKey, TValue>(dictionary);
        }
        public ObservableDictionary(IEqualityComparer<TKey> comparer)
        {
            _Dictionary = new Dictionary<TKey, TValue>(comparer);
        }
        public ObservableDictionary(int capacity)
        {
            _Dictionary = new Dictionary<TKey, TValue>(capacity);
        }
        public ObservableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
        {
            _Dictionary = new Dictionary<TKey, TValue>(dictionary, comparer);
        }
        public ObservableDictionary(int capacity, IEqualityComparer<TKey> comparer)
        {
            _Dictionary = new Dictionary<TKey, TValue>(capacity, comparer);
        }
        #endregion

        #region IDictionary<TKey,TValue> Members

        public void Add(TKey key, TValue value)
        {
            Insert(key, value, true);
        }

        public bool ContainsKey(TKey key)
        {
            return Dictionary.ContainsKey(key);
        }

        public ICollection<TKey> Keys
        {
            get { return Dictionary.Keys; }
        }

        public bool Remove(TKey key)
        {
            if (key == null) throw new ArgumentNullException("key");

            TValue value;
            Dictionary.TryGetValue(key, out value);
            var removed = Dictionary.Remove(key);
            if (removed)
                //OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value));
                OnCollectionChanged();    
            return removed;
        }    

        public bool TryGetValue(TKey key, out TValue value)
        {
            return Dictionary.TryGetValue(key, out value);
        }


        public ICollection<TValue> Values
        {
            get { return Dictionary.Values; }
        }


        public TValue this[TKey key]
        {
            get
            {
                return Dictionary[key];
            }
            set
            {
                Insert(key, value, false);
            }
        }


        #endregion


        #region ICollection<KeyValuePair<TKey,TValue>> Members

        public void Add(KeyValuePair<TKey, TValue> item)
        {
            Insert(item.Key, item.Value, true);
        }

        public void Clear()
        {
            if (Dictionary.Count > 0)
            {
                Dictionary.Clear();
                OnCollectionChanged();
            }
        }

        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            return Dictionary.Contains(item);
        }


        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            Dictionary.CopyTo(array, arrayIndex);
        }


        public int Count
        {
            get { return Dictionary.Count; }
        }


        public bool IsReadOnly
        {
            get { return Dictionary.IsReadOnly; }
        }


        public bool Remove(KeyValuePair<TKey, TValue> item)
        {
            return Remove(item.Key);
        }


        #endregion


        #region IEnumerable<KeyValuePair<TKey,TValue>> Members


        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return Dictionary.GetEnumerator();
        }


        #endregion


        #region IEnumerable Members


        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)Dictionary).GetEnumerator();
        }


        #endregion


        #region INotifyCollectionChanged Members


        public event NotifyCollectionChangedEventHandler CollectionChanged;


        #endregion


        #region INotifyPropertyChanged Members


        public event PropertyChangedEventHandler PropertyChanged;


        #endregion


        public void AddRange(IDictionary<TKey, TValue> items)
        {
            if (items == null) throw new ArgumentNullException("items");


            if (items.Count > 0)
            {
                if (Dictionary.Count > 0)
                {
                    if (items.Keys.Any((k) => Dictionary.ContainsKey(k)))
                        throw new ArgumentException("An item with the same key has already been added.");
                    else
                        foreach (var item in items) Dictionary.Add(item);
                }
                else
                    _Dictionary = new Dictionary<TKey, TValue>(items);


                OnCollectionChanged(NotifyCollectionChangedAction.Add, items.ToArray());
            }
        }


        private void Insert(TKey key, TValue value, bool add)
        {
            if (key == null) throw new ArgumentNullException("key");


            TValue item;
            if (Dictionary.TryGetValue(key, out item))
            {
                if (add) throw new ArgumentException("An item with the same key has already been added.");
                if (Equals(item, value)) return;
                Dictionary[key] = value;

                //OnCollectionChanged();
                OnCollectionChanged(NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item));
            }
            else
            {
                Dictionary[key] = value;

                OnCollectionChanged(NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value));
            }
        }


        private void OnPropertyChanged()
        {
            OnPropertyChanged(CountString);
            OnPropertyChanged(IndexerName);
            OnPropertyChanged(KeysName);
            OnPropertyChanged(ValuesName);
        }


        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }


        private void OnCollectionChanged()
        {
            OnPropertyChanged();
            if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }


        private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem)
        {
            OnPropertyChanged();
            if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, changedItem));
        }


        private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
        {
            OnPropertyChanged();
            if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem));
        }


        private void OnCollectionChanged(NotifyCollectionChangedAction action, IList newItems)
        {
            OnPropertyChanged();
            if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItems));
        }
    }
}

【问题讨论】:

  • 我自己还没有特权,但是应该删除这个问题上的重复标志。这个问题不仅仅是询问ArgumentOutOfRangeException 是什么以及如何处理它。这个问题揭示了一个特定问题,即集合类型类的实现(似乎应该可以正常工作)在其周围的其他代码中导致异常。任何看到这个有特权的人都应该投票删除标签。
  • 我不确定这是对版主标志的正确使用——但它可能是。正常的做法是对其进行投票,但我们都没有特权。您可以尝试稍微编辑问题以显示这两个问题之间的差异,看看是否能让人们投票。如果不是,并且您仍然足够关心,那么标记一个 mod 可能是唯一的其他选择 - 但请记住,给定的 mod 在这个特定领域可能不够了解,无法判断这一点。
  • @Keith: “这个问题不仅仅是询问 ArgumentOutOfRangeException 是什么以及如何处理一个”——问题的当前呈现方式,它正是如此。如果显示的ObservableDictionary 类产生了不正确的结果,并且问题的作者正在寻求帮助来避免这些结果,那么他们应该询问这个问题,而不是ArgumentOutOfRangeException。如果他们不寻求修复字典,那么他们非常想知道如何避免异常,这就是标记重复的含义。
  • 话虽如此,使用INotifyCollectionChangedIDictionary 的整个概念存在根本缺陷。 .NET 中的基本字典接口是unordered,而通知接口采用ordered 集合。因此在事件参数中使用索引来描述事件。如果实现者要偏离此规范,则由他们完全记录其预期用途,并让客户端代码遵循该文档。但鉴于INotifyCollectionChanged 的许多客户可能没有该选项,恕我直言,这首先是个坏主意。

标签: c# wpf xaml dictionary observable


【解决方案1】:

在重现此问题并进行自己的测试后,我发现了问题。

查看调用堆栈,异常实际上来自CollectionView,这就是ItemsControl在内部用来监控它的ItemsSource。这就是为什么只有在绑定ObservableDictionary 时才会发生异常。

我将上面ObservableDictionary 的实现与ObservableCollection 的.NET 源代码进行了比较。 This link 将您带到源中的方法,该方法在替换项目时被调用。您会注意到它们使用了 NotifyCollectionChangedEventArgs 构造函数的重载,其中包括 index。但是ObservableDictionary 实现使用了不同的构造函数。

将构造函数切换到 ObservableCollection 使用的构造函数可防止此异常。

所以,除了:

private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
{
    OnPropertyChanged();
    if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem));
}

使用这个:

private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
{
    OnPropertyChanged();
    if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem, Dictionary.ToList().IndexOf(newItem)));
}

Remove() 正在抛出 Collection Remove event must specify item position. 异常,因为调用的 CollectionChanged 重载也缺少索引参数。

public bool Remove(TKey key)
        {
            if (key == null) throw new ArgumentNullException("key");

            TValue value;
            if (!Dictionary.TryGetValue(key, out value)) { return false; }
            var removeditem = new KeyValuePair<TKey, TValue>(key, value);
            var removedindex = Dictionary.ToList().IndexOf(removeditem);
            var removed = Dictionary.Remove(key);

            if (removed)
                OnCollectionChanged(NotifyCollectionChangedAction.Remove, removeditem, removedindex);

            return removed;
        }

在这种情况下调用的新重载:

private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem, int removedindex)
        {
            OnPropertyChanged();
            if (CollectionChanged != null) CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, changedItem, removedindex));
        }

我在 GitHub 页面上为原始代码(您链接到的代码)留下了一条注释,通知他们这个错误,以便他们可以修复它 - 或者至少让未来的用户知道它。

【讨论】:

  • 感谢您的详细解释,remove Remove() 集合事件仍有一个错误,它应该发送索引,而无法添加 Dictionary.ToList().IndexOf(changedItem),因为 changedItem 已被删除。
  • 我认为缺少像 private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair&lt;TKey, TValue&gt; changedItem, int removedindex) 这样的重载。 removedindex 将在 public bool Remove(TKey key) 方法处理。
  • @johan472 我同意。您应该使用这样的重载,在删除项目之前检查索引,然后传递该索引。
  • @johan472 去吧。
  • @johan472 我批准了您的编辑,但有一项重要更改:您需要在调用 IndexOf 之前检查密钥是否存在,以避免不必要的异常。
【解决方案2】:

您可能偶然发现了 ObservableDictionary 中的一个错误。当现有键的值被替换时,似乎在随后的 CollectionChanged 事件的 NotifyCollectionChangedEventArgs 中未指定索引。如果未指定,则 New/OldItemIndex 属性默认为 -1。 ListBox 使用的集合更改处理程序可能没有预料到这一点,或者故意不支持它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-12-31
    • 2022-01-05
    • 1970-01-01
    • 1970-01-01
    • 2012-02-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多