您可以将ItemsControl中的ItemsPanel绑定到Items集合的超集吗?

时间:2019-01-23 22:22:14

标签: wpf listbox hierarchical-data itemscontrol

我一直在研究实际上基于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'的可访问性。)

1 个答案:

答案 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();
    }
}

结果:

enter image description here

检查视觉树可确认这不会以任何方式破坏虚拟化,并且如果您希望转换器响应原始集合的更改,则很容易对其进行修改以支持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);
    }
}