diff --git a/ICSharpCode.ILSpyX/AssemblyListManager.cs b/ICSharpCode.ILSpyX/AssemblyListManager.cs
index 9e635eec0..e98657b68 100644
--- a/ICSharpCode.ILSpyX/AssemblyListManager.cs
+++ b/ICSharpCode.ILSpyX/AssemblyListManager.cs
@@ -267,12 +267,7 @@ namespace ICSharpCode.ILSpyX
AddToListFromGAC("Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
break;
case object _ when path != null:
- foreach (var file in Directory.GetFiles(path, "*.dll"))
- {
- var dllname = Path.GetFileName(file);
- if (DoIncludeFile(dllname))
- AddToListFromDirectory(file);
- }
+ AddFrameworkAssembliesFromDirectory(list, path);
break;
}
return list;
@@ -284,27 +279,38 @@ namespace ICSharpCode.ILSpyX
if (file != null)
list.OpenAssembly(file);
}
+ }
- void AddToListFromDirectory(string file)
+ ///
+ /// Opens every managed framework assembly in into
+ /// , applying the same filter the preconfigured runtime lists use
+ /// (skips native helpers like *_cor3.dll and lower-cased runtime libraries such as
+ /// coreclr.dll / clrjit.dll). Shared by the "preconfigured runtime list" path and the
+ /// first-run default list, which seeds itself from the shared-framework directory ILSpy
+ /// is running on.
+ ///
+ public void AddFrameworkAssembliesFromDirectory(AssemblyList list, string directory)
+ {
+ foreach (var file in Directory.GetFiles(directory, "*.dll"))
{
- if (File.Exists(file))
+ if (IsIncludedFrameworkFile(Path.GetFileName(file)))
list.OpenAssembly(file);
}
+ }
- bool DoIncludeFile(string fileName)
- {
- if (fileName == "Microsoft.DiaSymReader.Native.amd64.dll")
- return false;
- if (fileName.EndsWith("_cor3.dll", StringComparison.OrdinalIgnoreCase))
- return false;
- if (char.IsUpper(fileName[0]))
- return true;
- if (fileName == "netstandard.dll")
- return true;
- if (fileName == "mscorlib.dll")
- return true;
+ static bool IsIncludedFrameworkFile(string fileName)
+ {
+ if (fileName == "Microsoft.DiaSymReader.Native.amd64.dll")
return false;
- }
+ if (fileName.EndsWith("_cor3.dll", StringComparison.OrdinalIgnoreCase))
+ return false;
+ if (char.IsUpper(fileName[0]))
+ return true;
+ if (fileName == "netstandard.dll")
+ return true;
+ if (fileName == "mscorlib.dll")
+ return true;
+ return false;
}
}
}
diff --git a/ILSpy.Tests/AssemblyList/DefaultAssemblyListTests.cs b/ILSpy.Tests/AssemblyList/DefaultAssemblyListTests.cs
new file mode 100644
index 000000000..4126b0973
--- /dev/null
+++ b/ILSpy.Tests/AssemblyList/DefaultAssemblyListTests.cs
@@ -0,0 +1,69 @@
+// 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;
+using System.IO;
+using System.Linq;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ICSharpCode.ILSpyX;
+
+using ILSpy;
+using ILSpy.AppEnv;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+[TestFixture]
+public class DefaultAssemblyListTests
+{
+ [AvaloniaTest]
+ public void Framework_Directory_Seed_Loads_Managed_Assemblies_And_Skips_Native()
+ {
+ // The first-run default list seeds itself from the shared-framework directory the
+ // running runtime lives in, via AssemblyListManager.AddFrameworkAssembliesFromDirectory.
+ // This exercises that seeding directly (the app wiring just calls it for the running
+ // runtime's directory): managed framework assemblies load; native runtime libraries are
+ // filtered out.
+ var manager = AppComposition.Current.GetExport().AssemblyListManager;
+ var list = manager.CreateList("framework-seed-probe-" + Guid.NewGuid().ToString("N"));
+ var frameworkDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
+
+ manager.AddFrameworkAssembliesFromDirectory(list, frameworkDir);
+
+ var assemblies = list.GetAssemblies();
+ var names = assemblies.Select(a => a.ShortName).ToHashSet();
+
+ names.Should().Contain("System.Private.CoreLib");
+ names.Should().Contain("System.Linq");
+ names.Should().Contain("System.Collections");
+
+ assemblies.Length.Should().BeGreaterThan(20,
+ "the whole shared-framework directory is far larger than a hard-coded handful");
+ assemblies.Should().OnlyContain(a => Path.GetDirectoryName(a.FileName) == frameworkDir,
+ "every seeded assembly comes from the framework directory");
+
+ // Native runtime libraries must not be loaded as assemblies.
+ names.Should().NotContain(n => n.Contains("coreclr"));
+ names.Should().NotContain(n => n.Contains("clrjit"));
+ }
+}
diff --git a/ILSpy.Tests/AssemblyTree/StartupAboutWelcomeTests.cs b/ILSpy.Tests/AssemblyTree/StartupAboutWelcomeTests.cs
new file mode 100644
index 000000000..2dbbd0fa4
--- /dev/null
+++ b/ILSpy.Tests/AssemblyTree/StartupAboutWelcomeTests.cs
@@ -0,0 +1,66 @@
+// 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.Threading.Tasks;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ICSharpCode.ILSpy.Properties;
+
+using ILSpy.AppEnv;
+using ILSpy.Docking;
+using ILSpy.ViewModels;
+using ILSpy.Views;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+[TestFixture]
+public class StartupAboutWelcomeTests
+{
+ [AvaloniaTest]
+ public async Task Startup_With_No_Saved_Selection_Shows_The_About_Page()
+ {
+ // On launch with nothing to restore (no saved tree-view path, no command-line target),
+ // ILSpy should greet the user with the About page in the main tab, rather than an empty
+ // "(no selection)" view. Mirrors the previous version's startup behaviour.
+
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
+
+ var dock = AppComposition.Current.GetExport();
+
+ // The About page is shown asynchronously after the tree signals it is ready.
+ await Waiters.WaitForAsync(
+ () => dock.ActiveDecompilerTab is { } tab && Equals(tab.Title, Resources.About),
+ description: "the empty-startup main tab should host the About welcome page");
+
+ // No tree node ended up selected, and the active page is the About page.
+ ((object?)vm.AssemblyTreeModel.SelectedItem).Should().BeNull(
+ "nothing should be selected when there is no saved selection to restore");
+ var active = dock.ActiveDecompilerTab;
+ active.Should().NotBeNull();
+ ((object?)active!.Title).Should().Be(Resources.About);
+ active.Text.Should().NotBeNullOrWhiteSpace("the About page must have rendered its content");
+ }
+}
diff --git a/ILSpy.Tests/TestApp.axaml.cs b/ILSpy.Tests/TestApp.axaml.cs
index 859bfd8ac..89e1447aa 100644
--- a/ILSpy.Tests/TestApp.axaml.cs
+++ b/ILSpy.Tests/TestApp.axaml.cs
@@ -41,6 +41,10 @@ public class TestApp : ProductionApp
public override void OnFrameworkInitializationCompleted()
{
+ // Tests boot the app repeatedly; seeding the entire shared framework (~150 assemblies)
+ // per test is ~10x slower. Opt into the minimal default-list seed instead.
+ ProductionApp.SeedFullFrameworkDefaultList = false;
+
var sessionsRoot = Path.Combine(Path.GetTempPath(), "ILSpy.Tests");
// Sweep any leftover session dirs from previous runs first — ProcessExit-time cleanup
// is best-effort and the active session's settings file is sometimes still locked when
diff --git a/ILSpy/App.axaml.cs b/ILSpy/App.axaml.cs
index 64edee8e9..0ae451cff 100644
--- a/ILSpy/App.axaml.cs
+++ b/ILSpy/App.axaml.cs
@@ -38,6 +38,12 @@ namespace ILSpy
public static CommandLineArguments? CommandLineArguments { get; private set; }
public static CompositionHost? Composition { get; private set; }
+ // On first run the default assembly list is seeded with the entire shared-framework
+ // directory the running runtime lives in (~150 assemblies). Headless tests flip this
+ // off so each test boots with a minimal default instead of re-seeding the whole
+ // framework per test (which is ~10x slower); see TestApp.
+ public static bool SeedFullFrameworkDefaultList { get; set; } = true;
+
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
index 853735d98..5008c54e7 100644
--- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -56,6 +56,9 @@ namespace ILSpy.AssemblyTree
readonly SettingsService settingsService;
readonly LanguageService languageService;
AssemblyListManager? listManager;
+ // True until the first assembly-list activation completes — gates the startup-only
+ // About-page greeting so switching lists later never re-triggers it.
+ bool initialActivation = true;
AssemblyListTreeNode? assemblyListTreeNode;
[ObservableProperty]
@@ -298,6 +301,11 @@ namespace ILSpy.AssemblyTree
settingsService.SessionSettings.ActiveAssemblyList = value;
ShowAssemblyList(value);
+ // First activation is the startup one; a later switch to a different list isn't.
+ // Only startup gets the empty-selection About-page greeting.
+ bool isInitial = initialActivation;
+ initialActivation = false;
+
// Restore the previously-selected tree node off the UI critical path. The walk
// crosses an AssemblyTreeNode whose EnsureLazyChildren synchronously blocks on
// GetLoadResultAsync — by going async here and awaiting the load, we let the
@@ -311,36 +319,37 @@ namespace ILSpy.AssemblyTree
if (App.CommandLineArguments?.NavigateTo is { Length: > 0 })
return;
var savedPath = settingsService.SessionSettings.ActiveTreeViewPath;
- if (savedPath is { Length: > 0 })
- _ = RestoreSelectedPathAsync(savedPath);
+ _ = RestoreSelectedPathAsync(savedPath, isInitial);
}
- async Task RestoreSelectedPathAsync(string[] path)
+ async Task RestoreSelectedPathAsync(string[]? path, bool isInitial)
{
- using var _ = AppEnv.AppLog.Phase($"RestoreSelectedPathAsync ({path.Length} segments)");
+ using var _ = AppEnv.AppLog.Phase($"RestoreSelectedPathAsync ({path?.Length ?? 0} segments, initial={isInitial})");
try
{
- if (Root == null)
- return;
// Snapshot — if the user selects something else before the restore completes,
// don't yank their selection out from under them.
var initialSelection = SelectedItem;
- SharpTreeNode? node = Root;
- foreach (var element in path)
+ SharpTreeNode? node = null;
+ if (path is { Length: > 0 } && Root != null)
{
- if (node == null)
- break;
- // Awaiting GetLoadResultAsync keeps EnsureLazyChildren — which itself does
- // GetAwaiter().GetResult() on the same task — from blocking the UI thread.
- // Once the load completes the .GetAwaiter().GetResult() returns instantly.
- if (node is AssemblyTreeNode asm)
+ node = Root;
+ foreach (var element in path)
{
- using (AppEnv.AppLog.Phase($"await GetLoadResultAsync ({asm.LoadedAssembly.ShortName})"))
- await asm.LoadedAssembly.GetLoadResultAsync().ConfigureAwait(true);
+ if (node == null)
+ break;
+ // Awaiting GetLoadResultAsync keeps EnsureLazyChildren — which itself does
+ // GetAwaiter().GetResult() on the same task — from blocking the UI thread.
+ // Once the load completes the .GetAwaiter().GetResult() returns instantly.
+ if (node is AssemblyTreeNode asm)
+ {
+ using (AppEnv.AppLog.Phase($"await GetLoadResultAsync ({asm.LoadedAssembly.ShortName})"))
+ await asm.LoadedAssembly.GetLoadResultAsync().ConfigureAwait(true);
+ }
+ using (AppEnv.AppLog.Phase($"EnsureLazyChildren ({node.GetType().Name} \"{element}\")"))
+ node.EnsureLazyChildren();
+ node = node.Children.FirstOrDefault(c => c.ToString() == element);
}
- using (AppEnv.AppLog.Phase($"EnsureLazyChildren ({node.GetType().Name} \"{element}\")"))
- node.EnsureLazyChildren();
- node = node.Children.FirstOrDefault(c => c.ToString() == element);
}
// Wait for the tree view to be Loaded before assigning SelectedItem. Without
// this, the SelectedItem assignment runs ShowSelectedNode → CurrentNodes →
@@ -353,8 +362,19 @@ namespace ILSpy.AssemblyTree
await Task.WhenAny(TreeReady, Task.Delay(TimeSpan.FromSeconds(5)))
.ConfigureAwait(true);
}
- if (node != null && node != Root && ReferenceEquals(SelectedItem, initialSelection))
+ // Bail if the user selected something else while we were resolving.
+ if (!ReferenceEquals(SelectedItem, initialSelection))
+ return;
+ if (node != null && node != Root)
+ {
SelectedItem = node;
+ }
+ else if (isInitial && SelectedItem == null)
+ {
+ // Launched with nothing to restore (no saved path, or it no longer resolves):
+ // greet the user with the About page in the main tab instead of a blank view.
+ ShowAboutWelcomePage();
+ }
}
catch (Exception ex)
{
@@ -362,6 +382,19 @@ namespace ILSpy.AssemblyTree
}
}
+ // Open the About page as the startup welcome screen. The command is resolved lazily
+ // through the menu registry (it's an ExportFactory, not instantiated until needed),
+ // and its instance IS the AboutCommand, so we can drive its welcome path directly.
+ void ShowAboutWelcomePage()
+ {
+ var registry = TryGetExport();
+ var export = registry?.Commands
+ .FirstOrDefault(c => c.Metadata.Header == nameof(ICSharpCode.ILSpy.Properties.Resources._About))
+ ?.CreateExport();
+ if (export?.Value is Commands.AboutCommand about)
+ about.ShowWelcome();
+ }
+
void ShowAssemblyList(string name)
{
if (listManager == null)
@@ -388,7 +421,7 @@ namespace ILSpy.AssemblyTree
if (list.GetAssemblies().Length == 0 && list.ListName == AssemblyListManager.DefaultListName)
{
using (AppEnv.AppLog.Phase("LoadInitialAssemblies"))
- LoadInitialAssemblies(list);
+ LoadInitialAssemblies(list, listManager);
}
AppEnv.AppLog.Mark($"AssemblyList contains {list.GetAssemblies().Length} assemblies");
using (AppEnv.AppLog.Phase("new AssemblyListTreeNode"))
@@ -602,18 +635,38 @@ namespace ILSpy.AssemblyTree
.FirstOrDefault(n => n.MethodDefinition.MetadataToken == method.MetadataToken);
}
- static void LoadInitialAssemblies(AssemblyList assemblyList)
+ static void LoadInitialAssemblies(AssemblyList assemblyList, AssemblyListManager? manager)
{
- System.Reflection.Assembly[] initialAssemblies = {
- typeof(object).Assembly,
- typeof(Uri).Assembly,
- typeof(System.Linq.Enumerable).Assembly,
- };
- foreach (var asm in initialAssemblies)
+ // Headless tests opt out of the full-framework seed (it would re-open ~150 assemblies
+ // per test); they fall back to the minimal trio the previous version shipped so their
+ // expectations and runtime stay unchanged.
+ if (!App.SeedFullFrameworkDefaultList || manager == null)
{
- if (!string.IsNullOrEmpty(asm.Location))
- assemblyList.OpenAssembly(asm.Location);
+ System.Reflection.Assembly[] minimal = {
+ typeof(object).Assembly,
+ typeof(Uri).Assembly,
+ typeof(System.Linq.Enumerable).Assembly,
+ };
+ foreach (var asm in minimal)
+ {
+ if (!string.IsNullOrEmpty(asm.Location))
+ assemblyList.OpenAssembly(asm.Location);
+ }
+ return;
}
+
+ // First-run default: seed the .NET framework assemblies ILSpy itself is running on -
+ // every managed assembly in the shared-framework directory that hosts the running
+ // runtime (the folder containing System.Private.CoreLib). The manager applies the
+ // same managed-PE filter the preconfigured runtime lists use, so native runtime
+ // libraries (coreclr, clrjit, *_cor3.dll, ...) are skipped.
+ var coreLibLocation = typeof(object).Assembly.Location;
+ if (string.IsNullOrEmpty(coreLibLocation))
+ return;
+ var frameworkDirectory = System.IO.Path.GetDirectoryName(coreLibLocation);
+ if (string.IsNullOrEmpty(frameworkDirectory))
+ return;
+ manager.AddFrameworkAssembliesFromDirectory(assemblyList, frameworkDirectory);
}
public void SelectNode(SharpTreeNode? node)
diff --git a/ILSpy/Commands/AboutCommand.cs b/ILSpy/Commands/AboutCommand.cs
index b95c21d8f..ab66ca215 100644
--- a/ILSpy/Commands/AboutCommand.cs
+++ b/ILSpy/Commands/AboutCommand.cs
@@ -57,6 +57,27 @@ 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"));
+ }
+
+ ///
+ /// Renders the About page into the reusable main tab as the startup welcome screen.
+ /// The content is non-static so the first tree-node selection cleanly reuses the tab
+ /// and replaces it. Invoked by when ILSpy
+ /// launches with no node selected and nothing to restore.
+ ///
+ public void ShowWelcome()
+ {
+ var output = BuildAboutOutput();
+ var content = CreateTabContent(Resources.About, output, ".txt", isStaticContent: false);
+ dockWorkspace.ShowWelcomePage(content);
+ }
+
+ AvaloniaEditTextOutput BuildAboutOutput()
{
var output = new AvaloniaEditTextOutput { Title = Resources.About };
output.WriteLine(Resources.ILSpyVersion + DecompilerVersionInfo.FullVersionWithCommitHash);
@@ -81,11 +102,10 @@ namespace ILSpy.Commands
foreach (var (phrase, uri) in Links)
output.AddVisualLineElementGenerator(new ResourceLinkGenerator(phrase, uri));
- var (tab, _) = OpenInNewTab(Resources.About, output, ".txt");
- dockWorkspace.RecordStaticPage(tab, new Uri("resource:aboutpage"));
+ return output;
}
- (ContentTabPage Tab, DecompilerTabPageModel Content) OpenInNewTab(string title, AvaloniaEditTextOutput output, string syntaxExtension)
+ DecompilerTabPageModel CreateTabContent(string title, AvaloniaEditTextOutput output, string syntaxExtension, bool isStaticContent)
{
var content = new DecompilerTabPageModel {
Title = title,
@@ -96,13 +116,13 @@ namespace ILSpy.Commands
DefinitionLookup = output.DefinitionLookup,
UIElements = output.UIElements,
Foldings = output.Foldings,
- IsStaticContent = true,
+ IsStaticContent = isStaticContent,
CustomElementGenerators = output.ElementGenerators.Count > 0
? new List(output.ElementGenerators)
: null,
};
content.OpenUriRequested += OnOpenUri;
- return (dockWorkspace.OpenNewTab(content), content);
+ return content;
}
bool OnOpenUri(Uri uri)
@@ -122,7 +142,8 @@ namespace ILSpy.Commands
using var reader = new StreamReader(stream);
var output = new AvaloniaEditTextOutput { Title = resourceName };
output.Write(reader.ReadToEnd());
- var (tab, _) = OpenInNewTab(resourceName, output, ".txt");
+ var content = CreateTabContent(resourceName, output, ".txt", isStaticContent: true);
+ var tab = dockWorkspace.OpenNewTab(content);
dockWorkspace.RecordStaticPage(tab, new Uri("resource:" + resourceName));
}
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index 9a18d5420..e7e971f19 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -1091,6 +1091,32 @@ namespace ILSpy.Docking
return tab;
}
+ ///
+ /// Renders in the reusable MainTab as a transient welcome
+ /// page (the About page shown at startup when nothing is selected). Unlike Help >
+ /// About this does not pin a static page: the content is non-static, so the first
+ /// tree-node selection reuses the MainTab and replaces it, leaving the user with a
+ /// single tab rather than a stray empty one.
+ ///
+ ///
+ /// The greeting is fired asynchronously at startup (after the tree signals ready), so by
+ /// the time it runs the user may already have opened a document tab that doesn't move the
+ /// tree selection (e.g. Compare...). Only greet when the MainTab is still empty AND still
+ /// the active document; otherwise stealing activation would yank the user off whatever
+ /// they just opened.
+ ///
+ public void ShowWelcomePage(TextView.DecompilerTabPageModel content)
+ {
+ if (factory.MainTab is not { } main)
+ return;
+ if (main.Content != null)
+ return;
+ if (factory.Documents is { } docs && !ReferenceEquals(docs.ActiveDockable, main))
+ return;
+ main.Content = content;
+ ActivateMainTabIfNeeded(main);
+ }
+
///
/// VS-style "pin tab" gesture. The current factory.MainTab keeps its position,
/// content, and active state but flips to pinned