Можете ли вы связать ItemsPanel в ItemsControl с расширенным набором коллекции Items? - PullRequest
0 голосов
/ 24 января 2019

Я работал над высокопроизводительным древовидным представлением, которое на самом деле основано на ListBox. Чтобы достичь этого, мы сначала начнем с иерархической модели, в которой каждый элемент реализует интерфейс IParent, который предоставляет перечисляемое свойство Children.

Затем мы «сплющиваем» эту иерархию в ViewModel на основе списка, добавляя свойство «глубина» к каждому элементу. Затем мы используем этот список как ItemsSource из ListBox, используя новое свойство глубины для отступа ContentPresenter в нашем пользовательском шаблоне 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

В настоящее время я делаю все это выравнивание, внешнее по отношению к элементу управления, но мне пришло в голову, что если я вместо этого создаю HierarchicalItemsControl, он может сделать это выравнивание внутренне, то есть я мог бы использовать его для любой данные иерархической модели, которые реализовали IParent (или даже если это не так, через делегат GetChildren.)

Проблема, с которой я сталкиваюсь при таком подходе, заключается в обычном ItemsControl, существует взаимно-однозначное отношение между элементами в свойствах Items / ItemsSource и созданными контейнерами, которые расположены на ItemsPanel. В этом случае существует отношение один ко многим.

Просто, я думал ... добавьте HierarchicalItems / HierarchicalItemsSource свойства, а затем установите обычные свойства Items / ItemsSource после выравнивания. Это поддерживало бы отношения один-к-одному.

Проблема в том, что свойства Items / ItemsSource доступны для чтения / записи, что означает, что люди могут напрямую ими манипулировать, и это нарушит внутреннюю логику моего контроля.

Я начинаю думать, что не могу использовать подкласс ItemsControl, и вместо этого мне придется создавать собственный базовый класс HierarchicalItemsControl, переопределяя большинство внутренних элементов ItemsControl вручную, но я надеюсь, что есть по-другому.

В итоге ...

Основная проблема, которую я пытаюсь решить, - это способ для этого специализированного HierarchicalItemsControl создать несколько контейнеров для каждого элемента в отличие от обычного ItemsControl.

.

Внутренне это в конечном итоге будет один к одному со сглаженным списком, но внешне я не хочу, чтобы люди могли манипулировать этим сглаженным списком (то есть я хотел заблокировать Items / ItemsSource только для чтения, но я не думаю, что вы можете сделать это, поскольку они зарегистрированы DependencyProperties, а не простые свойства CLR и AFAIK, вы не можете изменить доступность зарегистрированного DependencyProperty.)

1 Ответ

0 голосов
/ 26 января 2019

Хорошо, так что, если я вас правильно понимаю, вы хотите привязать массив массива 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);
    }
}
...