We\'re creating an object hierarchy where each item has a collection of other items, and each item also has a Parent
property pointing to its parent item. Pretty st
How about you make sure that only the item's current collection can orphan the item. That way no other collection can set the item's parent while it belongs to a collection. You could use a unique key of some sort so that a third party couldn't get involved:
public sealed class ItemsCollection : ObservableCollection<Item>
{
private Dictionary<Item, Guid> guids = new Dictionary<Item, Guid>();
public ItemsCollection(Item owner)
{
this.Owner = owner;
}
public Item Owner { get; private set; }
private Guid CheckParent(Item item)
{
if (item.Parent != null)
throw new Exception("Item already belongs to another ItemsCollection");
//item.Parent = this.Owner; // <-- This is where we need to access the private Parent setter
return item.BecomeMemberOf(this);
}
protected override void InsertItem(int index, Item item)
{
Guid g = CheckParent(item);
base.InsertItem(index, item);
guids.Add(item, g);
}
protected override void RemoveItem(int index)
{
Item item = this[index];
DisownItem(item);
base.RemoveItem(index);
}
protected override void DisownItem(Item item)
{
item.BecomeOrphan(guids[item]);
guids.Remove(item);
}
protected override void SetItem(int index, Item item)
{
var existingItem = this[index];
if (item == existingItem)
return;
Guid g = CheckParent(item);
existingItem.BecomeOrphan(guids[existingItem]);
base.SetItem(index, item);
guids.Add(item, g);
}
protected override void ClearItems()
{
foreach (var item in this)
DisownItem(item);
base.ClearItems();
}
}
public class Item
{
public string Name { get; set; }
public Item Parent { get; private set; }
public ItemsCollection ChildItems;
public Item()
{
this.ChildItems = new ItemsCollection(this);
}
private Guid guid;
public Guid BecomeMemberOf(ItemsCollection collection)
{
if (Parent != null)
throw new Exception("Item already belongs to another ItemsCollection");
Parent = collection.Owner;
guid = new Guid();
return guid; // collection stores this privately
}
public void BecomeOrphan(Guid guid) // collection passes back stored guid
{
if (guid != this.guid)
throw new InvalidOperationException("Item can only be orphaned by its current collection");
Parent = null;
}
}
Obviously there is redundancy there; the item collection is storing a second item collection (the dictionary). But there are numerous options for overcoming that which I assume you can think of. It's beside the point here.
However I do suggest you consider moving the task of child-item management to the item class, and keep the collection as 'dumb' as possible.
EDIT: in response to your quesion, how does this prevent and item from being in two ItemsCollection
s:
You ask what the point of the guids is. Why not just use the collection instance itself?
If you replace the guid argument with a collection reference, you could add an item to two different collections like this:
{
collection1.InsertItem(item); // item parent now == collection1
collection2.InsertItem(item); // fails, but I can get around it:
item.BecomeOrphan(collection1); // item parent now == null
collection2.InsertItem(item); // collection2 hijacks item by changing its parent (and exists in both collections)
}
Now imagine doing this with the guid argument:
{
collection1.InsertItem(item); // item parent now == collection1
collection2.InsertItem(item); // fails, so...
item.BecomeOrphan(????); // can't do it because I don't know the guid, only collection1 knows it.
}
So you can't add an item to more than one ItemsCollection. And ItemsCollection is sealed so you can't subclass it and override its Insert method (and even if you did that, you still couldn't change the item's parent).
One solution I've used to control visibility of class members is to define the class as partial, and then in a different namespace declare the class as partial, and define the special visibility members you want.
This controls member visibility depending on the namespace chosen.
The only thing you'll have to wrap your head around is referencing. It can get complex, but once you have it figured out, it works.
You can do these sorts of things using Delegates:
public delegate void ItemParentChangerDelegate(Item item, Item newParent);
public class Item
{
public string Name{ get; set; }
public Item Parent{ get; private set; }
public ItemsCollection ChildItems;
static Item()
{
// I hereby empower ItemsCollection to be able to set the Parent property:
ItemsCollection.ItemParentChanger = (item, parent) => { item.Parent = parent };
// Now I just have to trust the ItemsCollection not to do evil things with it, such as passing it to someone else...
}
public static void Dummy() { }
public Item()
{
this.ChildItems = new ItemsCollection (this);
}
}
public class ItemsCollection : ObservableCollection<Item>
{
static ItemsCollection()
{
/* Forces the static constructor of Item to run, so if anyone tries to set ItemParentChanger,
it runs this static constructor, which in turn runs the static constructor of Item,
which sets ItemParentChanger before the initial call can complete.*/
Item.Dummy();
}
private static object itemParentChangerLock = new object();
private static ItemParentChangerDelegate itemParentChanger;
public static ItemParentChangerDelegate ItemParentChanger
{
private get
{
return itemParentChanger;
}
set
{
lock (itemParentChangerLock)
{
if (itemParentChanger != null)
{
throw new InvalidStateException("ItemParentChanger has already been initialised!");
}
itemParentChanger = value;
}
}
}
public ItemsCollection(Item owner)
{
this.Owner = owner;
}
public Item Owner{ get; private set; }
private CheckParent(Item item)
{
if(item.Parent != null) throw new Exception("Item already belongs to another ItemsCollection");
//item.Parent = this.Owner;
ItemParentChanger(item, this.Owner); // Perfectly legal! :)
}
protected override void InsertItem(int index, Item item)
{
CheckParent(item);
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
ItemParentChanger(this[index], null);
base.RemoveItem(index);
}
protected override void SetItem(int index, Item item)
{
var existingItem = this[index];
if(item == existingItem) return;
CheckParent(item);
ItemParentChanger(existingItem, null);
base.SetItem(index, item);
}
protected override void ClearItems()
{
foreach(var item in this) ItemParentChanger(item, null);
base.ClearItems();
}