Выборочный биндинг коллекций внутри коллекций

Имеется коллекция объектов, внутри которых также присутствуют коллекции, каждый из объектов внутри коллекции обновляется. Необходимо по условию скопировать коллекции из вложенных объектов в одну и забиндить получившуюся коллекцию в контрол, но таким образом, что бы изменения в первоначальных объектах фиксировались в новой коллекции (что бы изменения в контроле отображались тоже), естественно inotifypropertychanged во всех объектах реализован. Согласно примеру изложенному на схеме, нужно, что бы изменения в коллекции 4 и изменения полей в объекте типа 4 переносились, в соответствующую коллекцию внутри коллекции 5. Получается мне нужна своего рода коллекция ссылок на объекты? Пробовал делать конвектор в который биндил всю коллекцию 1 и внутри перебирал объекты, получая в итоге нужную мне коллекцию, но она не обновляется когда идет изменение в коллекции 4 например, тк забинжена коллекция 1. Заранее спасибо!!Общая структура Ветка элемента коллекции, что бы было видно лучше


Ответы (1 шт):

Автор решения: VladD

Если я правильно понимаю, вам нужна коллекция, которая ведёт себя с одной стороны как ObservableCollection<>, а с другой стороны является выборкой из «внутренних» коллекций, аналогично тому, как ведёт себя SelectMany в LINQ. Ну что ж, давайте такую коллекцию напишем. У меня будет более простая структура:

Type1Items (коллекция<Type1>)
  |
  |-- Type1
  |     |
  |     *-- Type2Items (коллекция<Type2>)
  |           |
  |           |-- Type2
  |           |     * свойство Age
  |           |
  |           |-- Type2
  |                 * свойство Age
  |-- Type1
        |
        *-- Type2Items (коллекция<Type2>)
              |
              |-- Type2
              |     * свойство Age
              |
              |-- Type2
                    * свойство Age

и мы будем показывать в списке все экземпляры Type2, реагируя на изменения свойства Age. Для более глубокой степени вложенности вам придётся «нанизать» несколько коллекций. Приступим!

Итак, вот наша коллекция, которая будет вести себя как список элементов Type2VM. Мы её сделаем обобщённой, чтобы можно было легко использовать повторно. Параметризируемся двумя типами: типом промежуточного элемента, и типом конечного элемента, и реализуем интерфейс IEnumerable<TItem>, а также, конечно, INotifyCollectionChanged.

class SelectManyCollection<TSource, TItem> : IEnumerable<TItem>, INotifyCollectionChanged
{
    IEnumerable<TSource> sourceEnum;
    List<TSource> sourceEnumSnapshot;
    INotifyCollectionChanged sourceNotif;
    Func<TSource, IEnumerable<TItem>> itemSelector;
    Func<TSource, INotifyCollectionChanged> notifSelector;
    IEnumerable<TItem> resultingEnumerable;

    SelectManyCollection(
        IEnumerable<TSource> sourceEnum,
        INotifyCollectionChanged sourceNotif,
        Func<TSource, IEnumerable<TItem>> itemSelector,
        Func<TSource, INotifyCollectionChanged> notifSelector)
    {
        this.sourceEnum = sourceEnum;
        this.sourceNotif = sourceNotif;
        this.itemSelector = itemSelector;
        this.notifSelector = notifSelector;
        resultingEnumerable = sourceEnum.SelectMany(itemSelector);

        sourceNotif.CollectionChanged += OnSourceCollectionChanged;
        OnSourceCollectionChanged(
            null,
            new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        foreach (var item in sourceEnumSnapshot ?? Enumerable.Empty<TSource>())
            notifSelector(item).CollectionChanged -= OnInnerCollectionChanged;
        sourceEnumSnapshot = sourceEnum.ToList();
        foreach (var item in sourceEnumSnapshot)
            notifSelector(item).CollectionChanged += OnInnerCollectionChanged;
        RaiseCollectionChanged();
    }

    void OnInnerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) =>
            RaiseCollectionChanged();

    static public SelectManyCollection<TSource, TItem> Create<TColl, TInnerColl>(
            TColl sourceColl, Func<TSource, TInnerColl> selector)
        where TColl: IEnumerable<TSource>, INotifyCollectionChanged
        where TInnerColl: IEnumerable<TItem>, INotifyCollectionChanged
    {
        return new SelectManyCollection<TSource, TItem>(
            sourceColl, sourceColl,
            s => selector(s), s => selector(s));
    }

    void RaiseCollectionChanged() =>
        CollectionChanged?.Invoke(
            this,
            new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

    public event NotifyCollectionChangedEventHandler CollectionChanged;
    public IEnumerator<TItem> GetEnumerator() => resultingEnumerable.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

В sourceEnum вы храним обновляемую исходную коллекцию внутренних элементов, а в sourceEnumSnapshot — ещё текущий «снимок», то есть материализованное состояние. Мы будем обновлять sourceEnumSnapshot при приходе CollectionChanged для внутренней коллекции.

Каждый раз, когда внутренняя коллекция поменяется, мы будем переподписываться на изменения всех подколлекций. Когда любая из подколлекций поменяется, мы отправим нотификацию, что наша коллекция сама изменилась.

(Мне немного лень в этом учебном примере проводить различия между различными видами NotifyCollectionChangedAction, так что я всё время отправляю сигнал о «полном изменении», Reset.)

Вооружившись этим классом, набросамем тестовый пример.

Вот наши бесхитростные VM-классы:

class RootVM : VM
{
    public ObservableCollection<Type1VM> Type1Items { get; } = new();
}

class Type1VM : VM
{
    public ObservableCollection<Type2VM> Type2Items { get; } = new();
}

class Type2VM : VM
{
    int age;
    public int Age
    {
        get => age;
        set => Set(ref age, value);
    }
}

И конечно, нам понадобится референсная реализация INPC:

public class VM : INotifyPropertyChanged
{
    protected bool Set<T>(ref T field, T value,
                         [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;

        field = value;
        RaisePropertyChanged(propertyName);
        return true;
    }

    protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    public event PropertyChangedEventHandler PropertyChanged;
}

XAML будет тоже простым:

<ListView ItemsSource="{Binding}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Age}"/>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Код изменения элементов и установку DataContext я положу прямо в MainWindow, но так, конечно, в production-коде делать не стоит.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = SelectManyCollection<Type1VM, Type2VM>.Create(
                          root.Type1Items,
                          item => item.Type2Items);
        AddItems();
    }

    RootVM root = new();
    async void AddItems()
    {
        root.Type1Items.Add(
            new Type1VM()
            {
                Type2Items =
                {
                    new Type2VM() { Age = 1 },
                    new Type2VM() { Age = 2 }
                }
            });
        await Task.Delay(2000);
        root.Type1Items.Add(
            new Type1VM()
            {
                Type2Items =
                {
                    new Type2VM() { Age = 3 },
                    new Type2VM() { Age = 4 }
                }
            });
        await Task.Delay(2000);
        root.Type1Items[0].Type2Items[0].Age *= 1000;
        await Task.Delay(500);
        root.Type1Items[0].Type2Items[1].Age *= 1000;
        await Task.Delay(500);
        root.Type1Items[1].Type2Items[0].Age *= 1000;
        await Task.Delay(500);
        root.Type1Items[1].Type2Items[1].Age *= 1000;
        await Task.Delay(500);
    }
}

Получается:

анимашка, как же без неё?

→ Ссылка