Search code examples
c#reactiveuidynamic-dataavaloniaui

Use Dynamic Data as WPF's CompositeCollection alternative in AvaloniaUI


I want to use Dynamic Data as WPF's CompositeCollection alternative in an AvaloniaUI project.

Here is some code to expose the issue :

public class MainWindowViewModel : ViewModelBase
{
    private readonly ReadOnlyObservableCollection<ViewModelBase> _testBind;
    public ReadOnlyObservableCollection<ViewModelBase> TestBind => _testBind;
    
    public IObservable<IChangeSet<ViewModelBase>> SeveralListTypes { get; set; }
    
    public IObservable<IChangeSet<ViewModelBase>> SeveralListTypes2 { get; set; }
    public IObservable<IChangeSet<ViewModelBase>> SeveralListTypes3 { get; set; }

    public ObservableCollection<ViewModelBase> ListTypesObject1 { get; set; } 
    public ObservableCollection<ViewModelBase> ListTypesObject2 { get; set; }
    public ObservableCollection<ViewModelBase> ListTypesObject3 { get; set; }
    public IObservable<IChangeSet<ViewModelBase>> InBoth { get; set; }
    
    
    private readonly ReadOnlyObservableCollection<ViewModelBase> _testBindTypes;
    public ReadOnlyObservableCollection<ViewModelBase> TestBindTypes => _testBindTypes;
    public MainWindowViewModel()
    {

        // TODO : those object collections should be of the real type and not from ancestor
        // ListTypesObject1 = new ObservableCollection<Object1>()

        ListTypesObject1 = new ObservableCollection<ViewModelBase>()
        {
            new Object1(),
        };
        
        ListTypesObject2 = new ObservableCollection<ViewModelBase>()
        {
            new Object2(),
        };
        
        ListTypesObject3 = new ObservableCollection<ViewModelBase>()
        {
            new Object3(),
        };


        // Change observableCollection to IObservable to be running with engine ReactiveUI
        SeveralListTypes = ListTypesObject1.ToObservableChangeSet();
        SeveralListTypes2 = ListTypesObject2.ToObservableChangeSet();
        SeveralListTypes3 = ListTypesObject3.ToObservableChangeSet();
        
        //Group All Observable into one with Or operator 
        InBoth = SeveralListTypes.Or(SeveralListTypes2).Or(SeveralListTypes3);

        // Bind => output to Binded Property for xaml
        // Subscribe => to be notified when changes
        var t = InBoth.Bind(out _testBindTypes)
            .DisposeMany()
            .Subscribe();
    }
    
    public void AddObject1()
    {
        var obj1 = new Object1("Added Object 1");
        ListTypesObject1.Add(obj1);
    }
    public void AddObject2()
    {
        var obj2 = new Object2("Added Object 2");
        ListTypesObject2.Add(obj2);
    }
    public void AddObject3()
    {
        if (ListTypesObject3 == null)
            return;
        var obj3 = new Object3("Added Object 3");
        ListTypesObject3.Add(obj3);
    }

    public void DeleteObject1()
    {
        if(ListTypesObject1 != null && ListTypesObject1.Count > 0)
            ListTypesObject1.RemoveAt(0);
    }
    public void DeleteObject2()
    {
        if (ListTypesObject2 != null && ListTypesObject2.Count > 0)
            ListTypesObject2.RemoveAt(0);
    }
    public void DeleteObject3()
    {
        if (ListTypesObject3 != null && ListTypesObject3.Count > 0)
            ListTypesObject3.RemoveAt(0);
    }
    
    public void DeleteObjectClear()
    {
        if (ListTypesObject3 == null)
            return;
        ListTypesObject3.Clear();
        ListTypesObject3 = null;
        ListTypesObject3 = new ObservableCollection<ViewModelBase>()
        {
            new Object3("Added object 3 from new list 3"),
        };
        SeveralListTypes3 = ListTypesObject3.ToObservableChangeSet();
        InBoth = InBoth.Or(SeveralListTypes3);
        // TODO : the collection we want to remove is still binded, the new one is not

    }

    public void DeleteObject3List()
    {
        if (ListTypesObject3 == null)
            return;
        ListTypesObject3.Clear();
        ListTypesObject3 = null;
        // TODO : remove the Object3List from DynamicData
    }

    public void CreateObject3List()
    {
        if (ListTypesObject3 != null)
            return;
        ListTypesObject3 = new ObservableCollection<ViewModelBase>()
        {
            new Object3("Added object 3 from new list 3"),
        };
        SeveralListTypes3 = ListTypesObject3.ToObservableChangeSet();
        InBoth = InBoth.Or(SeveralListTypes3);
        // TODO : the collection we want to remove is still binded, the new one is not

    }

}

Object1,Object2 and Object3 are an heritage from ViewModelBase.

DeleteObjectClear method remove all Object3 from the binding but then the new list isn't displayed.

How to add or remove an ObservableCollection and refresh the binded object (TestBind) ?

As a second issue, is it possible to use the real type of object in ObservableCollection (with a common ancestor) instead of ViewModelBase and still use Dynamic Data to agregate all collections ?

Here is the full github repository POC to spotlight the issue : https://github.com/Whiletru3/pocbindingdmo

Thanks


Solution

  • I can't really do it only with DynamicData, so I came with this solution. Not really elegant but it works...

    I created an ObservableCollectionAggregate class. I can Assign and Unassign the different (typed) ObservableCollections.

    public class ObservableCollectionAggregate : ObservableCollection<ViewModelBase>
    {
        private ObservableCollection<Object1> _subCollection1;
        private ObservableCollection<Object2> _subCollection2;
        private ObservableCollection<Object3> _subCollection3;
    
    
        public ObservableCollectionAggregate()
        {
            _subCollection1 = null;
            _subCollection2 = null;
            _subCollection3 = null;
        }
    
        public void UnassignCollectionObject1()
        {
            if (_subCollection1 != null)
            {
                RemoveItems(_subCollection1);
                _subCollection1.CollectionChanged -= OnSubCollectionChanged;
                _subCollection1 = null;
            }
        }
    
        public void AssignCollectionObject1(ObservableCollection<Object1> collection)
        {
            if (_subCollection1 != null)
            {
                UnassignCollectionObject1();
            }
    
            _subCollection1 = collection;
            AddItems(_subCollection1);
            _subCollection1.CollectionChanged += OnSubCollectionChanged;
    
        }
    
        public void UnassignCollectionObject2()
        {
            if (_subCollection2 != null)
            {
                RemoveItems(_subCollection2);
                _subCollection2.CollectionChanged -= OnSubCollectionChanged;
                _subCollection2 = null;
            }
        }
    
        public void AssignCollectionObject2(ObservableCollection<Object2> collection)
        {
            if (_subCollection2 != null)
            {
                UnassignCollectionObject2();
            }
    
            _subCollection2 = collection;
            AddItems(_subCollection2);
            _subCollection2.CollectionChanged += OnSubCollectionChanged;
    
        }
    
        public void UnassignCollectionObject3()
        {
            if (_subCollection3 != null)
            {
                RemoveItems(_subCollection3);
                _subCollection3.CollectionChanged -= OnSubCollectionChanged;
                _subCollection3 = null;
            }
        }
    
        public void AssignCollectionObject3(ObservableCollection<Object3> collection)
        {
            if (_subCollection3 != null)
            {
                UnassignCollectionObject3();
            }
    
            _subCollection3 = collection;
            AddItems(_subCollection3);
            _subCollection3.CollectionChanged += OnSubCollectionChanged;
    
        }
    
        private void AddItems(IEnumerable<ViewModelBase> items)
        {
            foreach (ViewModelBase me in items)
                Add(me);
        }
    
        private void RemoveItems(IEnumerable<ViewModelBase> items)
        {
            foreach (ViewModelBase me in items)
                Remove(me);
        }
    
        private void OnSubCollectionChanged(object source, NotifyCollectionChangedEventArgs args)
        {
            switch (args.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    AddItems(args.NewItems.Cast<ViewModelBase>());
                    break;
    
                case NotifyCollectionChangedAction.Remove:
                    RemoveItems(args.OldItems.Cast<ViewModelBase>());
                    break;
                case NotifyCollectionChangedAction.Replace:
                    RemoveItems(args.OldItems.Cast<ViewModelBase>());
                    AddItems(args.NewItems.Cast<ViewModelBase>());
                    break;
                case NotifyCollectionChangedAction.Move:
                    throw new NotImplementedException();
                case NotifyCollectionChangedAction.Reset:
                    if (source is ObservableCollection<Object1>)
                    {
                        RemoveItems(this.Where(c => c is Object3).ToList());
                    }
                    if (source is ObservableCollection<Object2>)
                    {
                        RemoveItems(this.Where(c => c is Object3).ToList());
                    }
                    if (source is ObservableCollection<Object3>)
                    {
                        RemoveItems(this.Where(c => c is Object3).ToList());
                    }
                    break;
            }
        }
    }
    

    then I can use it in my viewmodel using DynamicData for the binding :

    public class MainWindowViewModel : ViewModelBase
    {
        private readonly ReadOnlyObservableCollection<ViewModelBase> _testBind;
        public ReadOnlyObservableCollection<ViewModelBase> TestBind => _testBind;
        
        public ObservableCollectionAggregate AggregatedCollection { get; set; }
        public IObservable<IChangeSet<ViewModelBase>> AggregatedChangeSetFull { get; set; }
    
        public ObservableCollection<Object1> ListTypesObject1 { get; set; } 
        public ObservableCollection<Object2> ListTypesObject2 { get; set; }
        public ObservableCollection<Object3> ListTypesObject3 { get; set; }
    
    
        private readonly ReadOnlyObservableCollection<ViewModelBase> _testBindTypes;
        public ReadOnlyObservableCollection<ViewModelBase> TestBindTypes => _testBindTypes;
        public MainWindowViewModel()
        {
    
            ListTypesObject1 = new ObservableCollection<Object1>()
            {
                new Object1(),
            };
            
            ListTypesObject2 = new ObservableCollection<Object2>()
            {
                new Object2(),
            };
            
    
            AggregatedCollection = new ObservableCollectionAggregate();
    
            AggregatedCollection.AssignCollectionObject1(ListTypesObject1);
            AggregatedCollection.AssignCollectionObject2(ListTypesObject2);
    
    
            AggregatedChangeSetFull = AggregatedCollection.ToObservableChangeSet();
    
    
            // Bind => output to Binded Property for xaml
            // Subscribe => to be notified when changes
            var t = AggregatedChangeSetFull
                .DisposeMany()
                .ObserveOn(RxApp.MainThreadScheduler)
                .Bind(out _testBindTypes)
                .Subscribe();
    
        }
    
        public void AddObject1()
        {
            if (ListTypesObject1 == null)
                return;
            var obj1 = new Object1("Added Object 1");
            ListTypesObject1.Add(obj1);
        }
        public void AddObject2()
        {
            if (ListTypesObject2 == null)
                return;
            var obj2 = new Object2("Added Object 2");
            ListTypesObject2.Add(obj2);
        }
        public void AddObject3()
        {
            if (ListTypesObject3 == null)
                return;
            var obj3 = new Object3("Added Object 3");
            ListTypesObject3.Add(obj3);
        }
    
        public void DeleteObject1()
        {
            if(ListTypesObject1 != null && ListTypesObject1.Count > 0)
                ListTypesObject1.RemoveAt(0);
        }
        public void DeleteObject2()
        {
            if (ListTypesObject2 != null && ListTypesObject2.Count > 0)
                ListTypesObject2.RemoveAt(0);
        }
        public void DeleteObject3()
        {
            if (ListTypesObject3 != null && ListTypesObject3.Count > 0)
                ListTypesObject3.RemoveAt(0);
        }
    
        public void DeleteObject3List()
        {
            if (ListTypesObject3 == null)
                return;
            ListTypesObject3.Clear();
            ListTypesObject3 = null;
            AggregatedCollection.UnassignCollectionObject3();
        }
        
        public void CreateObject3List()
        {
            if (ListTypesObject3 != null)
                return;
            ListTypesObject3 = new ObservableCollection<Object3>()
            {
                new Object3("Added object 3 from new list 3"),
            };
            AggregatedCollection.AssignCollectionObject3(ListTypesObject3);
        }
    
    
    }
    

    Full working example in this branch of the repository : https://github.com/Whiletru3/pocbindingdmo/tree/ObservableCollectionAggregate