diff --git a/ICSharpCode.Decompiler/IL/ILAmbience.cs b/ICSharpCode.Decompiler/IL/ILAmbience.cs index 071145d46..c750ff722 100644 --- a/ICSharpCode.Decompiler/IL/ILAmbience.cs +++ b/ICSharpCode.Decompiler/IL/ILAmbience.cs @@ -53,11 +53,23 @@ namespace ICSharpCode.Decompiler.IL void ConvertSymbol(StringWriter writer, ISymbol symbol) { - var metadata = (symbol as IEntity)?.ParentModule!.MetadataFile?.Metadata; - var token = (symbol as IEntity)?.MetadataToken ?? default; + var entity = symbol as IEntity; + var metadata = entity?.ParentModule?.MetadataFile?.Metadata; + var token = entity?.MetadataToken ?? default; var output = new PlainTextOutput(writer); + // When the symbol's owning module has been torn down (stale entity pinned in + // navigation history after the assembly was unloaded/reloaded), the rest of + // the switch can't read its metadata reader. Fall back to the symbol's name + // so callers still get sensible display text — the NRE would otherwise + // surface up through every consumer that formats a stale history entry. + if (metadata == null) + { + writer.Write(symbol?.Name ?? string.Empty); + return; + } + switch (symbol) { case IField f: diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index edc5200dd..b10385128 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -183,12 +183,24 @@ namespace ILSpy.Docking void OnAssemblyListChanged(object? sender, ILSpy.Util.CurrentAssemblyListChangedEventArgs e) { var inner = e.Inner; + + // On Reset (assembly list wholesale-cleared), drop ALL history — every entry is + // stale by definition. Mirrors WPF's assemblyList_CollectionChanged. + if (inner.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset) + { + PruneHistoryAfterAssemblyListChange(removed: null); + return; + } + if (inner.OldItems is not { Count: > 0 } oldItems) return; var removed = new HashSet( oldItems.OfType()); if (removed.Count == 0) return; + + PruneHistoryAfterAssemblyListChange(removed); + if (factory.Documents?.VisibleDockables is not { } visible) return; @@ -212,14 +224,29 @@ namespace ILSpy.Docking 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 - // calls JsonSerializer.Serialize per list element, which doesn't preserve the writer's - // reference-tracking state between calls — so Owner/Factory back-refs aren't deduplicated as - // $ref. Options when revisiting: try Dock.Serializer.Newtonsoft (better cycle handling), - // or build a custom JsonSerializerOptions with MaxDepth raised + replicate Dock's internal - // polymorphic resolver. + // Drops history entries whose tree node lives inside a now-removed assembly. With + // == null, drops everything (used for the Reset action). + // Without this, Back/Forward would surface entries whose Node reference has been + // detached from the live tree, and formatting their DisplayText routes through + // language-specific formatters that NRE on a stale IEntity.ParentModule. + void PruneHistoryAfterAssemblyListChange(HashSet? removed) + { + if (removed == null) + { + history.RemoveAll(_ => true); + } + else + { + history.RemoveAll(entry => + entry is Navigation.TreeNodeEntry t + && t.Node.AncestorsAndSelf() + .OfType() + .Any(a => removed.Contains(a.LoadedAssembly))); + } + NavigateBackCommand.NotifyCanExecuteChanged(); + NavigateForwardCommand.NotifyCanExecuteChanged(); } void OnDocumentMembershipChanged(object? sender, EventArgs e) => UpdateLastDocumentCanClose();