我一直在研究实际上基于ListBox
的高性能树视图。为此,我们首先从一个分层模型开始,其中每个项目都实现一个IParent
接口,该接口公开了一个可枚举的Children
属性。
然后,我们将该层次结构“平化”为基于列表的ViewModel,为每个项目添加一个“ depth”属性。然后,使用新的depth属性在我们的自定义ItemsSource
模板中缩进ListBox
,从而将该列表用作ContentPresenter
的{{1}}。所有这些工作都像冠军,并且允许我们显示数千个节点,这是普通ListBoxItem
会遇到的问题。这样做是因为,这只是现在的列表,TreeView
默认情况下很容易将其容器虚拟化,而ListBox
却在虚拟化方面举步维艰。
考虑此示例层次结构:
TreeView
展平后变成这个...
Parent1
Parent2
Child2a
Grandchild2a1
Grandchild2a2
Child2b
Parent3
Child3a
当前,我正在控件外部进行所有平坦化,但是如果我创建了Parent1, Level 0
Parent2, Level 0
Child2a, Level 1
Grandchild2a1, Level 2
Grandchild2a2, Level 2
Child2b, Level 1
Parent3, Level 0
Child3a, Level 1
,它就发生在我身上,它可以在内部进行平坦化,这意味着我可以将其用于 any 实现了HierarchicalItemsControl
的分层模型数据(或者即使没有通过GetChildren委托实现)。
我在使用这种方法时遇到的问题是在正常的IParent
中,ItemsControl
/ Items
属性中的项目之间存在一对一的关系,在ItemsSource
上排列的已创建容器。在这种情况下,存在一对多关系。
简单,我想...添加ItemsPanel
/ HierarchicalItems
属性,然后在展平后在内部设置常规HierarchicalItemsSource
/ Items
属性。这样可以保持一对一的关系。
存在ItemsSource
/ Items
属性是可读写的问题,这意味着人们可以直接操纵它们,这会破坏我控件的内部逻辑。
我开始认为我不能使用ItemsSource
子类,而不得不创建自己的ItemsControl
基类,手动重新实现HierarchicalItemsControl
的大部分内部结构,但我希望有另一种方式。
我要解决的主要问题是专门的ItemsControl
为给定项目创建多个容器的方法,而不是普通HierarchicalItemsControl
的一对一容器。
内部最终将与扁平化列表一对一,但在外部,我不希望人们能够操纵该扁平化列表(即,我想锁定ItemsControl
/ { {1}}为只读,但是我认为您不能这样做,因为它们是已注册的Items
,不是简单的CLR属性和AFAIK,因此您无法更改已注册的ItemsSource
'的可访问性。)
答案 0 :(得分:0)
好吧,所以,如果我对您的理解正确,那么您想绑定到Parents数组并让控件本身将其扩展到完整的扁平化列表,对吗?因此,假设您拥有这样的视图模型...
public class MainViewModel : ViewModelBase, IMainViewModel
{
// create a list of 10000 parents, each with 3 children
public IEnumerable<ViewModelItem> Items { get; } =
Enumerable.Range(1, 10000)
.Select(i => new ViewModelItem
{
Description = $"Parent {i}",
Children = new ViewModelItem[] { $"Child {i}a", $"Child {i}b", $"Child {i}c" }
});
}
public class ViewModelItem
{
public string Description { get; set; }
public ViewModelItem[] Children { get; set; }
public ViewModelItem()
{
}
public ViewModelItem(string desc)
{
this.Description = desc;
}
public static implicit operator ViewModelItem(string desc)
{
return new ViewModelItem(desc);
}
public override string ToString()
{
return this.Description;
}
}
...然后您可以使用常规转换器:
<Window.Resources>
<conv:FlattenedConverter x:Key="FlattenedConverter" ChildrenField="Children" />
</Window.Resources>
<ListBox ItemsSource="{Binding Items, Converter={StaticResource FlattenedConverter}}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling" />
转换器本身:
public class FlattenedConverter : IValueConverter
{
public string ChildrenField { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return FlattenArray(value as IEnumerable);
}
public IEnumerable FlattenArray(IEnumerable value)
{
if (value == null)
yield break;
foreach (var child in value)
foreach (var item in FlattenItem(child))
yield return item;
}
public IEnumerable FlattenItem(object value)
{
if (value == null)
yield break;
// return this item
yield return value;
// return any children
var property = value.GetType().GetProperty(this.ChildrenField);
if (property == null)
yield break;
var children = property.GetValue(value, null) as IEnumerable;
foreach (var child in FlattenArray(children))
yield return child;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
结果:
检查视觉树可确认这不会以任何方式破坏虚拟化,并且如果您希望转换器响应原始集合的更改,则很容易对其进行修改以支持INotifyCollectionChanged
。
这次我是否正确理解了这个问题?
更新:因此,使用我以前的视图模型并将ListBox替换为子类化的ListBox看起来像这样:
<controls:FlattenedListBox ItemsSource="{Binding Items}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling" />
为此,您将ListBox的依赖项属性替换为新属性,然后使用单向绑定在内部将两者绑定在一起,该单向绑定使用我上面发布的转换器代码:
public class FlattenedListBox : ListBox
{
public new IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public new static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(FlattenedListBox), new PropertyMetadata(null));
public FlattenedListBox()
{
Binding myBinding = new Binding("ItemsSource");
myBinding.Source = this;
myBinding.Mode = BindingMode.OneWay;
myBinding.Converter = new FlattenedConverter { ChildrenField = "Children" };
BindingOperations.SetBinding(this, ListBox.ItemsSourceProperty, myBinding);
}
}