Browse Source

Port the three remaining assembly-list-change subscribers

1) post-refresh-redecompile: AssemblyTreeModel.RefreshInternalAsync now calls
   DockWorkspace.ForceRefreshActiveTab() after SelectNode. F5 on the same
   assembly list doesn't rebuild the tree → FindNodeByPath returns the same
   tree-node reference → SelectedItem setter early-outs → ShowSelectedNode's
   lastShownNodes dedup short-circuits, leaving stale decompiled text. The
   force path resets lastShownNodes and re-runs the decompile pipeline.
   Mirrors WPF's RefreshDecompiledView() call.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
71e2be6147
  1. 23
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  2. 70
      ILSpy/Docking/DockWorkspace.cs
  3. 16
      ILSpy/NavigationHistory.cs

23
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -745,6 +745,22 @@ namespace ILSpy.AssemblyTree @@ -745,6 +745,22 @@ namespace ILSpy.AssemblyTree
/// </summary>
void OnActiveAssemblyListCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// Prune navigation-history entries that pointed at tree nodes inside removed
// assemblies BEFORE re-publishing — Back/Forward consumers (the toolbar
// commands + dropdowns) re-evaluate their CanExecute when the bus fires, so
// they must see the post-prune state. Mirrors WPF's history.RemoveAll(...)
// inside assemblyList_CollectionChanged.
if (e.OldItems is { Count: > 0 } oldItems)
{
var removed = new HashSet<LoadedAssembly>(oldItems.OfType<LoadedAssembly>());
if (removed.Count > 0)
{
TryGetExport<Docking.DockWorkspace>()?.PruneHistory(node =>
node.AncestorsAndSelf()
.OfType<AssemblyTreeNode>()
.Any(a => removed.Contains(a.LoadedAssembly)));
}
}
Util.MessageBus.Send(this, new Util.CurrentAssemblyListChangedEventArgs(e));
}
@ -799,6 +815,13 @@ namespace ILSpy.AssemblyTree @@ -799,6 +815,13 @@ namespace ILSpy.AssemblyTree
}
SelectNode(FindNodeByPath(path, returnBestMatch: true));
// Defensive re-decompile: F5 on the same assembly list doesn't rebuild the
// tree, so FindNodeByPath returns the same tree-node reference, the
// SelectedItem setter early-outs, and DockWorkspace.ShowSelectedNode's
// dedup short-circuits — leaving stale decompiled text. Force a fresh
// render. Mirrors WPF's RefreshDecompiledView() call.
TryGetExport<Docking.DockWorkspace>()?.ForceRefreshActiveTab();
}
}
}

70
ILSpy/Docking/DockWorkspace.cs

@ -84,6 +84,21 @@ namespace ILSpy.Docking @@ -84,6 +84,21 @@ namespace ILSpy.Docking
public IReadOnlyList<NavigationEntry> BackHistory => history.BackEntries;
public IReadOnlyList<NavigationEntry> ForwardHistory => history.ForwardEntries;
/// <summary>
/// Prunes navigation-history entries whose tree-node ancestor matches
/// <paramref name="predicateOnNode"/>. Called from the assembly-tree model
/// when the active assembly list emits a CollectionChanged with removals:
/// every history entry pointing at a node that lived in one of the removed
/// assemblies is dropped so Back/Forward can't surface a detached node.
/// Static-page entries are left alone (they don't reference removed assemblies).
/// </summary>
public void PruneHistory(Predicate<SharpTreeNode> predicateOnNode)
{
ArgumentNullException.ThrowIfNull(predicateOnNode);
history.RemoveAll(entry =>
entry is TreeNodeEntry t && predicateOnNode(t.Node));
}
public IRootDock Layout { get; }
public IReadOnlyList<ToolPaneMenuItem> ToolPaneMenuItems { get; }
@ -126,7 +141,46 @@ namespace ILSpy.Docking @@ -126,7 +141,46 @@ namespace ILSpy.Docking
factory.DockableClosed += OnDocumentMembershipChanged;
factory.DockableClosing += OnDockableClosing;
factory.ActiveDockableChanged += OnActiveDockableChanged;
// Close orphaned carve-out tabs when their assembly is removed. The persistent
// MainTab slot is left alone — its content will swap to whatever the user selects
// next via the assembly tree. Mirrors WPF's DockWorkspace.CurrentAssemblyList_Changed.
ILSpy.Util.MessageBus<ILSpy.Util.CurrentAssemblyListChangedEventArgs>.Subscribers
+= OnAssemblyListChanged;
ILSpy.AppEnv.StartupLog.Mark("DockWorkspace ctor exited");
}
void OnAssemblyListChanged(object? sender, ILSpy.Util.CurrentAssemblyListChangedEventArgs e)
{
var inner = e.Inner;
if (inner.OldItems is not { Count: > 0 } oldItems)
return;
var removed = new HashSet<ICSharpCode.ILSpyX.LoadedAssembly>(
oldItems.OfType<ICSharpCode.ILSpyX.LoadedAssembly>());
if (removed.Count == 0)
return;
if (factory.Documents?.VisibleDockables is not { } visible)
return;
foreach (var dockable in visible.OfType<ViewModels.ContentTabPage>().ToArray())
{
// Skip the persistent MainTab — its content swaps with tree selection. Also
// skip static content (About page etc.) which doesn't reference assemblies.
if (ReferenceEquals(dockable, factory.MainTab))
continue;
if (dockable.Content is not TextView.DecompilerTabPageModel tab || tab.IsStaticContent)
continue;
var nodes = tab.CurrentNodes;
if (nodes.Count == 0)
continue;
// "Some node in the tab still lives in a non-removed assembly" → keep.
// "Every node points at a removed assembly" → close.
bool anyAlive = nodes.Any(n =>
n.AncestorsAndSelf()
.OfType<TreeNodes.AssemblyTreeNode>()
.LastOrDefault() is { } owner && !removed.Contains(owner.LoadedAssembly));
if (!anyAlive)
factory.CloseDockable(dockable);
}
// TODO: layout persistence (load on startup, save on exit). DockSerializer.SystemTextJson
// trips System.Text.Json's MaxDepth=64 limit on our layout because Dock's JsonConverterList<T>
@ -384,6 +438,22 @@ namespace ILSpy.Docking @@ -384,6 +438,22 @@ namespace ILSpy.Docking
ILSpyTreeNode[]? lastShownNodes;
/// <summary>
/// Force a re-decompile of the active tree selection even if it equals the last
/// rendered selection. Called by <see cref="AssemblyTree.AssemblyTreeModel"/> at
/// the end of <c>RefreshInternalAsync</c>: when F5 reloads the same assembly list,
/// the tree isn't rebuilt and the SelectedItem reference is preserved, so the
/// normal selection-change cascade would no-op and the editor would keep stale
/// decompiled text. Resetting <c>lastShownNodes</c> defeats the
/// dedup short-circuit inside <see cref="ShowSelectedNode"/>. Mirrors WPF's
/// <c>RefreshDecompiledView()</c> call.
/// </summary>
public void ForceRefreshActiveTab()
{
lastShownNodes = null;
ShowSelectedNode();
}
void ShowSelectedNode()
{
var nodes = assemblyTreeModel.SelectedItems.OfType<ILSpyTreeNode>().ToArray();

16
ILSpy/NavigationHistory.cs

@ -98,6 +98,22 @@ namespace ILSpy.Navigation @@ -98,6 +98,22 @@ namespace ILSpy.Navigation
current = null;
}
/// <summary>
/// Removes every entry matched by <paramref name="predicate"/> from both stacks
/// AND from <see cref="Current"/>. Used by the assembly-list-changed handler to
/// drop history entries pointing at tree nodes whose containing assembly was
/// just unloaded — without this, Back/Forward could surface a stale entry whose
/// Node reference is now detached from the live tree.
/// </summary>
public void RemoveAll(Predicate<T> predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
back.RemoveAll(predicate);
forward.RemoveAll(predicate);
if (current != null && predicate(current))
current = null;
}
/// <summary>Records a new history entry. Discards the forward stack.</summary>
public void Record(T entry)
{

Loading…
Cancel
Save