ARTICLE AD BOX
I have a custom model class that implements IList, IList<TItem>, ICollection, ICollection<TItem>, IEnumerable<TItem>, and IEnumerable, that I want to use as DataType under HierarchicalDataTemplate for a TreeView. The custom collection uses an underlying/internal ObservableCollection<TItem> for items management, and the implemented interfaces are supposed to provide items just like an ObservableCollection.
The catch is, I don't want that custom collection to have an 'Item' or 'Children' property for anyone to accidentally call Add(item) or Remove(item) -like methods on those usually exposed properties. What I want to do in XAML is:
<TreeView ItemsSource="{Binding RootDirectories, ...}"> <TreeView.Resources> <HierarchicalDataTemplate DataType="{x:Type ioCtrl:FileSystemItem}" ItemsSource="{ Binding RelativeSource={RelativeSource Self}, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"> <!-- I don't want to do the following: --> <!--HierarchicalDataTemplate DataType="{x:Type ioCtrl:FileSystemItem}" ItemsSource="{ Binding Items, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"--> </HierarchicalDataTemplate> </TreeView.Resources> </TreeView> <!-- and no, this is not just yet another explorer, it will have virtual directories and files that does not actually exist, and directories with MULTIPLE physical directory sources, hence, MULTIPLE different files with possibly the SAME names, but they must all be displayed corretly -->The Question: Can someone point out what does my custom generic collection(s) lacks to self reference itself as the ItemsSource of the HierarchicalDataTemplate, without the need of an exposed property for child nodes. Yes, I'm not very familiar with WPF, but it seems possible, there should be a way. Or, am I trying to achieve something impossible/not per the implementation, and therefore must expose a property as per the usual way?
Implementation details so far (provided to give a context of what kind of implementation we are dealing with):
The following are classes model summary, some are collection all in themselves, made that way with the intended purpose to directly reference themseves in XAML, not one of their public property member as sub-items host. Everything derives from common abstract classes with many protected virtual methods (this is a requirement):
// Hierarchy // System.Object // ∟ class NotifyPropertyChangedBase : INotifyPropertyChanged // ∟ class ItemModel // ∟ class VisualItemModel // ∟ class VisualCollectionModel<TItem> // ∟ class VisualCollectionModel<TParent, TItem> // ∟ class HierarchicalCollectionModel<TClass, TParent, TItem> // ∟ class SomeFinalUsableClass public abstract class ItemModel: NotifyPropertyChangedBase { public bool IsSelected { get; set; } public bool IsVisible { get; set; } public virtual void ItemAddedToCollection(ItemModel collection) { ... } } public abstract class VisualItemModel: ItemModel { public bool IsExpanded { get; set; } } public abstract class VisualCollectionModel<TItem> : VisualItemModel, IList<TItem>, ICollection<TItem>, IList, ICollection, IEnumerable<TItem>, IEnumerable where TItem : VisualItemModel { // This is the internal collection protecte ObservableCollection<TItem> ItemsCollection { get { ... } } object IList.this[int index] { get {...} set {...} } public TItem this[int index] { get {...} set {...} } protected virtual Int32 AddItemDirect(TItem item) { ItemsCollection.Add(item); return ItemsCollection.Count - 1; } public Int32 Add(TItem item) { return AddItemDirect(item); // Call overridable method... } public Int32 Add(object obj) { if (obj == null) return -1; var item = obj as TItem; return AddItemDirect(item); // Call overridable method... } bool IList.Contains(object obj) { ... } bool ICollection<TItem>.Contains(TItem item) { ... } // ^^ all interface members are implemented } public abstract class HierarchicalCollectionModel<TClass, TParent, TItem> where TClass : VisualItemModel where where TParent : VisualCollectionModel<TClass> where where TItem : VisualItemModel { TParent Parent { get...; } // To have access to ancestors protected override void ItemAdded(TItem item) { ... } public override void ItemAddedToCollection(ItemModel collection) { var parent = collection as TParent; if (parent != null) SetParent(parent, true); base.ItemAddedToCollection(collection); } } // then a final class would derive from HierarchicalCollectionModelThe abstract base class(es) are way more complex as they sets the canvas for most cases potential usage. Here is the FileSystemItem class for instance, one of many of the like just for the entire application:
// Implementation for a single item type in TreeView public class FileSystemItem : HierarchicalCollectionModel<FileSystemItem, FileSystemItem, FileSystemItem>, IComparable { protected override Int32 AddItemDirect(TItem item) { ItemsCollection.InsertSorted(item); // InsertSorted is a custom generic extension method // to INSERT the IComparable TItem at the correct position in collection; // that's why ObservableCollection<>.Add() is to never be accessed directly } protected override void BeforeParentChanged( FileSystemItem oldParent, FileSystemItem newParent) { // inform child items their ancestor has vanished... // inform ancestors they have lost one descendant... } protected override void ProcessIsExpandedStateChanged(bool newState) { if (newState) SetIcon16(SelectedDirectoryIcon16, true); else SetIcon16(DirectoryIcon16, true); base.ProcessIsExpandedStateChanged(newState); } }or the set of date sorted items:
// Derived classes for any amount of different types in TreeView, // yet, strongly constrained alltogether public class RootNodeItem : HierarchicalCollectionModel<RootNodeItem, VisualItemModel, YearNodeItem>, IComparable { } public class YearNodeItem : HierarchicalCollectionModel<YearNodeItem, RootNode, MonthNodeItem>, IComparable { public Int32 Year { get; set; } } public class MonthNodeItem : HierarchicalCollectionModel<MonthNodeItem, YearNodeItem, DayNodeItem>, IComparable { public byte Month { get; set; } } public class DayNodeItem : HierarchicalCollectionModel<DayNodeItem, MonthNodeItem, EntryItem>, IComparable { public byte Day { get; set; } } public class EntryItem: VisualItem, IComparable { // ... }All the classes/models, including abstract collection classes, derives from ItemModel or VisualItemModel, which basically already provides all a TreeViewItem data model requires for binding.
When I expose an ObservableCollection<TItem> in deriving classes, everything works perfectly, like this:
public class FileSystemItem : HierarchicalCollectionModel<FileSystemItem, FileSystemItem, FileSystemItem>, IComparable { public ObservabeCollection<FileSystemItem> Items { // The property member I reference in binding, // with appropriate PropertyChanged implementation // that fires just perfect for every cases (add/remove/insert/clear...) return ItemsCollection; // this is the underlying collection. } }But that approach allows consumers to do this:
SomeFileSystemItem.Items.Add(someItemOutOfTheBlue);
Why make things difficult?
This bypasses all the necessary checks, proper insertion position in collection (IComparable), duplicate existence and handling, even some item from ANOTHER TreeView that should NEVER invade a fellow TreeView, among many other requirements too long to detail here.
I know I can sort the collection when done adding items and do the appropriate checks after any lambda item has been added, BUT, I'm dealing with mutiple types of datas that comes by hundreds of items per node, most of them updates in a matter of seconds: it's a live pool of pending items, handled items, incoming items, most sorted by date for an entire archive update task across two decades of data, while new updates comes live, which sources are various sort of files/archives types, recorded audio calls, ten thousand on local hard drive and maybe a million through local network, while network is permanently feeding streams of new datas that may be related to old data when those are updated.
I'm the one laying the basis of a dedicated application to easily group and display those archives for direct manipulation without fear of losing data and hierarchy. I'm not the one to actually define how those datas are to be normalized/updated/serialized; so, the end user will have access to most of the classes, and that tantalizing 'Item' or 'Children' property that woud make his/her work so simple; instead, that would likely corrupt all the datas (like invalid/broken 'Parent' property, would blow up the entire archive...)
That's why it's important I never give a direct access to the underying collection, otherwise, the alternative is to catch an item directly added, detect it didn't passed the proper checks like belonging to the proper TreeView, remove it from collection, then add it again the proper way, possibly as a clone. That woud raise a ton of PropertyChanged in the process...
^^ Yes, this is the 0.001% edge case nobody wants to deal with; me neither! but I'm running out of options as I don't understand why my collection root node is shown in TreeView, but not its items. The TreeView ItemsSource is the topmost item, it does show up. But the HierarchicalDataTemplate ItemsSource seems to not act as a collection with enumerable items when self referenced, instead, I have to expose the underlying collection as a property and specify a Path=Items in the binding.
