diff --git a/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs b/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs
new file mode 100644
index 000000000..c28ec06c4
--- /dev/null
+++ b/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs
@@ -0,0 +1,103 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Linq;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ICSharpCode.ILSpy.Properties;
+
+using ILSpy.AppEnv;
+using ILSpy.Commands;
+using ILSpy.Options;
+using ILSpy.TextView;
+using ILSpy.ViewModels;
+using ILSpy.Views;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Docking;
+
+///
+/// Options and About are static-content singletons: closing and reopening one must reuse the
+/// same instance (and therefore its owned view + content state, e.g.
+/// the selected options page) rather than rebuilding a fresh tab each time. Pins that contract.
+///
+[TestFixture]
+public class SingletonDocumentTabTests
+{
+ static void Invoke(MainWindow window, string header)
+ {
+ var registry = AppComposition.Current.GetExport();
+ registry.Commands.Single(c => c.Metadata.Header == header).CreateExport().Value.Execute(null);
+ Avalonia.Threading.Dispatcher.UIThread.RunJobs();
+ }
+
+ [AvaloniaTest]
+ public void Reopening_Options_Reuses_The_Same_Tab()
+ {
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ var dock = vm.DockWorkspace;
+
+ Invoke(window, nameof(Resources._Options));
+ var first = dock.Documents!.VisibleDockables!.OfType()
+ .Single(t => t.Content is OptionsPageModel);
+ var firstContent = first.Content;
+
+ dock.Factory.CloseDockable(first);
+ Avalonia.Threading.Dispatcher.UIThread.RunJobs();
+ dock.Documents!.VisibleDockables!.OfType()
+ .Any(t => t.Content is OptionsPageModel).Should().BeFalse("closing must remove the Options tab");
+
+ Invoke(window, nameof(Resources._Options));
+ var second = dock.Documents!.VisibleDockables!.OfType()
+ .Single(t => t.Content is OptionsPageModel);
+
+ second.Should().BeSameAs(first,
+ "reopening Options must reuse the retained singleton tab, not build a fresh one");
+ second.Content.Should().BeSameAs(firstContent,
+ "the OptionsPageModel (and its selected page) must survive close/reopen");
+ }
+
+ [AvaloniaTest]
+ public void Reopening_About_Reuses_The_Same_Tab()
+ {
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ var dock = vm.DockWorkspace;
+
+ bool IsAbout(ContentTabPage t) => t.Content is DecompilerTabPageModel { Title: var title } && title == Resources.About;
+
+ Invoke(window, nameof(Resources._About));
+ var first = dock.Documents!.VisibleDockables!.OfType().Single(IsAbout);
+
+ dock.Factory.CloseDockable(first);
+ Avalonia.Threading.Dispatcher.UIThread.RunJobs();
+
+ Invoke(window, nameof(Resources._About));
+ var second = dock.Documents!.VisibleDockables!.OfType().Single(IsAbout);
+
+ second.Should().BeSameAs(first,
+ "reopening About must reuse the retained singleton tab, not rebuild the page");
+ }
+}
diff --git a/ILSpy/Commands/AboutCommand.cs b/ILSpy/Commands/AboutCommand.cs
index ab66ca215..ca42bb896 100644
--- a/ILSpy/Commands/AboutCommand.cs
+++ b/ILSpy/Commands/AboutCommand.cs
@@ -58,10 +58,14 @@ namespace ILSpy.Commands
public override void Execute(object? parameter)
{
- var output = BuildAboutOutput();
- var content = CreateTabContent(Resources.About, output, ".txt", isStaticContent: true);
- var tab = dockWorkspace.OpenNewTab(content);
- dockWorkspace.RecordStaticPage(tab, new Uri("resource:aboutpage"));
+ // Singleton: one retained About tab, reused across close/reopen rather than rebuilt.
+ dockWorkspace.OpenSingletonTab("resource:aboutpage", () => {
+ var output = BuildAboutOutput();
+ var content = CreateTabContent(Resources.About, output, ".txt", isStaticContent: true);
+ var tab = dockWorkspace.OpenNewTab(content);
+ dockWorkspace.RecordStaticPage(tab, new Uri("resource:aboutpage"));
+ return tab;
+ });
}
///
@@ -140,11 +144,16 @@ namespace ILSpy.Commands
if (stream == null)
return;
using var reader = new StreamReader(stream);
- var output = new AvaloniaEditTextOutput { Title = resourceName };
- output.Write(reader.ReadToEnd());
- var content = CreateTabContent(resourceName, output, ".txt", isStaticContent: true);
- var tab = dockWorkspace.OpenNewTab(content);
- dockWorkspace.RecordStaticPage(tab, new Uri("resource:" + resourceName));
+ var text = reader.ReadToEnd();
+ // Singleton per resource: reopening the same embedded page reuses its retained tab.
+ dockWorkspace.OpenSingletonTab("resource:" + resourceName, () => {
+ var output = new AvaloniaEditTextOutput { Title = resourceName };
+ output.Write(text);
+ var content = CreateTabContent(resourceName, output, ".txt", isStaticContent: true);
+ var tab = dockWorkspace.OpenNewTab(content);
+ dockWorkspace.RecordStaticPage(tab, new Uri("resource:" + resourceName));
+ return tab;
+ });
}
static string GetDotnetProductVersion()
diff --git a/ILSpy/Commands/ShowOptionsCommand.cs b/ILSpy/Commands/ShowOptionsCommand.cs
index c7ac7671e..86c4868e1 100644
--- a/ILSpy/Commands/ShowOptionsCommand.cs
+++ b/ILSpy/Commands/ShowOptionsCommand.cs
@@ -18,7 +18,6 @@
using System.Collections.Generic;
using System.Composition;
-using System.Linq;
using ICSharpCode.ILSpy.Properties;
@@ -46,24 +45,10 @@ namespace ILSpy.Commands
{
public override void Execute(object? parameter)
{
- var existing = FindExistingOptionsTab();
- if (existing != null && dockWorkspace.Documents != null)
- {
- dockWorkspace.Factory.SetActiveDockable(existing);
- dockWorkspace.Factory.SetFocusedDockable(dockWorkspace.Documents, existing);
- return;
- }
-
- var model = new OptionsPageModel(settingsService, optionPages);
- dockWorkspace.OpenNewTab(model);
- }
-
- ContentTabPage? FindExistingOptionsTab()
- {
- var docs = dockWorkspace.Documents?.VisibleDockables;
- if (docs == null)
- return null;
- return docs.OfType().FirstOrDefault(t => t.Content is OptionsPageModel);
+ // Singleton: one retained Options tab for the session. Reopening after close reuses
+ // the same ContentTabPage (and its OptionsPageModel, so the selected page survives).
+ dockWorkspace.OpenSingletonTab("options",
+ () => dockWorkspace.OpenNewTab(new OptionsPageModel(settingsService, optionPages)));
}
}
}
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index 31e57cde1..7c3736cce 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -651,6 +651,11 @@ namespace ILSpy.Docking
// Created lazily on first need.
DecompilerTabPageModel? decompilerContent;
+ // Retained static-content singleton tabs (Options, About, embedded resource pages).
+ // Keeping the ContentTabPage instance across close/reopen preserves its owned view and
+ // content state (e.g. the selected options page) instead of rebuilding the tab each time.
+ readonly Dictionary singletonTabs = new();
+
ILSpyTreeNode[]? lastShownNodes;
///
@@ -1091,6 +1096,36 @@ namespace ILSpy.Docking
return tab;
}
+ ///
+ /// Open a retained static-content singleton tab (Options, About, an embedded resource
+ /// page). If the tab already exists -- visible or previously closed -- the same instance
+ /// is reactivated / re-added, so its owned view and content state survive close/reopen.
+ /// Otherwise builds it once and it is retained for the session.
+ ///
+ public ContentTabPage OpenSingletonTab(string key, Func create)
+ {
+ if (singletonTabs.TryGetValue(key, out var existing))
+ {
+ ReopenTab(existing);
+ return existing;
+ }
+ var tab = create();
+ singletonTabs[key] = tab;
+ return tab;
+ }
+
+ // Re-display a retained tab: re-add it to the Documents dock if it was closed, then make
+ // it active. The dockable (and the view it owns) is the same instance throughout.
+ void ReopenTab(ContentTabPage tab)
+ {
+ if (factory.Documents is not { } docs)
+ return;
+ if (docs.VisibleDockables?.Contains(tab) != true)
+ factory.AddDockable(docs, tab);
+ factory.SetActiveDockable(tab);
+ factory.SetFocusedDockable(docs, tab);
+ }
+
///
/// Renders in the reusable MainTab as a transient welcome
/// page (the About page shown at startup when nothing is selected). Unlike Help >