树形控件的层次结构数据模板将ItemsSource绑定到自身,并且不使用属性路径
我有一个自定义模型类,它实现 IList、IList<TItem>、ICollection、ICollection<TItem>、IEnumerable<TItem> 和 IEnumerable,我想把它用作 DataType,在 HierarchicalDataTemplate 下用于 TreeView。这个自定义集合在项的管理上使用一个底层/内部的 ObservableCollection<TItem>,所实现的接口应像 ObservableCollection 那样提供项。
关键是,我不想让那个自定义集合拥有一个 'Item' 或 'Children' 属性,让任何人都能不小心对那些通常暴露的属性调用 Add(item) 或 Remove(item) 类似的方法。我要在 XAML 中做的是:
<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 -->
问题: 有人能指出我的自定义泛型集合缺少什么,使它能够在 HierarchicalDataTemplate 处自我引用为 ItemsSource,而无需为子节点暴露属性吗?是的,我对WPF不太熟悉,但看起来是可能的,应该有办法。或者,我是在尝试实现某些不可能/不符合实现的问题,因此 必须 按照通常的方式暴露一个属性?
实现细节到目前为止(提供上下文,说明我们在处理哪种实现):
以下是一些类的模型摘要,其中一些本身就是集合,就是这样设计,旨在在 XAML 中直接引用它们自身,而不是把它们的公共属性成员作为子项宿主。所有的类/模型都派生自通用的抽象基类,包含大量受保护的虚方法(这是一个要求):
// 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 HierarchicalCollectionModel
抽象基类/基类在大多数场景的潜在用法上要复杂得多,为它们设定了舞台。这里是 FileSystemItem 类,类似的众多之一,仅用于整个应用程序:
// 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);
}
}
或按日期排序的项集合:
// 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 {
// ...
}
所有的类/模型,包括抽象集合类,都是从 ItemModel 或 VisualItemModel 派生的,基本上已经为绑定提供了一个 TreeViewItem 数据模型所需的一切。
当我在派生类中暴露一个 ObservableCollection<TItem> 时,一切都正常工作,如下所示:
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.
}
}
但这种方法允许使用者执行以下操作:
SomeFileSystemItem.Items.Add(someItemOutOfTheBlue);
为什么要让事情变得复杂?
这绕过了所有必要的检查、集合中的正确插入位置(IComparable)、重复存在与处理,甚至某个来自另一 TreeView 的项也不应侵入同一个 TreeView,以及其他许多在此不便逐一详述的要求。
我知道在添加完项后可以对集合进行排序,并在添加的任意一个 lambda项 之后进行适当的检查,但我正在处理多种数据类型,每个节点有数百项,大多数更新在几秒钟内完成:这是一个实时的待处理项、已处理项、传入项的集合,按日期排序的大多数用于跨越二十年的档案更新任务,而更新是实时进行的,来源包括各种文件/归档类型、记录的语音通话、本地硬盘上的数万条记录,可能通过本地网络达到百万条,同时网络持续提供新的数据流,更新时可能与旧数据相关。
我正在建立一个专门应用的基础,以便轻松对这些档案进行分组和显示,直接操作而不担心丢失数据和层级结构。我并不是来定义这些数据应如何规范化/更新/序列化;因此,最终用户将能访问大多数类,以及那个诱人的 'Item' 或 'Children' 属性,让他/她的工作变得如此简单;相反,这很可能会损坏所有数据(比如无效/损坏的 'Parent' 属性,甚至会炸掉整个档案……)
因此,确保永远不直接暴露底层集合非常重要,否则的替代方案是拦截直接添加的项,检测它是否通过了正确的检查,例如是否属于正确的 TreeView,从集合中移除它,然后以正确的方式重新添加,可能作为克隆。这一过程会引发大量的 PropertyChanged……
^^ 是的,这是那种极少见的边缘情况,没有人愿意处理;我也一样! 但我已经几乎别无选择,因为我不明白为什么我的集合根节点会出现在 TreeView,而不是它的项。TreeView ItemsSource 是最顶层的项,确实会出现。但当自引用时,HierarchicalDataTemplate ItemsSource 似乎并不能作为一个可枚举项的集合来工作,相反,我必须把底层集合暴露为一个属性并在绑定中指定一个 Path=Items。
解决方案
这就是有些日子你会觉得自己蠢到想消失的时刻……我忘了 Bindings需要小心定义:
这将 source 改为 Control,正如预期的那样,后者并非一个集合(但也不会抛出错误,这本身并无问题),因此没有要生成的项:
<TreeView ...>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{
Binding RelativeSource={RelativeSource Self},
UpdateSourceTrigger=PropertyChanged}">
...
点号(Path=.)反而把集合保持为 source:
<TreeView ...>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{
Binding Path=.,
UpdateSourceTrigger=PropertyChanged}">
...
就像写成:
<TreeView ...>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{
Binding UpdateSourceTrigger=PropertyChanged}">
...
或更具体地说,如EldHasp在下方评论中所提到的那样:
<TreeView ...>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{
Binding DataContext, RelativeSource={RelativeSource Self}}">
...
^^ DataContext 就是集合本身(那就是我将要使用的,感谢)
(另外,如所指出,UpdateSourceTrigger=PropertyChanged 在这种情况下完全不起作用:DataContext/collection 不是“自身”的属性,因为在 ItemsSource 绑定中没有任何 Path= 指定要监视/挂钩的 DataContext 属性成员,因此永远不会发生 PropertyChanged。)
我去找问题,但没有发现问题。类和集合都没问题,接口也没问题……这算是一个巨大的疏忽。抱歉带来麻烦。好吧,从今往后我每次写 RelativeSource=... 时都会格外小心……
多亏了另一个问题中的这个回答:
WPF Bind to itself, by Heinzi :
...这把源设置为控件本身,因此它将尝试访问UI控件的属性x...
好处是,现在我们可以为可实例化的集合提供正确的序列化与反序列化实现,对它们进行封印,然后继续。