How do I make changes in one ObservableCollection trigger a notification in another ObservableCollection?

80 Views Asked by At

In my C# application, I have two ObservableCollection properties:

public ObservableCollection<Material> Materials { get; init; } = new();

public ObservableCollection<string> MaterialNames
{
    get
    {
        ObservableCollection<string> names = new();

        foreach (Material material in Materials)
        {
            names.Add(material.Name);
        }
        return names;
    }
}

The second property shadows the first one, returning Name from the objects in the first list. A WPF ListBox is binded to this second property. What I need is for the second list to fire an update when members are added or removed from the first one, so that the ListBox updates. Right now, that doesn't happen. What is the easiest solution to implement this, "connecting" the notifications of both ObservableCollections?

Disclaimer: I know it is easier to bind to the first property and set DisplayMemberPath to Name, but due to certain constrains in the application I need to bind to a separate property.

2

There are 2 best solutions below

6
JonasH On

The easiest approach is to use an immutable collection as the source, and just replace the entire collections when needed. All changes must be done by replacing the immutable collection.

private ImmutableArray<Materials> MyMaterials{
   get => myMaterials;
   set{
       myMaterials = value;
       Materials = new ObservableCollection<Material>(value);
       MaterialNames = new ObservableCollection<string>(value.Select(m => m.Name));
       OnPropertyChanged();
   }
}
public ObservableCollection<Material> Materials {
   get => ...
   set => ...
}
public ObservableCollection<string> MaterialNames  {
   get => ...
   set => ...
}

You can do things like projecting changes from one list to another, just listen to CollectionChanged and replicate the changes to your other collection. But I find it fairly difficult to figure out exactly what have changed. So my personal preference is to use a custom list with more informative events I can use to replicate changes to a ObservableCollection.

If you need to serialize this data I would recommend creating separate "Data Transfer Objects" (DTOs) that only includes plain lists/arrays that can be handled by the serialization library. You can then create your UI model from this. That way you are not mixing UI concerns with serialization concerns.

But the better solution is almost certainly to address whatever issues that prevent you from just using DisplayMemberPath

3
EldHasp On

Overall I think your implementation is wrong. The ListBox should be bound to the Materials collection, but only display the Name Material property. Therefore, the MaterialNames collection is redundant.

But I thought it would be interesting to implement the binding you need. Here is my implementation. Check it out.

using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace Core2023.SO.Justinas_Rubinovas
{
    public class BindingCollections
    {
        public ObservableCollection<Material> Materials { get; } = new();

        public ReadOnlyObservableCollection<string> MaterialNames { get; }
        private readonly ObservableCollection<string> _materialNames = new();

        public BindingCollections()
        {
            MaterialNames = new(_materialNames);
            Materials.CollectionChanged += OnMaterialsChanged;
        }

        private void OnMaterialsChanged(object? sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    _ = e.NewItems ?? throw new NullReferenceException();
                    {
                        for (int i = e.NewItems.Count - 1; i >= 0; i--)
                        {
                            _materialNames.Insert(e.NewStartingIndex, ((Material?)e.NewItems[i])?.Name ?? string.Empty);
                        }
                    }
                    break;
                case NotifyCollectionChangedAction.Move:
                    _ = e.NewItems ?? throw new NullReferenceException();
                    {
                        if (e.NewStartingIndex < e.OldStartingIndex)
                            for (int i = 0; i < e.NewItems.Count; i++)
                            {
                                _materialNames.Move(e.OldStartingIndex + i, e.NewStartingIndex + i);
                            }
                        else if (e.NewStartingIndex > e.OldStartingIndex)
                            for (int i = e.NewItems.Count - 1; i >= 0; i--)
                            {
                                _materialNames.Move(e.OldStartingIndex + i, e.NewStartingIndex + i);
                            }
                    }
                    break;
                case NotifyCollectionChangedAction.Remove:
                    _ = e.OldItems ?? throw new NullReferenceException();
                    {
                        for (int i = e.OldItems.Count - 1; i >= 0; i--)
                        {
                            _materialNames.RemoveAt(e.OldStartingIndex);
                        }
                    }
                    break;
                case NotifyCollectionChangedAction.Replace:
                    _ = e.NewItems ?? throw new NullReferenceException();
                    {
                        for (int i = 0; i < e.NewItems.Count; i++)
                        {
                            _materialNames[e.NewStartingIndex + i] = ((Material?)e.NewItems[i])?.Name ?? string.Empty;
                        }
                    }
                    break;
                case NotifyCollectionChangedAction.Reset:
                    {
                        int i = 0;
                        for (; i < Materials.Count && i < _materialNames.Count; i++)
                        {
                            _materialNames[i] = Materials[i].Name;
                        }
                        for (; i < Materials.Count; i++)
                        {
                            _materialNames.Add(Materials[i].Name);
                        }
                        for (; i < _materialNames.Count;)
                        {
                            _materialNames.RemoveAt(_materialNames.Count - 1);
                        }
                    }
                    break;
                default:
                    throw new NotImplementedException();

            }
        }
    }

    public class Material
    {
        public string Name { get; set; } = string.Empty;
    }
}