diff --git a/src/OneWare.ErrorList/BatchObservableCollection.cs b/src/OneWare.ErrorList/BatchObservableCollection.cs new file mode 100644 index 000000000..b8ee57c1a --- /dev/null +++ b/src/OneWare.ErrorList/BatchObservableCollection.cs @@ -0,0 +1,67 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; + +namespace OneWare.ErrorList; + +/// +/// ObservableCollection that can suppress change notifications while a batch of mutations is applied and +/// raises a single Reset afterwards. +/// This is required for , which crashes +/// (ArgumentOutOfRangeException in AdjustCurrencyForRemove) when an item that is filtered out of the view is +/// removed while the current position is 0. Handling a Reset makes the view rebuild itself safely. +/// +internal class BatchObservableCollection : ObservableCollection +{ + private bool _isDirty; + private int _suspendLevel; + + public IDisposable BeginBatch() + { + _suspendLevel++; + return new BatchScope(this); + } + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (_suspendLevel > 0) + { + _isDirty = true; + return; + } + + base.OnCollectionChanged(e); + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + if (_suspendLevel > 0) return; + + base.OnPropertyChanged(e); + } + + private void EndBatch() + { + if (_suspendLevel == 0) return; + + _suspendLevel--; + if (_suspendLevel > 0 || !_isDirty) return; + + _isDirty = false; + OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count))); + OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + private sealed class BatchScope(BatchObservableCollection owner) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + owner.EndBatch(); + } + } +} diff --git a/src/OneWare.ErrorList/ViewModels/ErrorListViewModel.cs b/src/OneWare.ErrorList/ViewModels/ErrorListViewModel.cs index 97bacff8d..7f25b75eb 100644 --- a/src/OneWare.ErrorList/ViewModels/ErrorListViewModel.cs +++ b/src/OneWare.ErrorList/ViewModels/ErrorListViewModel.cs @@ -22,7 +22,7 @@ public class ErrorListViewModel : ExtendedTool, IErrorService { public const string IconKey = "MaterialDesign.ErrorOutline"; - private readonly ObservableCollection _items = new(); + private readonly BatchObservableCollection _items = new(); private readonly IMainDockService _mainDockService; private readonly IProjectExplorerService _projectExplorerExplorerViewModel; @@ -194,7 +194,11 @@ public void RegisterErrorSource(string source) public void ClearFile(string filePath) { - ListEx.RemoveMany(_items, _items.Where(x => x.FilePath.EqualPaths(filePath))); + using (_items.BeginBatch()) + { + ListEx.RemoveMany(_items, _items.Where(x => x.FilePath.EqualPaths(filePath))); + } + ErrorRefresh?.Invoke(this, filePath); RefreshCountToggle(); } @@ -204,7 +208,10 @@ public void Clear(string source) var errors = _items.Where(x => x.Source == source).ToList(); var files = errors.Select(x => x.FilePath).Distinct(); - ListEx.RemoveMany(_items, errors); + using (_items.BeginBatch()) + { + ListEx.RemoveMany(_items, errors); + } foreach (var file in files) ErrorRefresh?.Invoke(this, file); @@ -231,10 +238,13 @@ public IEnumerable GetErrorsForFile(string filePath) /// public void RefreshErrors(IList errors, string source, string filePath) { - ListEx.RemoveMany(_items, - _items.Where(x => x.FilePath.EqualPaths(filePath) && x.Source == source && !errors.Contains(x))); + using (_items.BeginBatch()) + { + ListEx.RemoveMany(_items, + _items.Where(x => x.FilePath.EqualPaths(filePath) && x.Source == source && !errors.Contains(x))); - foreach (var e in errors) Add(e); + foreach (var e in errors) Add(e); + } ErrorRefresh?.Invoke(this, filePath); RefreshCountToggle(); @@ -328,15 +338,23 @@ private void RefreshCountToggle() public void Clear(IProjectRoot project) { - ListEx.RemoveMany(_items, _items.Where(x => x.Root == project)); + using (_items.BeginBatch()) + { + ListEx.RemoveMany(_items, _items.Where(x => x.Root == project)); + } + ErrorRefresh?.Invoke(this, project); RefreshCountToggle(); } public void Clear(IProjectRoot project, string source) { - ListEx.RemoveMany(_items, - _items.Where(x => x.Root == project && x.Source == source)); + using (_items.BeginBatch()) + { + ListEx.RemoveMany(_items, + _items.Where(x => x.Root == project && x.Source == source)); + } + ErrorRefresh?.Invoke(this, project); RefreshCountToggle(); }