using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.DependencyInjection; using System.Collections; using System.Collections.Specialized; using System.Reactive.Disposables; namespace Toolkit.Foundation; public partial class ObservableCollection : ObservableObject, IObservableCollectionViewModel, IInitialization, IActivated, IDeactivating, IDeactivated, IList, IList, IReadOnlyList, INotifyCollectionChanged, ICollectionSynchronization, IServiceProviderRequired, IServiceFactoryRequired, IMediatorRequired, IPublisherRequired, IDisposerRequired, INotificationHandler>, INotificationHandler>, INotificationHandler>, INotificationHandler>, INotificationHandler>, INotificationHandler>, INotificationHandler>, INotificationHandler> where TViewModel : notnull, IDisposable { private readonly System.Collections.ObjectModel.ObservableCollection collection = []; private readonly IDispatcher dispatcher; private readonly Queue pendingEvents = []; private readonly Dictionary trackedProperties = []; [ObservableProperty] private int count; private Func? defaultSelectionFactory; [ObservableProperty] private bool isActivated; private bool isClearing; [ObservableProperty] private bool isInitialized; [ObservableProperty] private TViewModel? selectedItem; public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer) { Provider = provider; Factory = factory; Mediator = mediator; Publisher = publisher; Subscriber = subscriber; Disposer = disposer; dispatcher = Provider.GetRequiredService(); collection.CollectionChanged += OnCollectionChanged; } public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer, IEnumerable items) { Provider = provider; Factory = factory; Mediator = mediator; Publisher = publisher; Subscriber = subscriber; Disposer = disposer; dispatcher = Provider.GetRequiredService(); collection.CollectionChanged += OnCollectionChanged; AddRange(items); } public event NotifyCollectionChangedEventHandler? CollectionChanged; public IDisposer Disposer { get; private set; } public IServiceFactory Factory { get; private set; } bool IList.IsFixedSize => false; bool ICollection.IsReadOnly => false; bool IList.IsReadOnly => false; bool ICollection.IsSynchronized => false; public IMediator Mediator { get; } public IServiceProvider Provider { get; private set; } public IPublisher Publisher { get; private set; } public ISubscriber Subscriber { get; } object ICollection.SyncRoot => this; public TViewModel this[int index] { get => collection[index]; set => SetItem(index, value); } object? IList.this[int index] { get => index >= 0 && collection.Count > 0 ? collection[index] : null; set { TViewModel? item = default; try { item = (TViewModel)value!; } catch (InvalidCastException) { } this[index] = item!; } } public void Activate(Func activateDelegate, bool reset = false) { if (reset) { Clear(); } ActivationBuilder builder = activateDelegate.Invoke(); Publisher.Publish(builder.Value, builder.Key); } public void Activate(bool reset = false) { if (reset) { Clear(); } ActivationBuilder builder = ActivationBuilder(); Publisher.PublishUI(builder.Value, builder.Key); } public TViewModel Add(params object?[] parameters) where T : TViewModel { T? item = Factory.Create(args => { if (args is IInitialization initialization) { initialization.Initialize(); } }, parameters); Add(item); return item; } public void Add(TViewModel item) { int index = collection.Count; InsertItem(index, item); UpdateSelection(item); } public void Add(object item) { int index = collection.Count; InsertItem(index, (TViewModel)item); } int IList.Add(object? value) { TViewModel? item = default; try { item = (TViewModel)value!; } catch (InvalidCastException) { } Add(item!); return Count - 1; } public void AddRange(IEnumerable items) { foreach (TViewModel? item in items) { if (item is IInitialization initialization) { initialization.Initialize(); } Add(item); } } public void Clear(bool disposeItems = false) { isClearing = true; if (disposeItems) { foreach (TViewModel item in this.ToList()) { Disposer.Dispose(item); Disposer.Remove(this, item); } } ClearItems(); isClearing = false; } public void Clear() { isClearing = true; foreach (TViewModel item in this.ToList()) { Disposer.Dispose(item); Disposer.Remove(this, item); } ClearItems(); isClearing = false; } public void Commit() { foreach (object trackedProperty in trackedProperties.Values) { ((dynamic)trackedProperty).Commit(); } } public bool Contains(TViewModel item) => collection.Contains(item); bool IList.Contains(object? value) => IsCompatibleObject(value) && Contains((TViewModel)value!); public void CopyTo(TViewModel[] array, int index) => collection.CopyTo(array, index); void ICollection.CopyTo(Array array, int index) => collection.CopyTo((TViewModel[])array, index); public virtual void Dispose() { GC.SuppressFinalize(this); Disposer.Dispose(this); } public IEnumerator GetEnumerator() => collection.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)collection).GetEnumerator(); public Task Handle(RemoveEventArgs args) { if (IsActivated) { foreach (TViewModel item in this.ToList()) { if (args.Sender is not null && args.Sender.Equals(item)) { Remove(item); } } } else { pendingEvents.Enqueue(args); } return Task.CompletedTask; } public Task Handle(CreateEventArgs args) { if (IsActivated) { if (args.Sender is TViewModel item) { Add(item); } } else { pendingEvents.Enqueue(args); } return Task.CompletedTask; } public Task Handle(InsertEventArgs args) { if (IsActivated) { if (args.Sender is TViewModel item) { Insert(args.Index, item); } } else { pendingEvents.Enqueue(args); } return Task.CompletedTask; } public Task Handle(MoveToEventArgs args) { if (IsActivated) { Move(args.OldIndex, args.NewIndex); } else { pendingEvents.Enqueue(args); } return Task.CompletedTask; } public Task Handle(MoveEventArgs args) { if (IsActivated) { if (args.Sender is TViewModel item) { Move(args.Index, item); } } else { pendingEvents.Enqueue(args); } return Task.CompletedTask; } public Task Handle(ReplaceEventArgs args) { if (IsActivated) { if (args.Sender is TViewModel item) { Replace(args.Index, item); } } else { pendingEvents.Enqueue(args); } return Task.CompletedTask; } public Task Handle(RemoveAtEventArgs args) { if (IsActivated) { int index = args.Index; if (index >= 0 && index <= Count - 1) { RemoveAt(index); } } else { pendingEvents.Enqueue(args); } return Task.CompletedTask; } public Task Handle(SelectionEventArgs args) => Task.CompletedTask; public int IndexOf(TViewModel item) => collection.IndexOf(item); int IList.IndexOf(object? value) => IsCompatibleObject(value) ? IndexOf((TViewModel)value!) : -1; [RelayCommand] public virtual void Initialize() { if (IsInitialized) { return; } IsInitialized = true; Subscriber.Subscribe(this); OnInitialize(); Activate(); } public TViewModel Insert(int index = 0, params object?[] parameters) where T : TViewModel { T? item = Factory.Create(args => { if (args is IInitialization initialization) { initialization.Initialize(); } }, parameters); InsertItem(index, item); UpdateSelection(item); return item; } public void Insert(int index, TViewModel item) { InsertItem(index, item); UpdateSelection(item); } void IList.Insert(int index, object? value) { if (value is TViewModel item) { Insert(index, item); UpdateSelection(item); } } public bool Move(int oldIndex, int newIndex) { if (oldIndex < 0) { return false; } TViewModel item = this[oldIndex]; bool moveSelection = false; if (item is ISelectable oldSelection) { if (oldSelection.IsSelected) { moveSelection = true; SelectedItem = default; } } RemoveItem(oldIndex); InsertItem(newIndex, item); if (moveSelection) { if (item is ISelectable newSelection) { newSelection.IsSelected = true; dispatcher.Invoke(() => SelectedItem = item); } } return true; } public bool Move(int index, TViewModel item) { int oldIndex = collection.IndexOf(item); if (oldIndex < 0) { return false; } RemoveItem(oldIndex); Insert(index, item); return true; } public virtual Task OnActivated() { IsActivated = true; while (pendingEvents.Count > 0) { object current = pendingEvents.Dequeue(); Handle((dynamic)current); } return Task.CompletedTask; } public virtual Task OnDeactivated() { IsActivated = false; return Task.CompletedTask; } public virtual Task OnDeactivating() => Task.CompletedTask; public virtual void OnInitialize() { } public bool Remove(TViewModel item) { int index = collection.IndexOf(item); if (index < 0) { return false; } Disposer.Dispose(item); Disposer.Remove(this, item); TViewModel? oldSelection = SelectedItem; RemoveItem(index); if (item.Equals(oldSelection)) { int newIndex = Math.Min(index, Count - 1); TViewModel? selectedItem = newIndex >= 0 ? this[newIndex] : default; dispatcher.Invoke(() => SelectedItem = selectedItem); } return true; } void IList.Remove(object? value) { if (IsCompatibleObject(value)) { Remove((TViewModel)value!); } } public void RemoveAt(int index) => RemoveItem(index); public bool Replace(int index, params object?[] parameters) where T : TViewModel { if (index <= Count - 1) { RemoveItem(index); } else { index = Count; } T? item = Factory.Create(args => { if (args is IInitialization initialization) { initialization.Initialize(); } }, parameters); Insert(index, item); return true; } public bool Replace(int index, TViewModel item) { if (index <= Count - 1) { RemoveItem(index); } else { index = Count; } Insert(index, item); return true; } public void Reset(Action> factory, bool disposeItems = true) { SelectedItem = default; Clear(disposeItems); factory.Invoke(this); } public void Revert() { foreach (object trackedProperty in trackedProperties.Values) { ((dynamic)trackedProperty).Revert(); } } public void SetSource(IList source, Func? defaultSelectionFactory) { foreach (TViewModel item in source) { Add(item); } if (defaultSelectionFactory is not null) { this.defaultSelectionFactory = defaultSelectionFactory; SelectedItem = defaultSelectionFactory.Invoke(); } if (source is INotifyCollectionChanged observableSource) { observableSource.CollectionChanged -= SourceCollectionChanged; observableSource.CollectionChanged += SourceCollectionChanged; } } public void Track(string propertyName, Func getter, Action setter) { if (!trackedProperties.ContainsKey(propertyName)) { T initialValue = getter(); trackedProperties[propertyName] = new TrackedProperty(initialValue, setter, getter); } } protected virtual ActivationBuilder ActivationBuilder() => new(new ActivationEventArgs()); protected virtual void ClearItems() => collection.Clear(); protected virtual void InsertItem(int index, TViewModel item) { Disposer.Add(this, item); Disposer.Add(item, Disposable.Create(() => { if (item is IDisposable && !isClearing) { if (item is IList collection) { collection.Clear(); } Remove(item); } })); collection.Insert(index > Count ? Count : index, item); } protected virtual void OnSelectedItemChanged() { } protected virtual void RemoveItem(int index) => collection.RemoveAt(index); protected virtual void SetItem(int index, TViewModel item) => collection[index] = item; private static bool IsCompatibleObject(object? value) => (value is TViewModel) || (value == null && default(TViewModel) == null); private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) { Count = collection.Count; CollectionChanged?.Invoke(this, args); } partial void OnIsActivatedChanged(bool value) { if (value) { while (pendingEvents.Count > 0) { object current = pendingEvents.Dequeue(); Handle((dynamic)current); } } } partial void OnSelectedItemChanged(TViewModel? oldValue, TViewModel? newValue) { if (oldValue is ISelectable oldSelection) { oldSelection.IsSelected = false; } if (newValue is ISelectable newSelection) { newSelection.IsSelected = true; } Publisher.Publish(Selection.As(SelectedItem)); OnSelectedItemChanged(); } private void SourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) { case NotifyCollectionChangedAction.Add: if (args.NewItems is not null) { foreach (TViewModel newItem in args.NewItems) { Add(newItem); } } break; case NotifyCollectionChangedAction.Remove: if (args.OldItems is not null) { foreach (TViewModel oldItem in args.OldItems) { if (this.FirstOrDefault(x => x.Equals(oldItem)) is TViewModel removedItem) { Remove(removedItem); } } } break; case NotifyCollectionChangedAction.Reset: Clear(); if (sender is IEnumerable collection) { foreach (TViewModel item in collection) { Add(item); } if (defaultSelectionFactory is not null) { SelectedItem = defaultSelectionFactory.Invoke(); } } break; } } private void UpdateSelection(TViewModel item) { if (item is ISelectable newSelection) { if (newSelection.IsSelected) { if (SelectedItem is ISelectable oldSelection) { oldSelection.IsSelected = false; } dispatcher.Invoke(() => SelectedItem = item); } } } } public partial class ObservableCollection : ObservableCollection where TViewModel : IDisposable { [ObservableProperty] private TValue? value; public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer, TValue? value = default) : base(provider, factory, mediator, publisher, subscriber, disposer) { Value = value; } public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer, IEnumerable items, TValue? value = default) : base(provider, factory, mediator, publisher, subscriber, disposer, items) { Value = value; } protected virtual void OnChanged(TValue? value) { } partial void OnValueChanged(TValue? value) => OnChanged(value); } public partial class ObservableCollection : ObservableCollection where TViewModel : IDisposable { [ObservableProperty] private TKey key; [ObservableProperty] private TValue? value; public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer, TKey key, TValue? value = default) : base(provider, factory, mediator, publisher, subscriber, disposer) { Key = key; Value = value; } public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer, IEnumerable items, TKey key, TValue? value = default) : base(provider, factory, mediator, publisher, subscriber, disposer, items) { Key = key; Value = value; } } public class ObservableCollection : ObservableCollection { public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer) : base(provider, factory, mediator, publisher, subscriber, disposer) { } public ObservableCollection(IServiceProvider provider, IServiceFactory factory, IMediator mediator, IPublisher publisher, ISubscriber subscriber, IDisposer disposer, IEnumerable items) : base(provider, factory, mediator, publisher, subscriber, disposer, items) { } }