在没有索引的集合上实现INotifyCollectionChanged

15

我是一个长期专攻ASP.Net的开发者,现在刚开始涉足WPF。我目前遇到的问题是,我有一个需要绑定到listbox的自定义集合类。除了删除一个项目以外,一切都运作良好。当我尝试删除一个项目时,出现错误:"Collection Remove event must specify item position." 。问题在于这个集合不使用索引,所以我找不到指定位置的方法,到目前为止谷歌也没能给我提供可行的解决方案。

这个类定义了实现ICollection<>INotifyCollectionChanged接口。我的内部项目容器是一个Dictionary,它使用项的名称(string)值作为键。除了这两个接口定义的方法之外,这个集合还有一个索引器,可以通过名称访问项目,并重写了ContainsRemove方法,以便它们也可以使用项目名称调用。这对添加和编辑工作正常,但在尝试删除时会抛出上述异常。

下面是相关代码摘录:

class Foo
{
    public string Name
    {
        get;
        set;
    }
}
class FooCollection : ICollection<Foo>, INotifyCollectionChanged
{
    Dictionary<string, Foo> Items;

    public FooCollection()
    {
        Items = new Dictionary<string, Foo>();
    }

    #region ICollection<Foo> Members

    //***REMOVED FOR BREVITY***

    public bool Remove(Foo item)
    {
        return this.Remove(item.Name);
    }
    public bool Remove(string name)
    {
        bool Value = this.Contains(name);
        if (Value)
        {
            NotifyCollectionChangedEventArgs E = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, Items[name]);
            Value = Items.Remove(name);
            if (Value)
            {
                RaiseCollectionChanged(E);
            }
        }
        return Value;
    }
    #endregion

    #region INotifyCollectionChanged Members
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    private void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (CollectionChanged != null)
        {
            CollectionChanged(this, e);
        }
    }
    #endregion
}

你尝试过始终给出-1的位置吗? - user7116
根据调试,它默认为-1。我发现一些其他人收到类似错误的帖子表明,位置必须是int >= 0且<集合大小。我只是不确定如何找到字典中的值的索引... - Rozwel
2
因为这个确切的问题,我可以告诉你,我们的团队已经“禁止”尝试使用ObservableDictionary,而是使用一个派生的ObservableCollection,它添加了所需的业务逻辑以确保唯一性(通过某些属性/函数)。 - user7116
你尝试将字典转换为列表并使用 .ToList()(在LINQ中)获取索引了吗? - Camron B
@Camron - 我也曾考虑过这样做,而且可能会起作用,但我希望有一些更优雅/高效的解决方案。 - Rozwel
我不知道你还想要多么优雅的翻译。按设计,字典并不关心索引号,但你需要一个。创建 .To*() 扩展方法的原因是为了方便地在各种集合类型之间移动,使程序员能够转换为使用该类型的特性(字典用于唯一键,列表用于计数和索引等)。 - Camron B
4个回答

11

您的自定义集合似乎是对 KeyedCollection<TKey,TItem> 的重新实现,它在内部使用字典,并且具有索引。如果 TKeyint 或基于 int 的枚举类型,则索引器将对 int 索引进行隐藏,但是可以通过修复来解决这个问题。

至于使 KeyedCollection 与 WPF 兼容,我在这篇文章中发现了一个方法,作者基本上通过实现 INotifyCollectionChanged 并重写 SetItem()InsertItem()ClearItems()RemoveItem() 方法,以及添加 AddRange() 方法并将 Func<TItem,TKey> 传递给构造函数,从TItem中获取 TKey,来创建了一个 ObservableKeyedCollection<TKey,TItem>


我以前从未意识到那个类。是的,它与我正在做的非常相似,看起来也应该能够正常工作。我可以想到许多地方都编写了这种类型的结构,使用KeyedCollection作为基础可能会更好,但在这种情况下,我认为SortedList更适合一些。 - Rozwel
所以我在应用程序的另一部分遇到了一些问题,这迫使我回溯并稍微更改了我的框架设计。在此过程中,我继续使用了KeyedCollection,并实现了与您第二个链接中非常相似的方法。最终,这似乎是我所做的更好的解决方案。 - Rozwel

2

需要稍微绕一点路,但你可以用Linq做到。不包括错误处理,你可以这样做:

var items = dict.Keys.Select((k, i) => new { idx = i, key = k });
var index = items.FirstOrDefault(f => f.key == name).idx;

您也可以使用值代替键,只要保持一致即可。


我在LINQ方面的经验有限。我需要做一些研究才能理解你在这里做的事情。 - Rozwel
这其实非常简单。Select 只是转换集合中的项目并返回一个新的集合,所以在这种情况下,我要说(实际上)“对于 keys 中的每个键,返回一个带有键和其索引的新对象”(第一行)。第二行只是说,“获取键等于 name 的对象,然后返回该对象的 idx 属性”。 - Paul
1
由于键/值的顺序不可靠且可能发生变化,这可能导致意外/不一致的结果。 - mike

2
因此,我通过将删除事件更改为重置来进行了临时处理,并开始处理代码的其他部分。当我回到这个问题时,我发现/意识到SortedList类将满足我的要求,并允许我在最小更改现有代码的情况下正确实现集合更改事件。
对于那些不熟悉这个类(我以前从未使用过它)的人,这里是一个基于我到目前为止所做的阅读的快速摘要。在大多数方面,它看起来像字典,尽管内部结构不同。该集合维护按键和值排序的列表,而不是哈希表。这意味着在将数据放入和从集合中取出时需要更多的开销,但其内存消耗较低。这种差异有多明显似乎取决于您需要存储多少数据以及您用于键的数据类型。
由于在这种情况下我的数据量相对较低,并且我需要按名称值对列表框中的项目进行排序,因此在我的情况下使用此类似乎是一个很好的答案。如果有人认为不应该使用此类,请告诉我。
感谢所有人的建议和评论,希望这个线程能帮助其他人。

0

我成功地使用了NotifyCollectionChangedAction.Replace操作和一个空的NewItems列表,为一个非索引集合成功地触发了CollectionChanged事件。


1
它不起作用。该项已从集合中删除,但在用户界面上未受影响。您能否分享一下您是如何实现这一点的示例? - Shimmy Weitzhandler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接