SubsetCollection
I had an interesting design problem in Comicster over the weekend, and I thought I'd document my solution here.
In Comicster you categorise your trade paperbacks and titles in folders, and folders can also contain subfolders. Therefore, the Folder class has three properties: Folders, Trades and Titles, each an IList<T> (where T is of the corresponding type).
However, to ease data binding, Folder also has a property called Items, which (at first) was an IEnumerable<Item> ("Item" being the base class of Folder, Trade and Title). This means that I can bind a TreeView to the Items property and get the entire contents of that folder in one TreeViewItem.
The problem, though, is that IEnumerable<T> doesn't notify the UI when it changes. So if I add a title to a folder, my TreeView doesn't update.
I tried forcing the issue by calling NotifyPropertyChanged("Items") whenever my Folders, Trades or Titles collections changed, and that kind of worked, but the TreeView would completely repopulate and lose its SelectedItem. Ugly.
So what to do? I didn't want to make Items a normal ObservableCollection, because I wanted it to be read-only, and I like the strongly-typed approach of adding subfolders to the Folders collection, trades to the Trades collection etc. Logically the Items property had to become a ReadOnlyObservableCollection ... but how would I make it always contain the union of all three of my "real" collections?
So I struck upon an idea of defining a new class which derives from ObservableCollection, which I called SubsetCollection. When you construct it, you give it a "parent" collection, and anything that you add to this "subset" automatically gets added to the parent. Likewise, when you remove an item from the "subset", the item is removed from the parent.
The "parent" collection, in this case, is a private ObservableCollection<Item>, which is easily converted to a ReadOnlyObservableCollection<Item> for public use.
So here's my SubsetCollection class, and I'll show you some example usage code afterwards:
public class SubsetCollection<T, TAncestor>
: ObservableCollection<T> where T : TAncestor
{ public SubsetCollection(IList<TAncestor> parent) { _parent = parent; } private IList<TAncestor> _parent; protected override void InsertItem(int index, T item) { base.InsertItem(index, item); _parent.Add(item); } protected override void SetItem(int index, T item) { TAncestor oldItem = this[index]; base.SetItem(index, item); var i = _parent.IndexOf(oldItem); if (i >= 0) { _parent[ i ] = item; } } protected override void RemoveItem(int index) { TAncestor item = this[index]; base.RemoveItem(index); _parent.Remove(item); } protected override void ClearItems() { foreach (TAncestor item in this) { _parent.Remove(item); } base.ClearItems(); } }
See what's happening? You define the class with two types - "T", which is the type of object that will be stored in this subset, and "U", from which "T" must derive, which is the type of object that the "parent" collection contains.
Any time this subset is modified, it passes that change up to the parent.
Here's a pared-down version of my Folder class so you can see what I do to make this work:
public class Folder : Item { public Folder() { _folders = new SubsetCollection<Folder, Item>(_items); _trades = new SubsetCollection<Trade, Item>(_items); _readOnlyItems = new ReadOnlyObservableCollection<Item>(_items); } private ObservableCollection<Item> _items = new ObservableCollection<Item>(); private SubsetCollection<Folder, Item> _folders; private SubsetCollection<Trade, Item> _trades; private ReadOnlyObservableCollection<Item> _readOnlyItems; public IList<Folder> Folders { get { return _folders; } } public IList<Trade> Trades { get { return _trades; } } public ReadOnlyObservableCollection<Item> Items { get { return _readOnlyItems; } } }
So my "_folders" and "_trades" collections are subsets of the "_items" collection, which itself is made public as a read-only collection via the "_readOnlyItems" field.
Needless to say, this works a treat.
I wonder now, though, whether SyncLINQ might have taken care of this for me. Had I done a "union" LINQ query from folders, trades and titles, would SyncLINQ have kept the results up to date as each collection changed? Only one person knows for sure.
# Trackback from PaulStovell.NET » SubsetCollection: SyncLINQ Style on 17/02/2008 12:50 AM
Comments
# Paul Stovell
30/01/2008 12:26 PM
Hi Mabster
Answer is "yes". This is EXACTLY +he kind of scenario SyncLINQ solves. I might blog an example.
-Paul
# mabster
30/01/2008 12:37 PM
Thanks Paul!
Well that's actually a pleasant surprise! I have something that works right now, but I will investigate SyncLINQ closer with a mind to swap out my code and replace it with a simple union query down the track.
# Marc Ridey
18/02/2008 9:05 AM
Hi Mabster,
I had a similar problem on a recent project and took the opposite approach. I maintained a single collection and created filtered collections on the master collection to allow the databinding.
www.mridey.com/.../Post.aspx
But I must say Paul's solution looks so much cleaner and easier.
Marc
# mabster
18/02/2008 9:57 AM
Hi Mark,
Yeah, in my project I wanted the type-safety that the individual collections guaranteed me. Since "Folder" and "Trade" both derive from "Item", but so do other things (like "Publisher") that don't belong in the "Folder.Items" collection, I didn't want people to be able to do this:
myFolder.Items.Add(new Publisher());
... so all my modifications to the folder's items are done through Folder.Folders and Folder.Trades.