diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs index ff31e320a..a79a9c76c 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -975,6 +975,8 @@ public class AssemblyTreeTests grid.Focus(); Dispatcher.UIThread.RunJobs(); + int itemsBefore = (grid.ItemsSource as System.Collections.ICollection)?.Count ?? -1; + // Act — headless keyboard event simulating the user pressing Del while the tree has // focus. global::Avalonia.Headless.HeadlessWindowExtensions.KeyPress( @@ -984,7 +986,11 @@ public class AssemblyTreeTests global::Avalonia.Input.PhysicalKey.Delete, null); - // Assert — the sacrificial assembly is gone from the list; the survivor remains. + // Assert — the sacrificial assembly is gone from the data list AND the grid's bound + // ItemsSource drops by one. Both halves matter: a passing AssemblyList assertion alone + // would mask regressions where the data updates but the grid stays bound to a stale + // snapshot (which is exactly what happened when the BindTree filter materialised the + // children into a List instead of forwarding the live ObservableCollection). await Waiters.WaitForAsync(() => !vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() .Any(a => string.Equals(a.ShortName, sacrificialName, System.StringComparison.Ordinal))); @@ -992,6 +998,10 @@ public class AssemblyTreeTests vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() .Should().Contain(a => a.ShortName == survivorName, "only the selected assembly should be removed"); + + await Waiters.WaitForAsync( + () => (grid.ItemsSource as System.Collections.ICollection)?.Count < itemsBefore, + description: "DataGrid item count should drop after the assembly is unloaded"); } [AvaloniaTest] diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs index 54d71a4a9..3c05268d3 100644 --- a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs +++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs @@ -352,10 +352,10 @@ namespace ILSpy.AssemblyTree } } - // Apply the active LanguageSettings filter (if any). Returns the children unfiltered when - // no settings are available (design-time / unit-host scenarios) so the tree still - // renders. Materialises into a List so each call returns a stable snapshot the grid - // can iterate twice without re-evaluating. + // Apply the active LanguageSettings filter to a child collection materialised on + // expansion. Returns a fresh List so iteration is stable; live updates at deeper levels + // are not preserved (members are realised once per expansion — adds/removes mid-expand + // don't happen in practice). static IEnumerable FilterChildren(IEnumerable children, LanguageSettings? settings) { if (settings == null) @@ -383,7 +383,11 @@ namespace ILSpy.AssemblyTree }; var hierarchicalModel = new HierarchicalModel(options); - hierarchicalModel.SetRoots(FilterChildren(root.Children, settings)); + // Pass the live ObservableCollection at the root so the grid observes + // CollectionChanged when assemblies are unloaded / opened. Filter is not applied + // at this level (AssemblyTreeNode never reports Hidden); deeper levels filter + // on expansion via ChildrenSelector. + hierarchicalModel.SetRoots(root.Children); TreeGrid.HierarchicalModel = hierarchicalModel; }