15

I am just now getting my toes wet in WPF after several years of working in ASP.Net exclusively. The problem I am currently struggling with is that I have a custom collection class which I need to bind to a listbox. Everything seems to be working except for removing an item from the collection. When I try to I get the error: “Collection Remove event must specify item position.” The catch is that this collection does not use indexes so I am not seeing a way to specify the position and so far Google has failed to show me a workable solution…

The class is defined to implement ICollection<> and INotifyCollectionChanged. My internal items container is a Dictionary which uses the item’s Name(string) value for a key. Aside from the methods defined by these two interfaces, this collection has an indexer that allows items to be accessed by Name, and overrides for the Contains and Remove methods so that they can also be called with the item Name. This is working for Adds and Edits but throws the above exception when I try to remove.

Here is an excerpt of the relevant code:

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
}
Rozwel
  • 1,990
  • 2
  • 20
  • 30
  • Have you tried always giving a position of -1? – user7116 Jun 06 '11 at 17:16
  • According to the debug it defaults to -1. Some of the posts I have found where others have received similar errors state that the position must be an int >= 0 and < collection size. I am just not sure how to go about finding an index for a value that is in a dictionary… – Rozwel Jun 06 '11 at 17:20
  • 2
    Because of this exact problem, I can tell you that our group has "outlawed" attempts at ObservableDictionary and instead use a derived ObservableCollection that adds the business logic required to ensure uniqueness (by some property/function). – user7116 Jun 06 '11 at 17:23
  • Have you tried casting to dictionary to a list using .ToList() (in LINQ) and getting the index that way? – Camron B Jun 06 '11 at 17:23
  • @Camron - The thought of doing something along those lines has occurred to me, and will probably work, but I am hoping for something a bit more elegant/efficient. – Rozwel Jun 06 '11 at 17:30
  • Well, I don't know how much more elegant you are looking for. Dictionaries by design don't care about index numbers, but you need one. The reason that the .To*() extension methods were created was to easily allow movement among the various collection types, allowing the programmer to convert to use a feature of that type (dictionaries for unique keys, lists for counts and indexes, etc) – Camron B Jun 07 '11 at 19:58

4 Answers4

11

Your custom collection seems like a re-invention of KeyedCollection<TKey,TItem>, which internally uses a dictionary, and has indexes. The indexer for int indexes can get hidden if TKey is int or int-based enum, but this can be fixed.

As for making KeyedCollection work with WPF, I found this article, in which he basically makes an ObservableKeyedCollection<TKey,TItem> by implementing INotifyCollectionChanged and overriding SetItem(), InsertItem(), ClearItems(), and RemoveItem(), along with adding AddRange() and passing a Func<TItem,TKey> to the constructor for getting the TKey from a TItem.

Community
  • 1
  • 1
Joel B Fant
  • 24,406
  • 4
  • 66
  • 67
  • Somehow I was never aware of that class. Yes, it is very similar to what I am doing and looks like it should work as well. I can think of many places where I have coded this type of structure which may have benifited from using the KeyedCollection as a base but in this case I think the SortedList is a slightly better fit. – Rozwel Jun 09 '11 at 12:10
  • So I ran into some issues in another portion of my app which caused me to backtrack and change my framework design a bit. In the process I went ahead and changed over to using the KeyedCollection with an implementation very similar to what was in your second link. In the end this does appear to be a better solution for what I am doing. – Rozwel Jul 15 '11 at 18:48
2

Takes a little indirection, but you can do it with Linq. Not including error handling you can do this:

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

You could likewise use values instead of Keys, so long as you stay consistent.

Paul
  • 35,689
  • 11
  • 93
  • 122
  • I have limited experience with LINQ. I will have to do a bit of research to follow what you are doing here. – Rozwel Jun 06 '11 at 17:34
  • It's surprisingly straightforward. Select just transforms the items in a collection and returns a new collection, so in this case I'm saying (effectively) 'for each key in keys, return a new object with the key and its index' (in the first line). second line just says, 'get me the object whos key is equal to name, and then return the idx property of that object). – Paul Jun 06 '11 at 17:42
  • 1
    since the order of keys/values is not reliable and may change this could lead to unexpected/inconsistent results – mike Jul 29 '18 at 22:45
2

So I threw in a temporary hack by changing the remove event to a reset, and went off to work on some other areas of my code. When I came back to this issue I discovered/realized that the SortedList class would satisfy my requirements and allow me to implement the Collection Changed events correctly with minimal changes to my existing code.

For those not familiar with this class (I had never used it before), here is a quick summary based on the reading I have done so far. In most ways it appears to behave like a dictionary, though the internal structure is different. This collection maintains sorted lists of the keys and values instead of a hash table. This means that there is a bit more overhead involved with getting data into and out of the collection but its memory consumption is lower. How noticeable this difference is appears to be dependent on how much data you need to store and what data types you are using for your keys.

Since my data volume in this instance is relatively low, and I need to have the items in the listbox sorted by their name values, using this class seems to be a good answer in my case. If anyone has an argument why this class should not be used, please let me know.

Thanks to all for their suggestions and comments, hopefully this thread helps someone else along the way.

Rozwel
  • 1,990
  • 2
  • 20
  • 30
0

I was able to use the NotifyCollectionChangedAction.Replace action with an empty NewItems list to raise the CollectionChanged event successfully for a non-indexed collection.

Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
dtm
  • 794
  • 5
  • 15
  • It doesn't work. The item is removed from the collection but it's not affected on the UI. Can you please share an example on how you achieved this? – Shimmy Weitzhandler Nov 21 '17 at 23:27