diff --git a/ILSpy.Tests/Metadata/MetadataFilterTests.cs b/ILSpy.Tests/Metadata/MetadataFilterTests.cs index fb98b0991..a8d40f04b 100644 --- a/ILSpy.Tests/Metadata/MetadataFilterTests.cs +++ b/ILSpy.Tests/Metadata/MetadataFilterTests.cs @@ -89,6 +89,41 @@ public class MetadataFilterTests }).Should().BeFalse(); } + [Test] + public void Setting_ColumnFilter_Text_Raises_ColumnFilterChanged_On_The_Page() + { + // The view subscribes to the page's ColumnFilterChanged event to drive its + // DataGridCollectionView.Refresh — without this forwarding the filter row would be + // invisible to the grid and typing would have no effect on visible rows. + var page = new MetadataTablePageModel(); + var filter = new ColumnFilter("Name"); + page.ColumnFilters.Add(filter); + var fired = 0; + page.ColumnFilterChanged += () => fired++; + + filter.Text = "System"; + + fired.Should().Be(1); + } + + [Test] + public void Removing_A_ColumnFilter_Stops_Forwarding_Its_Text_Changes() + { + // When Populate clears the collection on a schema swap, the previously-attached + // filters must stop driving refreshes — otherwise stale headers from a former tab + // would keep firing into the new view's CollectionView. + var page = new MetadataTablePageModel(); + var filter = new ColumnFilter("Name"); + page.ColumnFilters.Add(filter); + page.ColumnFilters.Remove(filter); + var fired = 0; + page.ColumnFilterChanged += () => fired++; + + filter.Text = "System"; + + fired.Should().Be(0); + } + [Test] public void MatchesFilters_Returns_False_When_The_Filtered_Column_Does_Not_Exist_On_The_Row() { diff --git a/ILSpy/ViewModels/MetadataTablePageModel.cs b/ILSpy/ViewModels/MetadataTablePageModel.cs index 0e85bc606..2f16b7a51 100644 --- a/ILSpy/ViewModels/MetadataTablePageModel.cs +++ b/ILSpy/ViewModels/MetadataTablePageModel.cs @@ -20,6 +20,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; using System.Reflection; using Avalonia.Controls; @@ -59,13 +61,34 @@ namespace ILSpy.ViewModels public ObservableCollection ColumnFilters { get; } = new(); /// - /// Raised whenever any column's filter text changes. The view subscribes to this so - /// it can refresh the underlying DataGridCollectionView without each column - /// having to know how to find the live view instance. + /// Raised whenever any column's filter text changes. The view subscribes here to + /// refresh its DataGridCollectionView; tests use it as the canonical filter + /// signal too. The page wires this internally via ColumnFilters.CollectionChanged + /// + each filter's so callers don't have to chase + /// individual filter instances. /// public event Action? ColumnFilterChanged; - internal void RaiseColumnFilterChanged() => ColumnFilterChanged?.Invoke(); + public MetadataTablePageModel() + { + ColumnFilters.CollectionChanged += OnColumnFiltersCollectionChanged; + } + + void OnColumnFiltersCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.OldItems is not null) + foreach (ColumnFilter f in e.OldItems) + f.PropertyChanged -= OnColumnFilterTextChanged; + if (e.NewItems is not null) + foreach (ColumnFilter f in e.NewItems) + f.PropertyChanged += OnColumnFilterTextChanged; + } + + void OnColumnFilterTextChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ColumnFilter.Text)) + ColumnFilterChanged?.Invoke(); + } /// /// Raised when the user clicks a hyperlink-styled token cell. The host (the dock diff --git a/ILSpy/Views/MetadataTablePage.axaml.cs b/ILSpy/Views/MetadataTablePage.axaml.cs index f1ed07a4b..195b42f35 100644 --- a/ILSpy/Views/MetadataTablePage.axaml.cs +++ b/ILSpy/Views/MetadataTablePage.axaml.cs @@ -130,13 +130,13 @@ namespace ILSpy.Views if (boundModel != null) { boundModel.PropertyChanged -= OnModelPropertyChanged; - DetachColumnFilterListeners(boundModel); + boundModel.ColumnFilterChanged -= OnAnyColumnFilterChanged; } boundModel = DataContext as MetadataTablePageModel; if (boundModel != null) { boundModel.PropertyChanged += OnModelPropertyChanged; - AttachColumnFilterListeners(boundModel); + boundModel.ColumnFilterChanged += OnAnyColumnFilterChanged; } ApplySchema(); ApplyScrollTarget(); @@ -146,37 +146,12 @@ namespace ILSpy.Views { if (e.PropertyName is nameof(MetadataTablePageModel.Columns) or nameof(MetadataTablePageModel.Items)) - { - if (sender is MetadataTablePageModel m) - { - DetachColumnFilterListeners(m); - AttachColumnFilterListeners(m); - } ApplySchema(); - } else if (e.PropertyName == nameof(MetadataTablePageModel.ScrollToRow)) - { ApplyScrollTarget(); - } - } - - void OnColumnFilterChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(ColumnFilter.Text)) - itemsView?.Refresh(); - } - - void AttachColumnFilterListeners(MetadataTablePageModel model) - { - foreach (var f in model.ColumnFilters) - f.PropertyChanged += OnColumnFilterChanged; } - void DetachColumnFilterListeners(MetadataTablePageModel model) - { - foreach (var f in model.ColumnFilters) - f.PropertyChanged -= OnColumnFilterChanged; - } + void OnAnyColumnFilterChanged() => itemsView?.Refresh(); void ApplySchema() {