diff --git a/BuildTools/update-assemblyinfo.ps1 b/BuildTools/update-assemblyinfo.ps1
index b07199c82..f4096cb74 100644
--- a/BuildTools/update-assemblyinfo.ps1
+++ b/BuildTools/update-assemblyinfo.ps1
@@ -69,6 +69,17 @@ function gitCommitHash() {
     }
 }
 
+function gitShortCommitHash() {
+    if (No-Git) {
+        return "00000000";
+    }
+    try {
+        return (git rev-parse --short=8 (git rev-list --max-count 1 HEAD));
+    } catch {
+        return "00000000";
+    }	
+}
+
 function gitBranch() {
     if (No-Git) {
         return "no-branch";
@@ -117,6 +128,7 @@ try {
     $revision = gitVersion;
     $branchName = gitBranch;
     $gitCommitHash = gitCommitHash;
+	$gitShortCommitHash = gitShortCommitHash;
 
     if ($branchName -match $masterBranches) {
         $postfixBranchName = "";
@@ -150,7 +162,7 @@ try {
 		$out = $out.Replace('$INSERTMAJORVERSION$', $major);
 		$out = $out.Replace('$INSERTREVISION$', $revision);
 		$out = $out.Replace('$INSERTCOMMITHASH$', $gitCommitHash);
-		$out = $out.Replace('$INSERTSHORTCOMMITHASH$', $gitCommitHash.Substring(0, 8));
+		$out = $out.Replace('$INSERTSHORTCOMMITHASH$', $gitShortCommitHash);
 		$out = $out.Replace('$INSERTDATE$', [System.DateTime]::Now.ToString("MM/dd/yyyy"));
 		$out = $out.Replace('$INSERTYEAR$', [System.DateTime]::Now.Year.ToString());
 		$out = $out.Replace('$INSERTBRANCHNAME$', $branchName);
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/VariableNaming.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/VariableNaming.cs
index 497a36c06..3b5bc08ec 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/VariableNaming.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/VariableNaming.cs
@@ -88,5 +88,80 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 			}
 #endif
 		}
+
+		private static void NestedForLoopTest2()
+		{
+			for (int i = 0; i < 10; i++)
+			{
+				Nop(i);
+			}
+#if EXPECTED_OUTPUT && !(LEGACY_CSC && !OPT)
+			for (int j = 0; j < 10; j++)
+			{
+				Nop(j);
+			}
+
+			for (int k = 0; k < 10; k++)
+			{
+				Nop(k);
+			}
+
+			for (int l = 0; l < 10; l++)
+			{
+				Nop(l);
+			}
+
+			for (int m = 0; m < 10; m++)
+			{
+				for (int n = 0; n < 10; n++)
+				{
+					Nop(n);
+				}
+			}
+
+			for (int num = 0; num < 10; num++)
+			{
+				for (int num2 = 0; num2 < 10; num2++)
+				{
+					Nop(num2);
+				}
+			}
+#else
+			for (int i = 0; i < 10; i++)
+			{
+				Nop(i);
+			}
+
+			for (int i = 0; i < 10; i++)
+			{
+				Nop(i);
+			}
+
+			for (int i = 0; i < 10; i++)
+			{
+				Nop(i);
+			}
+
+			for (int i = 0; i < 10; i++)
+			{
+				for (int j = 0; j < 10; j++)
+				{
+					Nop(j);
+				}
+			}
+
+			for (int i = 0; i < 10; i++)
+			{
+				for (int j = 0; j < 10; j++)
+				{
+					Nop(j);
+				}
+			}
+#endif
+		}
+
+		private static void Nop(int v)
+		{
+		}
 	}
 }
diff --git a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs
index ecc219400..c30339837 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs
@@ -289,7 +289,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			string nameWithoutNumber = SplitName(newName, out int newIndex);
 			if (reservedVariableNames.TryGetValue(nameWithoutNumber, out int lastUsedIndex))
 			{
-				// name without number was already used
 				if (v.Type.IsKnownType(KnownTypeCode.Int32) && loopCounters.Contains(v))
 				{
 					// special case for loop counters,
@@ -298,20 +297,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 					nameWithoutNumber = newName;
 					newIndex = 1;
 				}
+			}
+			if (reservedVariableNames.TryGetValue(nameWithoutNumber, out lastUsedIndex))
+			{
+				// name without number was already used
+				if (newIndex > lastUsedIndex)
+				{
+					// new index is larger than last, so we can use it
+				}
 				else
 				{
-					if (newIndex > lastUsedIndex)
-					{
-						// new index is larger than last, so we can use it
-					}
-					else
-					{
-						// new index is smaller or equal, so we use the next value
-						newIndex = lastUsedIndex + 1;
-					}
-					// resolve conflicts by appending the index to the new name:
-					newName = nameWithoutNumber + newIndex.ToString();
+					// new index is smaller or equal, so we use the next value
+					newIndex = lastUsedIndex + 1;
 				}
+				// resolve conflicts by appending the index to the new name:
+				newName = nameWithoutNumber + newIndex.ToString();
 			}
 			// update the last used index
 			reservedVariableNames[nameWithoutNumber] = newIndex;
diff --git a/ICSharpCode.Decompiler/Properties/AssemblyInfo.cs b/ICSharpCode.Decompiler/Properties/AssemblyInfo.cs
index 3f6d671f7..aeac8ac43 100644
--- a/ICSharpCode.Decompiler/Properties/AssemblyInfo.cs
+++ b/ICSharpCode.Decompiler/Properties/AssemblyInfo.cs
@@ -15,7 +15,7 @@ using System.Runtime.InteropServices;
 [assembly: ComVisible(false)]
 
 [assembly: AssemblyVersion(DecompilerVersionInfo.Major + "." + DecompilerVersionInfo.Minor + "." + DecompilerVersionInfo.Build + "." + DecompilerVersionInfo.Revision)]
-[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithShortCommitHash)]
+[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithCommitHash)]
 
 [assembly: InternalsVisibleTo("ICSharpCode.Decompiler.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004dcf3979c4e902efa4dd2163a039701ed5822e6f1134d77737296abbb97bf0803083cfb2117b4f5446a217782f5c7c634f9fe1fc60b4c11d62c5b3d33545036706296d31903ddcf750875db38a8ac379512f51620bb948c94d0831125fbc5fe63707cbb93f48c1459c4d1749eb7ac5e681a2f0d6d7c60fa527a3c0b8f92b02bf")]
 
diff --git a/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs b/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs
index ee12ad9b8..6d29f56fb 100644
--- a/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs
+++ b/ICSharpCode.Decompiler/Properties/DecompilerVersionInfo.template.cs
@@ -7,5 +7,6 @@
 	public const string VersionName = "preview2";
 
 	public const string FullVersion = Major + "." + Minor + "." + Build + ".$INSERTREVISION$$INSERTBRANCHPOSTFIX$$INSERTVERSIONNAMEPOSTFIX$";
-	public const string FullVersionWithShortCommitHash = FullVersion + "-$INSERTSHORTCOMMITHASH$";
+	public const string FullVersionWithShortCommitHash = FullVersion + "+$INSERTSHORTCOMMITHASH$";
+	public const string FullVersionWithCommitHash = FullVersion + "+$INSERTCOMMITHASH$";
 }
diff --git a/ICSharpCode.ILSpyCmd/Properties/AssemblyInfo.cs b/ICSharpCode.ILSpyCmd/Properties/AssemblyInfo.cs
index f89527a81..0a60d6bf6 100644
--- a/ICSharpCode.ILSpyCmd/Properties/AssemblyInfo.cs
+++ b/ICSharpCode.ILSpyCmd/Properties/AssemblyInfo.cs
@@ -1,12 +1,8 @@
 #region Using directives
 
-using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
-using System.Resources;
-using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
 
 #endregion
 
@@ -15,7 +11,7 @@ using System.Runtime.Versioning;
 [assembly: ComVisible(false)]
 
 [assembly: AssemblyVersion(DecompilerVersionInfo.Major + "." + DecompilerVersionInfo.Minor + "." + DecompilerVersionInfo.Build + "." + DecompilerVersionInfo.Revision)]
-[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithShortCommitHash)]
+[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithCommitHash)]
 
 [assembly: SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly",
 	Justification = "AssemblyInformationalVersion does not need to be a parsable version")]
diff --git a/ICSharpCode.ILSpyX/AssemblyList.cs b/ICSharpCode.ILSpyX/AssemblyList.cs
index b52e79117..190f45e52 100644
--- a/ICSharpCode.ILSpyX/AssemblyList.cs
+++ b/ICSharpCode.ILSpyX/AssemblyList.cs
@@ -186,7 +186,7 @@ namespace ICSharpCode.ILSpyX
 			get { return listName; }
 		}
 
-		internal void Move(LoadedAssembly[] assembliesToMove, int index)
+		public void Move(LoadedAssembly[] assembliesToMove, int index)
 		{
 			VerifyAccess();
 			lock (lockObj)
@@ -230,7 +230,7 @@ namespace ICSharpCode.ILSpyX
 			}
 		}
 
-		internal void RefreshSave()
+		public void RefreshSave()
 		{
 			// Whenever the assembly list is modified, mark it as dirty
 			// and enqueue a task that saves it once the UI has finished modifying the assembly list.
@@ -281,8 +281,9 @@ namespace ICSharpCode.ILSpyX
 		{
 			file = Path.GetFullPath(file);
 			return OpenAssembly(file, () => {
-				var newAsm = new LoadedAssembly(this, file, fileLoaders: manager?.LoaderRegistry, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols);
-				newAsm.IsAutoLoaded = isAutoLoaded;
+				var newAsm = new LoadedAssembly(this, file, fileLoaders: manager?.LoaderRegistry, applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols) {
+					IsAutoLoaded = isAutoLoaded
+				};
 				return newAsm;
 			});
 		}
diff --git a/ICSharpCode.ILSpyX/AssemblyListManager.cs b/ICSharpCode.ILSpyX/AssemblyListManager.cs
index 8a886df5f..35e1021ce 100644
--- a/ICSharpCode.ILSpyX/AssemblyListManager.cs
+++ b/ICSharpCode.ILSpyX/AssemblyListManager.cs
@@ -39,7 +39,7 @@ namespace ICSharpCode.ILSpyX
 		public const string DotNet35List = ".NET 3.5";
 		public const string ASPDotNetMVC3List = "ASP.NET (MVC3)";
 
-		private ISettingsProvider settingsProvider;
+		private readonly ISettingsProvider settingsProvider;
 
 		public AssemblyListManager(ISettingsProvider settingsProvider)
 		{
@@ -59,9 +59,9 @@ namespace ICSharpCode.ILSpyX
 
 		public bool UseDebugSymbols { get; set; }
 
-		public ObservableCollection<string> AssemblyLists { get; } = new ObservableCollection<string>();
+		public ObservableCollection<string> AssemblyLists { get; } = [];
 
-		public FileLoaderRegistry LoaderRegistry { get; } = new FileLoaderRegistry();
+		public FileLoaderRegistry LoaderRegistry { get; } = new();
 
 		/// <summary>
 		/// Loads an assembly list from the ILSpySettings.
@@ -69,14 +69,13 @@ namespace ICSharpCode.ILSpyX
 		/// </summary>
 		public AssemblyList LoadList(string listName)
 		{
-			this.settingsProvider = this.settingsProvider.Load();
 			AssemblyList list = DoLoadList(listName);
 			if (!AssemblyLists.Contains(list.ListName))
 				AssemblyLists.Add(list.ListName);
 			return list;
 		}
 
-		AssemblyList DoLoadList(string listName)
+		AssemblyList DoLoadList(string? listName)
 		{
 			XElement doc = this.settingsProvider["AssemblyLists"];
 			if (listName != null)
diff --git a/ICSharpCode.ILSpyX/Extensions/CollectionExtensions.cs b/ICSharpCode.ILSpyX/Extensions/CollectionExtensions.cs
index 772c46476..1d4f359e1 100644
--- a/ICSharpCode.ILSpyX/Extensions/CollectionExtensions.cs
+++ b/ICSharpCode.ILSpyX/Extensions/CollectionExtensions.cs
@@ -27,7 +27,7 @@ namespace ICSharpCode.ILSpyX.Extensions
 {
 	public static class CollectionExtensions
 	{
-		public static void AddRange<T>(this ICollection<T> list, IEnumerable<T> items)
+		internal static void AddRange<T>(this ICollection<T> list, IEnumerable<T> items)
 		{
 			foreach (T item in items)
 				if (!list.Contains(item))
diff --git a/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj b/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj
index 41e03236d..e0614d694 100644
--- a/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj
+++ b/ICSharpCode.ILSpyX/ICSharpCode.ILSpyX.csproj
@@ -40,7 +40,6 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <InternalsVisibleTo Include="ILSpy" Key="00240000048000009400000006020000002400005253413100040000010001004dcf3979c4e902efa4dd2163a039701ed5822e6f1134d77737296abbb97bf0803083cfb2117b4f5446a217782f5c7c634f9fe1fc60b4c11d62c5b3d33545036706296d31903ddcf750875db38a8ac379512f51620bb948c94d0831125fbc5fe63707cbb93f48c1459c4d1749eb7ac5e681a2f0d6d7c60fa527a3c0b8f92b02bf" />
     <InternalsVisibleTo Include="ILSpy.Tests" Key="00240000048000009400000006020000002400005253413100040000010001004dcf3979c4e902efa4dd2163a039701ed5822e6f1134d77737296abbb97bf0803083cfb2117b4f5446a217782f5c7c634f9fe1fc60b4c11d62c5b3d33545036706296d31903ddcf750875db38a8ac379512f51620bb948c94d0831125fbc5fe63707cbb93f48c1459c4d1749eb7ac5e681a2f0d6d7c60fa527a3c0b8f92b02bf" />
   </ItemGroup>
 
diff --git a/ICSharpCode.ILSpyX/LoadedPackage.cs b/ICSharpCode.ILSpyX/LoadedPackage.cs
index 47e80b962..78f80b349 100644
--- a/ICSharpCode.ILSpyX/LoadedPackage.cs
+++ b/ICSharpCode.ILSpyX/LoadedPackage.cs
@@ -51,14 +51,14 @@ namespace ICSharpCode.ILSpyX
 
 		public PackageKind Kind { get; }
 
-		internal SingleFileBundle.Header BundleHeader { get; set; }
+		public SingleFileBundle.Header BundleHeader { get; set; }
 
 		/// <summary>
 		/// List of all entries, including those in sub-directories within the package.
 		/// </summary>
 		public IReadOnlyList<PackageEntry> Entries { get; }
 
-		internal PackageFolder RootFolder { get; }
+		public PackageFolder RootFolder { get; }
 
 		public LoadedPackage(PackageKind kind, IEnumerable<PackageEntry> entries)
 		{
@@ -256,7 +256,7 @@ namespace ICSharpCode.ILSpyX
 		public abstract string FullName { get; }
 	}
 
-	sealed class PackageFolder : IAssemblyResolver
+	public sealed class PackageFolder : IAssemblyResolver
 	{
 		/// <summary>
 		/// Gets the short name of the folder.
@@ -326,7 +326,7 @@ namespace ICSharpCode.ILSpyX
 
 		readonly Dictionary<string, LoadedAssembly?> assemblies = new Dictionary<string, LoadedAssembly?>(StringComparer.OrdinalIgnoreCase);
 
-		internal LoadedAssembly? ResolveFileName(string name)
+		public LoadedAssembly? ResolveFileName(string name)
 		{
 			if (package.LoadedAssembly == null)
 				return null;
diff --git a/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs b/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs
index ec4cb8faf..a5432f47c 100644
--- a/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs
+++ b/ICSharpCode.ILSpyX/PdbProvider/PortableDebugInfoProvider.cs
@@ -32,7 +32,7 @@ using static ICSharpCode.Decompiler.Metadata.MetadataFile;
 
 namespace ICSharpCode.ILSpyX.PdbProvider
 {
-	class PortableDebugInfoProvider : IDebugInfoProvider
+	public class PortableDebugInfoProvider : IDebugInfoProvider
 	{
 		string? pdbFileName;
 		string moduleFileName;
@@ -40,7 +40,7 @@ namespace ICSharpCode.ILSpyX.PdbProvider
 		MetadataReaderOptions options;
 		bool hasError;
 
-		internal bool IsEmbedded => pdbFileName == null;
+		public bool IsEmbedded => pdbFileName == null;
 
 		public PortableDebugInfoProvider(string moduleFileName, MetadataReaderProvider provider,
 			MetadataReaderOptions options = MetadataReaderOptions.Default,
@@ -69,7 +69,7 @@ namespace ICSharpCode.ILSpyX.PdbProvider
 			}
 		}
 
-		internal MetadataReader? GetMetadataReader()
+		public MetadataReader? GetMetadataReader()
 		{
 			try
 			{
diff --git a/ICSharpCode.ILSpyX/Properties/AssemblyInfo.cs b/ICSharpCode.ILSpyX/Properties/AssemblyInfo.cs
index 34d6f6fa1..f4c52a6e3 100644
--- a/ICSharpCode.ILSpyX/Properties/AssemblyInfo.cs
+++ b/ICSharpCode.ILSpyX/Properties/AssemblyInfo.cs
@@ -1,12 +1,8 @@
 #region Using directives
 
-using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
-using System.Resources;
-using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
-using System.Runtime.Versioning;
 
 #endregion
 
@@ -15,7 +11,7 @@ using System.Runtime.Versioning;
 [assembly: ComVisible(false)]
 
 [assembly: AssemblyVersion(DecompilerVersionInfo.Major + "." + DecompilerVersionInfo.Minor + "." + DecompilerVersionInfo.Build + "." + DecompilerVersionInfo.Revision)]
-[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithShortCommitHash)]
+[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithCommitHash)]
 
 [assembly: SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly",
 	Justification = "AssemblyInformationalVersion does not need to be a parsable version")]
diff --git a/ICSharpCode.ILSpyX/Settings/ILSpySettings.cs b/ICSharpCode.ILSpyX/Settings/ILSpySettings.cs
index 63e94299b..d19998587 100644
--- a/ICSharpCode.ILSpyX/Settings/ILSpySettings.cs
+++ b/ICSharpCode.ILSpyX/Settings/ILSpySettings.cs
@@ -34,16 +34,11 @@ namespace ICSharpCode.ILSpyX.Settings
 		/// </summary>
 		public static ISettingsFilePathProvider? SettingsFilePathProvider { get; set; }
 
-		readonly XElement root;
+		XElement root;
 
-		ILSpySettings()
+		ILSpySettings(XElement? root = null)
 		{
-			this.root = new XElement("ILSpy");
-		}
-
-		ILSpySettings(XElement root)
-		{
-			this.root = root;
+			this.root = root ?? new XElement("ILSpy");
 		}
 
 		public XElement this[XName section] {
@@ -64,13 +59,7 @@ namespace ICSharpCode.ILSpyX.Settings
 			{
 				try
 				{
-					XDocument doc = LoadWithoutCheckingCharacters(GetConfigFile());
-					if (null == doc.Root)
-					{
-						return new ILSpySettings();
-					}
-
-					return new ILSpySettings(doc.Root);
+					return new ILSpySettings(LoadFile(GetConfigFile()).Root);
 				}
 				catch (IOException)
 				{
@@ -83,7 +72,7 @@ namespace ICSharpCode.ILSpyX.Settings
 			}
 		}
 
-		static XDocument LoadWithoutCheckingCharacters(string fileName)
+		static XDocument LoadFile(string fileName)
 		{
 			return XDocument.Load(fileName, LoadOptions.None);
 		}
@@ -91,16 +80,15 @@ namespace ICSharpCode.ILSpyX.Settings
 		/// <summary>
 		/// Saves a setting section.
 		/// </summary>
-		public static void SaveSettings(XElement section)
+		public void SaveSettings(XElement section)
 		{
-			Update(
-				delegate (XElement root) {
-					XElement? existingElement = root.Element(section.Name);
-					if (existingElement != null)
-						existingElement.ReplaceWith(section);
-					else
-						root.Add(section);
-				});
+			Update(rootElement => {
+				XElement? existingElement = rootElement.Element(section.Name);
+				if (existingElement != null)
+					existingElement.ReplaceWith(section);
+				else
+					rootElement.Add(section);
+			});
 		}
 
 		/// <summary>
@@ -108,7 +96,7 @@ namespace ICSharpCode.ILSpyX.Settings
 		/// We always reload the file on updates to ensure we aren't overwriting unrelated changes performed
 		/// by another ILSpy instance.
 		/// </summary>
-		public static void Update(Action<XElement> action)
+		public void Update(Action<XElement> action)
 		{
 			using (new MutexProtector(ConfigFileMutex))
 			{
@@ -116,7 +104,7 @@ namespace ICSharpCode.ILSpyX.Settings
 				XDocument doc;
 				try
 				{
-					doc = LoadWithoutCheckingCharacters(config);
+					doc = LoadFile(config);
 				}
 				catch (IOException)
 				{
@@ -131,14 +119,10 @@ namespace ICSharpCode.ILSpyX.Settings
 				doc.Root!.SetAttributeValue("version", DecompilerVersionInfo.Major + "." + DecompilerVersionInfo.Minor + "." + DecompilerVersionInfo.Build + "." + DecompilerVersionInfo.Revision);
 				action(doc.Root);
 				doc.Save(config, SaveOptions.None);
+				this.root = doc.Root;
 			}
 		}
 
-		void ISettingsProvider.Update(Action<XElement> action)
-		{
-			Update(action);
-		}
-
 		static string GetConfigFile()
 		{
 			if (null != SettingsFilePathProvider)
@@ -148,11 +132,6 @@ namespace ICSharpCode.ILSpyX.Settings
 			// return "ILSpy.xml";
 		}
 
-		ISettingsProvider ISettingsProvider.Load()
-		{
-			return Load();
-		}
-
 		const string ConfigFileMutex = "01A91708-49D1-410D-B8EB-4DE2662B3971";
 
 		/// <summary>
@@ -164,17 +143,16 @@ namespace ICSharpCode.ILSpyX.Settings
 
 			public MutexProtector(string name)
 			{
-				bool createdNew;
-				this.mutex = new Mutex(true, name, out createdNew);
-				if (!createdNew)
+				this.mutex = new Mutex(true, name, out bool createdNew);
+				if (createdNew)
+					return;
+
+				try
+				{
+					mutex.WaitOne();
+				}
+				catch (AbandonedMutexException)
 				{
-					try
-					{
-						mutex.WaitOne();
-					}
-					catch (AbandonedMutexException)
-					{
-					}
 				}
 			}
 
diff --git a/ICSharpCode.ILSpyX/Settings/IMiscSettings.cs b/ICSharpCode.ILSpyX/Settings/IMiscSettings.cs
deleted file mode 100644
index 363dba66b..000000000
--- a/ICSharpCode.ILSpyX/Settings/IMiscSettings.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (c) 2022 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.Xml.Linq;
-
-namespace ICSharpCode.ILSpyX.Settings
-{
-	public interface IMiscSettings
-	{
-		public bool AllowMultipleInstances { get; set; }
-		public bool LoadPreviousAssemblies { get; set; }
-
-		public static void Save(XElement root, IMiscSettings miscSettings)
-		{
-			var section = new XElement("MiscSettings");
-			section.SetAttributeValue(nameof(miscSettings.AllowMultipleInstances), miscSettings.AllowMultipleInstances);
-			section.SetAttributeValue(nameof(miscSettings.LoadPreviousAssemblies), miscSettings.LoadPreviousAssemblies);
-
-			XElement? existingElement = root.Element("MiscSettings");
-			if (existingElement != null)
-				existingElement.ReplaceWith(section);
-			else
-				root.Add(section);
-		}
-	}
-}
diff --git a/ICSharpCode.ILSpyX/Settings/ISettingsFilePathProvider.cs b/ICSharpCode.ILSpyX/Settings/ISettingsFilePathProvider.cs
index 15b16a09b..5e3ffba9b 100644
--- a/ICSharpCode.ILSpyX/Settings/ISettingsFilePathProvider.cs
+++ b/ICSharpCode.ILSpyX/Settings/ISettingsFilePathProvider.cs
@@ -16,8 +16,6 @@
 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
-using System;
-
 namespace ICSharpCode.ILSpyX.Settings
 {
 	public interface ISettingsFilePathProvider
diff --git a/ICSharpCode.ILSpyX/Settings/ISettingsProvider.cs b/ICSharpCode.ILSpyX/Settings/ISettingsProvider.cs
index 3f62dba76..1ddcec131 100644
--- a/ICSharpCode.ILSpyX/Settings/ISettingsProvider.cs
+++ b/ICSharpCode.ILSpyX/Settings/ISettingsProvider.cs
@@ -17,13 +17,8 @@
 // DEALINGS IN THE SOFTWARE.
 
 using System;
-using System.ComponentModel;
-using System.Linq;
-using System.Reflection;
 using System.Xml.Linq;
 
-using static System.Collections.Specialized.BitVector32;
-
 namespace ICSharpCode.ILSpyX.Settings
 {
 	public interface ISettingsProvider
@@ -31,39 +26,7 @@ namespace ICSharpCode.ILSpyX.Settings
 		XElement this[XName section] { get; }
 
 		void Update(Action<XElement> action);
-		ISettingsProvider Load();
-
-		public static ICSharpCode.Decompiler.DecompilerSettings LoadDecompilerSettings(ISettingsProvider settingsProvider)
-		{
-			XElement e = settingsProvider["DecompilerSettings"];
-			var newSettings = new Decompiler.DecompilerSettings();
-			var properties = typeof(Decompiler.DecompilerSettings).GetProperties()
-				.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false);
-			foreach (var p in properties)
-			{
-				var value = (bool?)e.Attribute(p.Name);
-				if (value.HasValue)
-					p.SetValue(newSettings, value.Value);
-			}
-			return newSettings;
-		}
-
-		public static void SaveDecompilerSettings(XElement root, ICSharpCode.Decompiler.DecompilerSettings newSettings)
-		{
-			var properties = typeof(Decompiler.DecompilerSettings).GetProperties()
-				.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false);
-
-			XElement section = new XElement("DecompilerSettings");
-			foreach (var p in properties)
-			{
-				section.SetAttributeValue(p.Name, p.GetValue(newSettings));
-			}
 
-			XElement? existingElement = root.Element("DecompilerSettings");
-			if (existingElement != null)
-				existingElement.ReplaceWith(section);
-			else
-				root.Add(section);
-		}
+		void SaveSettings(XElement section);
 	}
 }
diff --git a/ICSharpCode.ILSpyX/Settings/ISettingsSection.cs b/ICSharpCode.ILSpyX/Settings/ISettingsSection.cs
deleted file mode 100644
index 1735289ef..000000000
--- a/ICSharpCode.ILSpyX/Settings/ISettingsSection.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2022 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;
-
-namespace ICSharpCode.ILSpyX.Settings
-{
-	public interface ISettingsSection<TSelf>
-	{
-		// This should be abstract, but that needs C# 11.0 (see IParseable<TSelf>)
-		// Keep it to be enabled in the future
-		public static TSelf Load(ISettingsProvider settingsProvider)
-		{
-			throw new NotImplementedException();
-		}
-	}
-}
diff --git a/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs b/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs
index e34675fc3..5f8e1cc31 100644
--- a/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs
+++ b/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs
@@ -182,7 +182,7 @@ namespace ICSharpCode.ILSpyX.TreeView
 		#endregion
 
 		#region OnChildrenChanged
-		protected internal virtual void OnChildrenChanged(NotifyCollectionChangedEventArgs e)
+		public virtual void OnChildrenChanged(NotifyCollectionChangedEventArgs e)
 		{
 			if (e.OldItems != null)
 			{
@@ -359,7 +359,7 @@ namespace ICSharpCode.ILSpyX.TreeView
 			return TreeTraversal.PreOrder(this.Children.Where(c => c.isVisible), n => n.Children.Where(c => c.isVisible));
 		}
 
-		internal IEnumerable<SharpTreeNode> VisibleDescendantsAndSelf()
+		public IEnumerable<SharpTreeNode> VisibleDescendantsAndSelf()
 		{
 			return TreeTraversal.PreOrder(this, n => n.Children.Where(c => c.isVisible));
 		}
@@ -637,7 +637,7 @@ namespace ICSharpCode.ILSpyX.TreeView
 			return false;
 		}
 
-		internal void InternalDrop(IPlatformDragEventArgs e, int index)
+		public void InternalDrop(IPlatformDragEventArgs e, int index)
 		{
 			if (LazyLoading)
 			{
@@ -658,8 +658,9 @@ namespace ICSharpCode.ILSpyX.TreeView
 
 		public bool IsLast {
 			get {
-				return Parent == null ||
-					Parent.Children[Parent.Children.Count - 1] == this;
+				return Parent == null
+					|| Parent.Children.Count == 0
+					|| Parent.Children[^1] == this;
 			}
 		}
 
diff --git a/ICSharpCode.ILSpyX/TreeView/TreeFlattener.cs b/ICSharpCode.ILSpyX/TreeView/TreeFlattener.cs
index 46cc5cafd..80803c194 100644
--- a/ICSharpCode.ILSpyX/TreeView/TreeFlattener.cs
+++ b/ICSharpCode.ILSpyX/TreeView/TreeFlattener.cs
@@ -26,7 +26,7 @@ using System.Diagnostics;
 
 namespace ICSharpCode.ILSpyX.TreeView
 {
-	sealed class TreeFlattener : IList, INotifyCollectionChanged
+	public sealed class TreeFlattener : IList, INotifyCollectionChanged
 	{
 		/// <summary>
 		/// The root node of the flat list tree.
diff --git a/ILSpy.ReadyToRun/ReadyToRunDisassembler.cs b/ILSpy.ReadyToRun/ReadyToRunDisassembler.cs
index 564922d1b..7f4fd0817 100644
--- a/ILSpy.ReadyToRun/ReadyToRunDisassembler.cs
+++ b/ILSpy.ReadyToRun/ReadyToRunDisassembler.cs
@@ -26,6 +26,7 @@ using Iced.Intel;
 using ICSharpCode.Decompiler;
 using ICSharpCode.Decompiler.IL;
 using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.ILSpy.Util;
 
 using ILCompiler.Reflection.ReadyToRun;
 using ILCompiler.Reflection.ReadyToRun.Amd64;
@@ -50,11 +51,13 @@ namespace ICSharpCode.ILSpy.ReadyToRun
 			ReadyToRunMethod readyToRunMethod = runtimeFunction.Method;
 			WriteCommentLine(readyToRunMethod.SignatureString);
 
-			if (ReadyToRunOptions.GetIsShowGCInfo(null))
+			var options = SettingsService.Instance.GetSettings<ReadyToRunOptions>();
+
+			if (options.IsShowGCInfo)
 			{
 				if (readyToRunMethod.GcInfo != null)
 				{
-					string[] lines = readyToRunMethod.GcInfo.ToString().Split(Environment.NewLine);
+					string[] lines = readyToRunMethod.GcInfo.ToString()?.Split(Environment.NewLine) ?? [];
 					WriteCommentLine("GC info:");
 					foreach (string line in lines)
 					{
@@ -68,12 +71,12 @@ namespace ICSharpCode.ILSpy.ReadyToRun
 			}
 
 			Dictionary<ulong, UnwindCode>? unwindInfo = null;
-			if (ReadyToRunOptions.GetIsShowUnwindInfo(null) && bitness == 64)
+			if (options.IsShowUnwindInfo && bitness == 64)
 			{
 				unwindInfo = WriteUnwindInfo();
 			}
 
-			bool isShowDebugInfo = ReadyToRunOptions.GetIsShowDebugInfo(null);
+			bool isShowDebugInfo = options.IsShowDebugInfo;
 			DebugInfoHelper? debugInfo = null;
 			if (isShowDebugInfo)
 			{
@@ -97,7 +100,7 @@ namespace ICSharpCode.ILSpy.ReadyToRun
 				decoder.Decode(out instructions.AllocUninitializedElement());
 			}
 
-			string disassemblyFormat = ReadyToRunOptions.GetDisassemblyFormat(null);
+			string disassemblyFormat = options.DisassemblyFormat;
 			Formatter? formatter = null;
 			if (disassemblyFormat.Equals(ReadyToRunOptions.intel))
 			{
@@ -144,7 +147,7 @@ namespace ICSharpCode.ILSpy.ReadyToRun
 						}
 					}
 				}
-				if (ReadyToRunOptions.GetIsShowGCInfo(null))
+				if (options.IsShowGCInfo)
 				{
 					DecorateGCInfo(instr, baseInstrIP, readyToRunMethod.GcInfo);
 				}
diff --git a/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml b/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml
index 137e0a6a1..388923678 100644
--- a/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml
+++ b/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml
@@ -1,7 +1,11 @@
 <UserControl x:Class="ICSharpCode.ILSpy.ReadyToRun.ReadyToRunOptionPage"
 	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 	xmlns:properties="clr-namespace:ILSpy.ReadyToRun.Properties"
-	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+	xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d:DesignHeight="500" d:DesignWidth="500" mc:Ignorable="d"
+	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+	xmlns:readyToRun="clr-namespace:ICSharpCode.ILSpy.ReadyToRun"
+	d:DataContext="{d:DesignInstance readyToRun:ReadyToRunOptionsViewModel}">
 	<Grid>
 		<Grid.ColumnDefinitions>
 			<ColumnDefinition />
@@ -14,12 +18,12 @@
 			<RowDefinition Height="Auto" />
 		</Grid.RowDefinitions>
 		<TextBlock Margin="3" Text="{x:Static properties:Resources.DisassemblyFormat}" />
-		<ComboBox Grid.Column="1" Margin="3" ItemsSource="{Binding DisassemblyFormats}" SelectedItem="{Binding DisassemblyFormat}" />
-		<TextBlock Grid.Row="1" Margin="3" Text="{x:Static properties:Resources.ShowStackUnwindInfo}"/>
-		<CheckBox Grid.Row="1" Grid.Column="1" Margin="3" IsChecked="{Binding IsShowUnwindInfo}" />
+		<ComboBox Grid.Row="0" Grid.Column="1" Margin="3" ItemsSource="{Binding Options.DisassemblyFormats}" SelectedItem="{Binding Options.DisassemblyFormat}" />
+		<TextBlock Grid.Row="1" Grid.Column="0"  Margin="3" Text="{x:Static properties:Resources.ShowStackUnwindInfo}"/>
+		<CheckBox Grid.Row="1" Grid.Column="1" Margin="3" IsChecked="{Binding Options.IsShowUnwindInfo}" />
 		<TextBlock Grid.Row="2" Margin="3" Text="{x:Static properties:Resources.ShowDebugInfo}"/>
-		<CheckBox Grid.Row="2" Grid.Column="1" Margin="3" IsChecked="{Binding IsShowDebugInfo}" />
+		<CheckBox Grid.Row="2" Grid.Column="1" Margin="3" IsChecked="{Binding Options.IsShowDebugInfo}" />
 		<TextBlock Grid.Row="3" Margin="3" Text="{x:Static properties:Resources.ShowGCInfo}"/>
-		<CheckBox Grid.Row="3" Grid.Column="1" Margin="3" IsChecked="{Binding IsShowGCInfo}" />
+		<CheckBox Grid.Row="3" Grid.Column="1" Margin="3" IsChecked="{Binding Options.IsShowGCInfo}" />
 	</Grid>
 </UserControl>
\ No newline at end of file
diff --git a/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml.cs b/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml.cs
index 8b8ae256e..d48d55ec1 100644
--- a/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml.cs
+++ b/ILSpy.ReadyToRun/ReadyToRunOptionPage.xaml.cs
@@ -16,112 +16,47 @@
 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
-using System.ComponentModel;
 using System.ComponentModel.Composition;
-using System.Windows.Controls;
-using System.Xml.Linq;
 
 using ICSharpCode.ILSpy.Options;
-using ICSharpCode.ILSpyX.Settings;
+using ICSharpCode.ILSpy.Util;
+
+using TomsToolbox.Wpf;
+using TomsToolbox.Wpf.Composition.Mef;
 
 namespace ICSharpCode.ILSpy.ReadyToRun
 {
-	[ExportOptionPage(Title = nameof(global::ILSpy.ReadyToRun.Properties.Resources.ReadyToRun), Order = 40)]
+	[DataTemplate(typeof(ReadyToRunOptionsViewModel))]
 	[PartCreationPolicy(CreationPolicy.NonShared)]
-	partial class ReadyToRunOptionPage : UserControl, IOptionPage
+	partial class ReadyToRunOptionPage
 	{
 		public ReadyToRunOptionPage()
 		{
 			InitializeComponent();
 		}
-
-		public void Load(ILSpySettings settings)
-		{
-			Options s = new Options();
-			s.DisassemblyFormat = ReadyToRunOptions.GetDisassemblyFormat(settings);
-			s.IsShowUnwindInfo = ReadyToRunOptions.GetIsShowUnwindInfo(settings);
-			s.IsShowDebugInfo = ReadyToRunOptions.GetIsShowDebugInfo(settings);
-			s.IsShowGCInfo = ReadyToRunOptions.GetIsShowGCInfo(settings);
-
-			this.DataContext = s;
-		}
-
-		public void LoadDefaults()
-		{
-			this.DataContext = new Options();
-		}
-
-		public void Save(XElement root)
-		{
-			Options s = (Options)this.DataContext;
-			ReadyToRunOptions.SetDisassemblyOptions(root, s.DisassemblyFormat, s.IsShowUnwindInfo, s.IsShowDebugInfo, s.IsShowGCInfo);
-		}
 	}
 
-	internal class Options : INotifyPropertyChanged
+	[ExportOptionPage(Order = 40)]
+	[PartCreationPolicy(CreationPolicy.NonShared)]
+	class ReadyToRunOptionsViewModel : ObservableObject, IOptionPage
 	{
-		public string[] DisassemblyFormats {
-			get {
-				return ReadyToRunOptions.disassemblyFormats;
-			}
-		}
-
-		private bool isShowUnwindInfo;
-		public bool IsShowUnwindInfo {
-			get {
-				return isShowUnwindInfo;
-			}
-			set {
-				isShowUnwindInfo = value;
-				OnPropertyChanged(nameof(IsShowUnwindInfo));
-			}
-		}
-
-		private bool isShowDebugInfo;
+		private ReadyToRunOptions options;
 
-		public bool IsShowDebugInfo {
-			get {
-				return isShowDebugInfo;
-			}
-			set {
-				isShowDebugInfo = value;
-				OnPropertyChanged(nameof(IsShowDebugInfo));
-			}
+		public ReadyToRunOptions Options {
+			get => options;
+			set => SetProperty(ref options, value);
 		}
 
-		private bool isShowGCInfo;
+		public string Title => global::ILSpy.ReadyToRun.Properties.Resources.ReadyToRun;
 
-		public bool IsShowGCInfo {
-			get {
-				return isShowGCInfo;
-			}
-			set {
-				isShowGCInfo = value;
-				OnPropertyChanged(nameof(IsShowGCInfo));
-			}
-		}
-
-		private string disassemblyFormat;
-
-		public string DisassemblyFormat {
-			get { return disassemblyFormat; }
-			set {
-				if (disassemblyFormat != value)
-				{
-					disassemblyFormat = value;
-					OnPropertyChanged(nameof(DisassemblyFormat));
-				}
-			}
+		public void Load(SettingsSnapshot snapshot)
+		{
+			Options = snapshot.GetSettings<ReadyToRunOptions>();
 		}
 
-		public event PropertyChangedEventHandler? PropertyChanged;
-
-		protected virtual void OnPropertyChanged(string? propertyName)
+		public void LoadDefaults()
 		{
-			if (PropertyChanged != null)
-			{
-				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
-			}
+			Options.LoadFromXml(new("empty"));
 		}
 	}
 }
\ No newline at end of file
diff --git a/ILSpy.ReadyToRun/ReadyToRunOptions.cs b/ILSpy.ReadyToRun/ReadyToRunOptions.cs
index 5e65b574a..e6b47e69f 100644
--- a/ILSpy.ReadyToRun/ReadyToRunOptions.cs
+++ b/ILSpy.ReadyToRun/ReadyToRunOptions.cs
@@ -18,110 +18,77 @@
 
 using System.Xml.Linq;
 
-using ICSharpCode.ILSpyX;
-using ICSharpCode.ILSpyX.Settings;
+using ICSharpCode.ILSpy.Util;
+
+using TomsToolbox.Wpf;
 
 namespace ICSharpCode.ILSpy.ReadyToRun
 {
-	internal class ReadyToRunOptions
+	internal partial class ReadyToRunOptions : ObservableObject, ISettingsSection
 	{
 		private static readonly XNamespace ns = "http://www.ilspy.net/ready-to-run";
 
 		internal static string intel = "Intel";
 		internal static string gas = "AT & T";
-		internal static string[] disassemblyFormats = new string[] { intel, gas };
+		internal static string[] disassemblyFormats = [intel, gas];
 
-		public static string GetDisassemblyFormat(ILSpySettings? settings)
-		{
-			if (settings == null)
-			{
-				settings = ILSpySettings.Load();
-			}
-			XElement e = settings[ns + "ReadyToRunOptions"];
-			XAttribute? a = e.Attribute("DisassemblyFormat");
-			if (a == null)
-			{
-				return ReadyToRunOptions.intel;
-			}
-			else
-			{
-				return (string)a;
+		public string[] DisassemblyFormats {
+			get {
+				return disassemblyFormats;
 			}
 		}
 
-		public static bool GetIsShowUnwindInfo(ILSpySettings? settings)
-		{
-			if (settings == null)
-			{
-				settings = ILSpySettings.Load();
-			}
-			XElement e = settings[ns + "ReadyToRunOptions"];
-			XAttribute? a = e.Attribute("IsShowUnwindInfo");
+		private bool isShowUnwindInfo;
+		public bool IsShowUnwindInfo {
+			get => isShowUnwindInfo;
+			set => SetProperty(ref isShowUnwindInfo, value);
+		}
 
-			if (a == null)
-			{
-				return false;
-			}
-			else
-			{
-				return (bool)a;
-			}
+		private bool isShowDebugInfo;
+		public bool IsShowDebugInfo {
+			get => isShowDebugInfo;
+			set => SetProperty(ref isShowDebugInfo, value);
 		}
 
-		public static bool GetIsShowDebugInfo(ILSpySettings? settings)
-		{
-			if (settings == null)
-			{
-				settings = ILSpySettings.Load();
-			}
-			XElement e = settings[ns + "ReadyToRunOptions"];
-			XAttribute? a = e.Attribute("IsShowDebugInfo");
+		private bool isShowGCInfo;
+		public bool IsShowGCInfo {
+			get => isShowGCInfo;
+			set => SetProperty(ref isShowGCInfo, value);
+		}
 
-			if (a == null)
-			{
-				return true;
-			}
-			else
-			{
-				return (bool)a;
-			}
+		private string disassemblyFormat;
+		public string DisassemblyFormat {
+			get => disassemblyFormat;
+			set => SetProperty(ref disassemblyFormat, value);
 		}
 
-		public static bool GetIsShowGCInfo(ILSpySettings? settings)
+		public XName SectionName { get; } = ns + "ReadyToRunOptions";
+
+		public void LoadFromXml(XElement e)
 		{
-			if (settings == null)
-			{
-				settings = ILSpySettings.Load();
-			}
-			XElement e = settings[ns + "ReadyToRunOptions"];
-			XAttribute? a = e.Attribute("IsShowGCInfo");
+			XAttribute? format = e.Attribute("DisassemblyFormat");
+			DisassemblyFormat = format == null ? intel : (string)format;
 
-			if (a == null)
-			{
-				return false;
-			}
-			else
-			{
-				return (bool)a;
-			}
+			XAttribute? unwind = e.Attribute("IsShowUnwindInfo");
+			IsShowUnwindInfo = unwind != null && (bool)unwind;
+
+			XAttribute? debug = e.Attribute("IsShowDebugInfo");
+			IsShowDebugInfo = debug == null || (bool)debug;
+
+			XAttribute? showGc = e.Attribute("IsShowGCInfo");
+			IsShowGCInfo = showGc != null && (bool)showGc;
 		}
 
-		public static void SetDisassemblyOptions(XElement root, string disassemblyFormat, bool isShowUnwindInfo, bool isShowDebugInfo, bool isShowGCInfo)
+		public XElement SaveToXml()
 		{
-			XElement section = new XElement(ns + "ReadyToRunOptions");
+			var section = new XElement(SectionName);
+
 			section.SetAttributeValue("DisassemblyFormat", disassemblyFormat);
 			section.SetAttributeValue("IsShowUnwindInfo", isShowUnwindInfo);
 			section.SetAttributeValue("IsShowDebugInfo", isShowDebugInfo);
 			section.SetAttributeValue("IsShowGCInfo", isShowGCInfo);
-			XElement? existingElement = root.Element(ns + "ReadyToRunOptions");
-			if (existingElement != null)
-			{
-				existingElement.ReplaceWith(section);
-			}
-			else
-			{
-				root.Add(section);
-			}
+
+			return section;
 		}
 	}
 }
diff --git a/ILSpy/AboutPage.cs b/ILSpy/AboutPage.cs
index f9f969eda..4bbe5769d 100644
--- a/ILSpy/AboutPage.cs
+++ b/ILSpy/AboutPage.cs
@@ -42,7 +42,7 @@ namespace ICSharpCode.ILSpy
 	{
 		public override void Execute(object? parameter)
 		{
-			MainWindow.Instance.NavigateTo(
+			MainWindow.Instance.AssemblyTreeModel.NavigateTo(
 				new RequestNavigateEventArgs(new Uri("resource://aboutpage"), null),
 				inNewTabPage: true
 			);
@@ -54,7 +54,7 @@ namespace ICSharpCode.ILSpy
 				Title = Resources.About,
 				EnableHyperlinks = true
 			};
-			output.WriteLine(Resources.ILSpyVersion + DecompilerVersionInfo.FullVersion);
+			output.WriteLine(Resources.ILSpyVersion + DecompilerVersionInfo.FullVersionWithCommitHash);
 
 			string prodVersion = GetDotnetProductVersion();
 			output.WriteLine(Resources.NETFrameworkVersion + prodVersion);
@@ -76,7 +76,7 @@ namespace ICSharpCode.ILSpy
 				CheckBox checkBox = new CheckBox();
 				checkBox.Margin = new Thickness(4);
 				checkBox.Content = Resources.AutomaticallyCheckUpdatesEveryWeek;
-				UpdateSettings settings = new UpdateSettings(ILSpySettings.Load());
+				UpdateSettings settings = new UpdateSettings(SettingsService.Instance.SpySettings);
 				checkBox.SetBinding(CheckBox.IsCheckedProperty, new Binding("AutomaticUpdateCheckEnabled") { Source = settings });
 				return new StackPanel {
 					Margin = new Thickness(0, 4, 0, 0),
diff --git a/ILSpy/Analyzers/AnalyzeCommand.cs b/ILSpy/Analyzers/AnalyzeCommand.cs
index 65e2f3095..f8230fad7 100644
--- a/ILSpy/Analyzers/AnalyzeCommand.cs
+++ b/ILSpy/Analyzers/AnalyzeCommand.cs
@@ -78,12 +78,12 @@ namespace ICSharpCode.ILSpy.Analyzers
 
 		public override bool CanExecute(object? parameter)
 		{
-			return MainWindow.Instance.SelectedNodes.All(n => n is IMemberTreeNode);
+			return MainWindow.Instance.AssemblyTreeModel.SelectedNodes.All(n => n is IMemberTreeNode);
 		}
 
 		public override void Execute(object? parameter)
 		{
-			foreach (var node in MainWindow.Instance.SelectedNodes.OfType<IMemberTreeNode>())
+			foreach (var node in MainWindow.Instance.AssemblyTreeModel.SelectedNodes.OfType<IMemberTreeNode>())
 			{
 				AnalyzerTreeView.Analyze(node.Member);
 			}
diff --git a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs
index 3dfab8c06..42451c80f 100644
--- a/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs
+++ b/ILSpy/Analyzers/AnalyzerEntityTreeNode.cs
@@ -42,7 +42,8 @@ namespace ICSharpCode.ILSpy.Analyzers
 				MessageBox.Show(Properties.Resources.CannotAnalyzeMissingRef, "ILSpy");
 				return;
 			}
-			MainWindow.Instance.JumpToReference(new EntityReference(this.Member.ParentModule.MetadataFile, this.Member.MetadataToken));
+
+			MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(this.Member.ParentModule?.MetadataFile, this.Member.MetadataToken)));
 		}
 
 		public override bool HandleAssemblyListChanged(ICollection<LoadedAssembly> removedAssemblies, ICollection<LoadedAssembly> addedAssemblies)
diff --git a/ILSpy/Analyzers/AnalyzerRootNode.cs b/ILSpy/Analyzers/AnalyzerRootNode.cs
index c47fbe27c..93d641554 100644
--- a/ILSpy/Analyzers/AnalyzerRootNode.cs
+++ b/ILSpy/Analyzers/AnalyzerRootNode.cs
@@ -2,7 +2,6 @@
 using System.Collections.Specialized;
 using System.Linq;
 
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 using ICSharpCode.ILSpyX.TreeView;
 
diff --git a/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs b/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs
index 0a7bbb95c..7c3d484bc 100644
--- a/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs
+++ b/ILSpy/Analyzers/AnalyzerSearchTreeNode.cs
@@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy.Analyzers
 				var context = new AnalyzerContext() {
 					CancellationToken = ct,
 					Language = Language,
-					AssemblyList = MainWindow.Instance.CurrentAssemblyList
+					AssemblyList = MainWindow.Instance.AssemblyTreeModel.AssemblyList
 				};
 				var results = analyzer.Analyze(symbol, context).Select(SymbolTreeNodeFactory);
 				if (context.SortResults)
diff --git a/ILSpy/Analyzers/AnalyzerTreeNode.cs b/ILSpy/Analyzers/AnalyzerTreeNode.cs
index 8972926a7..fb052303e 100644
--- a/ILSpy/Analyzers/AnalyzerTreeNode.cs
+++ b/ILSpy/Analyzers/AnalyzerTreeNode.cs
@@ -18,7 +18,6 @@
 
 using System.Collections.Generic;
 
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 using ICSharpCode.ILSpyX.TreeView;
 
diff --git a/ILSpy/Analyzers/TreeNodes/AnalyzedModuleTreeNode.cs b/ILSpy/Analyzers/TreeNodes/AnalyzedModuleTreeNode.cs
index b54f31f2e..ad8395d31 100644
--- a/ILSpy/Analyzers/TreeNodes/AnalyzedModuleTreeNode.cs
+++ b/ILSpy/Analyzers/TreeNodes/AnalyzedModuleTreeNode.cs
@@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy.Analyzers.TreeNodes
 				MessageBox.Show(Properties.Resources.CannotAnalyzeMissingRef, "ILSpy");
 				return;
 			}
-			MainWindow.Instance.JumpToReference(analyzedModule.MetadataFile);
+			MessageBus.Send(this, new NavigateToReferenceEventArgs(analyzedModule.MetadataFile));
 		}
 
 		public override IEntity? Member => null;
diff --git a/ILSpy/App.xaml b/ILSpy/App.xaml
index 0cfda48ce..ce2e14086 100644
--- a/ILSpy/App.xaml
+++ b/ILSpy/App.xaml
@@ -1,15 +1,14 @@
 <Application x:Class="ICSharpCode.ILSpy.App"
-             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:styles="urn:TomsToolbox.Wpf.Styles"
-             xmlns:toms="urn:TomsToolbox"
-             xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes"
-             StartupUri="MainWindow.xaml">
+			 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+			 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+			 xmlns:styles="urn:TomsToolbox.Wpf.Styles"
+			 xmlns:toms="urn:TomsToolbox"
+			 xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes">
 	<Application.Resources>
 		<Style x:Key="DialogWindow" TargetType="{x:Type Window}">
-			<Setter Property="ShowInTaskbar" Value="False"/>
-			<Setter Property="UseLayoutRounding" Value="True"/>
-			<Setter Property="TextOptions.TextFormattingMode" Value="Display"/>
+			<Setter Property="ShowInTaskbar" Value="False" />
+			<Setter Property="UseLayoutRounding" Value="True" />
+			<Setter Property="TextOptions.TextFormattingMode" Value="Display" />
 			<Setter Property="toms:StyleBindings.Behaviors">
 				<Setter.Value>
 					<toms:BehaviorCollection>
diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs
index fa45bd234..a5027f08e 100644
--- a/ILSpy/App.xaml.cs
+++ b/ILSpy/App.xaml.cs
@@ -32,7 +32,7 @@ using System.Windows.Navigation;
 using System.Windows.Threading;
 
 using ICSharpCode.ILSpy.AppEnv;
-using ICSharpCode.ILSpy.Options;
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpyX.Analyzers;
 using ICSharpCode.ILSpyX.Settings;
 using ICSharpCode.ILSpyX.TreeView;
@@ -43,6 +43,9 @@ using Microsoft.VisualStudio.Composition;
 
 using TomsToolbox.Composition;
 using TomsToolbox.Wpf.Composition;
+using ICSharpCode.ILSpy.Themes;
+using System.Globalization;
+using System.Threading;
 using TomsToolbox.Wpf.Styles;
 
 namespace ICSharpCode.ILSpy
@@ -65,31 +68,31 @@ namespace ICSharpCode.ILSpy
 
 		public App()
 		{
-			ILSpySettings.SettingsFilePathProvider = new ILSpySettingsFilePathProvider();
-
 			var cmdArgs = Environment.GetCommandLineArgs().Skip(1);
-			App.CommandLineArguments = CommandLineArguments.Create(cmdArgs);
+			CommandLineArguments = CommandLineArguments.Create(cmdArgs);
 
-			bool forceSingleInstance = (App.CommandLineArguments.SingleInstance ?? true)
-				&& !MiscSettingsPanel.CurrentMiscSettings.AllowMultipleInstances;
+			bool forceSingleInstance = (CommandLineArguments.SingleInstance ?? true)
+									   && !SettingsService.Instance.MiscSettings.AllowMultipleInstances;
 			if (forceSingleInstance)
 			{
 				SingleInstance.Attach();  // will auto-exit for second instance
 				SingleInstance.NewInstanceDetected += SingleInstance_NewInstanceDetected;
 			}
 
-			SharpTreeNode.SetImagesProvider(new WpfWindowsTreeNodeImagesProvider());
-
 			InitializeComponent();
 
-			Resources.RegisterDefaultStyles();
-
-			if (!System.Diagnostics.Debugger.IsAttached)
+			if (!Debugger.IsAttached)
 			{
 				AppDomain.CurrentDomain.UnhandledException += ShowErrorBox;
 				Dispatcher.CurrentDispatcher.UnhandledException += Dispatcher_UnhandledException;
 			}
+
 			TaskScheduler.UnobservedTaskException += DotNet40_UnobservedTaskException;
+
+			SharpTreeNode.SetImagesProvider(new WpfWindowsTreeNodeImagesProvider());
+
+			Resources.RegisterDefaultStyles();
+
 			InitializeMef().GetAwaiter().GetResult();
 
 			// Register the export provider so that it can be accessed from WPF/XAML components.
@@ -97,30 +100,34 @@ namespace ICSharpCode.ILSpy
 			// Add data templates registered via MEF.
 			Resources.MergedDictionaries.Add(DataTemplateManager.CreateDynamicDataTemplates(ExportProvider));
 
+			var sessionSettings = SettingsService.Instance.SessionSettings;
+			ThemeManager.Current.Theme = sessionSettings.Theme;
+			if (!string.IsNullOrEmpty(sessionSettings.CurrentCulture))
+			{
+				Thread.CurrentThread.CurrentUICulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(sessionSettings.CurrentCulture);
+			}
+
 			EventManager.RegisterClassHandler(typeof(Window),
 											  Hyperlink.RequestNavigateEvent,
 											  new RequestNavigateEventHandler(Window_RequestNavigate));
 			ILSpyTraceListener.Install();
 
-			if (App.CommandLineArguments.ArgumentsParser.IsShowingInformation)
+			if (CommandLineArguments.ArgumentsParser.IsShowingInformation)
 			{
-				MessageBox.Show(App.CommandLineArguments.ArgumentsParser.GetHelpText(), "ILSpy Command Line Arguments");
+				MessageBox.Show(CommandLineArguments.ArgumentsParser.GetHelpText(), "ILSpy Command Line Arguments");
 			}
 
-			if (App.CommandLineArguments.ArgumentsParser.RemainingArguments.Any())
+			if (CommandLineArguments.ArgumentsParser.RemainingArguments.Any())
 			{
-				string unknownArguments = string.Join(", ", App.CommandLineArguments.ArgumentsParser.RemainingArguments);
+				string unknownArguments = string.Join(", ", CommandLineArguments.ArgumentsParser.RemainingArguments);
 				MessageBox.Show(unknownArguments, "ILSpy Unknown Command Line Arguments Passed");
 			}
-		}
 
-		private static void SingleInstance_NewInstanceDetected(object? sender, NewInstanceEventArgs e)
-		{
-#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
-			ICSharpCode.ILSpy.MainWindow.Instance.HandleSingleInstanceCommandLineArguments(e.Args);
-#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
+			SettingsService.Instance.AssemblyListManager.CreateDefaultAssemblyLists();
 		}
 
+		private static void SingleInstance_NewInstanceDetected(object? sender, NewInstanceEventArgs e) => ExportProvider.GetExportedValue<AssemblyTreeModel>().HandleSingleInstanceCommandLineArguments(e.Args).HandleExceptions();
+
 		static Assembly? ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName)
 		{
 			var rootPath = Path.GetDirectoryName(typeof(App).Assembly.Location);
@@ -199,6 +206,8 @@ namespace ICSharpCode.ILSpy
 
 		protected override void OnStartup(StartupEventArgs e)
 		{
+			base.OnStartup(e);
+
 			var output = new StringBuilder();
 
 			if (StartupExceptions.FormatExceptions(output))
@@ -206,7 +215,12 @@ namespace ICSharpCode.ILSpy
 				MessageBox.Show(output.ToString(), "Sorry we crashed!");
 				Environment.Exit(1);
 			}
-			base.OnStartup(e);
+
+			MainWindow = new MainWindow();
+			MainWindow.Loaded += (sender, args) => {
+				ExportProvider.GetExportedValue<AssemblyTreeModel>().Initialize();
+			};
+			MainWindow.Show();
 		}
 
 		void DotNet40_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
@@ -267,7 +281,7 @@ namespace ICSharpCode.ILSpy
 
 		void Window_RequestNavigate(object sender, RequestNavigateEventArgs e)
 		{
-			ILSpy.MainWindow.Instance.NavigateTo(e);
+			ExportProvider.GetExportedValue<AssemblyTreeModel>().NavigateTo(e);
 		}
 	}
 }
\ No newline at end of file
diff --git a/ILSpy/AssemblyTree/AssemblyListPane.xaml b/ILSpy/AssemblyTree/AssemblyListPane.xaml
new file mode 100644
index 000000000..7222fdabf
--- /dev/null
+++ b/ILSpy/AssemblyTree/AssemblyListPane.xaml
@@ -0,0 +1,54 @@
+<treeView:SharpTreeView x:Class="ICSharpCode.ILSpy.AssemblyTree.AssemblyListPane"
+						xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+						xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+						xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+						xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+						xmlns:treeView="clr-namespace:ICSharpCode.ILSpy.Controls.TreeView"
+						xmlns:treeNodes="clr-namespace:ICSharpCode.ILSpy.TreeNodes"
+						xmlns:assemblyTree="clr-namespace:ICSharpCode.ILSpy.AssemblyTree"
+						xmlns:toms="urn:TomsToolbox"
+						mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"
+						d:DataContext="{d:DesignInstance assemblyTree:AssemblyTreeModel}"
+						AutomationProperties.Name="Assemblies and Classes"
+						ShowRoot="False"
+						AllowDropOrder="True"
+						AllowDrop="True"
+						BorderThickness="0" Visibility="Visible"
+						Root="{Binding Root}"
+						SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
+						toms:MultiSelectorExtensions.SelectionBinding="{Binding SelectedItems}">
+	<treeView:SharpTreeView.ItemContainerStyle>
+		<Style TargetType="treeView:SharpTreeViewItem">
+			<Setter Property="Template">
+				<Setter.Value>
+					<ControlTemplate TargetType="{x:Type treeView:SharpTreeViewItem}"
+									 d:DataContext="{d:DesignInstance treeNodes:ILSpyTreeNode}">
+						<Border Background="Transparent">
+							<Border Background="{TemplateBinding Background}">
+								<treeView:SharpTreeNodeView x:Name="nodeView" HorizontalAlignment="Left" />
+							</Border>
+						</Border>
+						<ControlTemplate.Triggers>
+							<DataTrigger Binding="{Binding IsAutoLoaded}" Value="True">
+								<Setter Property="Foreground" Value="SteelBlue" />
+							</DataTrigger>
+							<DataTrigger Binding="{Binding IsPublicAPI}" Value="False">
+								<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
+							</DataTrigger>
+							<Trigger Property="IsSelected" Value="True">
+								<Setter TargetName="nodeView" Property="TextBackground"
+										Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
+								<Setter TargetName="nodeView" Property="Foreground"
+										Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
+							</Trigger>
+							<Trigger Property="IsEnabled" Value="False">
+								<Setter TargetName="nodeView" Property="Foreground"
+										Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
+							</Trigger>
+						</ControlTemplate.Triggers>
+					</ControlTemplate>
+				</Setter.Value>
+			</Setter>
+		</Style>
+	</treeView:SharpTreeView.ItemContainerStyle>
+</treeView:SharpTreeView>
\ No newline at end of file
diff --git a/ILSpy/AssemblyTree/AssemblyListPane.xaml.cs b/ILSpy/AssemblyTree/AssemblyListPane.xaml.cs
new file mode 100644
index 000000000..9b6e7fb79
--- /dev/null
+++ b/ILSpy/AssemblyTree/AssemblyListPane.xaml.cs
@@ -0,0 +1,44 @@
+using System.ComponentModel.Composition;
+using System.Windows;
+
+using ICSharpCode.ILSpyX.TreeView;
+
+using TomsToolbox.Wpf.Composition.Mef;
+
+namespace ICSharpCode.ILSpy.AssemblyTree
+{
+	/// <summary>
+	/// Interaction logic for AssemblyListPane.xaml
+	/// </summary>
+	[DataTemplate(typeof(AssemblyTreeModel))]
+	[PartCreationPolicy(CreationPolicy.NonShared)]
+	public partial class AssemblyListPane
+	{
+		public AssemblyListPane()
+		{
+			InitializeComponent();
+
+			ContextMenuProvider.Add(this);
+		}
+
+		protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
+		{
+			base.OnPropertyChanged(e);
+
+			if (e.Property == DataContextProperty)
+			{
+				if (e.NewValue is not AssemblyTreeModel model)
+					return;
+
+				model.SetActiveView(this);
+			}
+			else if (e.Property == SelectedItemProperty)
+			{
+				if (e.NewValue is not SharpTreeNode treeNode)
+					return;
+
+				FocusNode(treeNode);
+			}
+		}
+	}
+}
diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
new file mode 100644
index 000000000..1c6903a55
--- /dev/null
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -0,0 +1,922 @@
+// Copyright (c) 2019 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.Collections.Generic;
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using System.Windows.Threading;
+
+using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpy.Docking;
+using ICSharpCode.ILSpy.Properties;
+using ICSharpCode.ILSpy.TextView;
+using ICSharpCode.ILSpy.TreeNodes;
+using ICSharpCode.ILSpy.ViewModels;
+using ICSharpCode.ILSpyX;
+using ICSharpCode.ILSpyX.Settings;
+using ICSharpCode.ILSpyX.TreeView;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+using ICSharpCode.Decompiler.Metadata;
+
+using System.Reflection.Metadata.Ecma335;
+using System.Windows;
+using ICSharpCode.Decompiler.Documentation;
+using ICSharpCode.Decompiler.TypeSystem.Implementation;
+using System.Reflection.Metadata;
+using System.Text;
+using System.Windows.Navigation;
+
+using ICSharpCode.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.Search;
+using ICSharpCode.Decompiler;
+
+using TomsToolbox.Essentials;
+using TomsToolbox.Wpf;
+
+namespace ICSharpCode.ILSpy.AssemblyTree
+{
+	[ExportToolPane]
+	[PartCreationPolicy(CreationPolicy.Shared)]
+	[Export]
+	public class AssemblyTreeModel : ToolPaneModel
+	{
+		public const string PaneContentId = "assemblyListPane";
+
+		AssemblyListPane activeView;
+		AssemblyListTreeNode assemblyListTreeNode;
+
+		readonly NavigationHistory<NavigationState> history = new();
+		private bool isNavigatingHistory;
+
+		public AssemblyTreeModel()
+		{
+			Title = Resources.Assemblies;
+			ContentId = PaneContentId;
+			IsCloseable = false;
+			ShortcutKey = new KeyGesture(Key.F6);
+
+			MessageBus<NavigateToReferenceEventArgs>.Subscribers += JumpToReference;
+			MessageBus<SettingsChangedEventArgs>.Subscribers += (sender, e) => Settings_PropertyChanged(sender, e);
+
+			var selectionChangeThrottle = new DispatcherThrottle(DispatcherPriority.Input, TreeView_SelectionChanged);
+			SelectedItems.CollectionChanged += (_, _) => selectionChangeThrottle.Tick();
+		}
+
+		private void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e)
+		{
+			if (sender is SessionSettings sessionSettings)
+			{
+				switch (e.PropertyName)
+				{
+					case nameof(SessionSettings.ActiveAssemblyList):
+						ShowAssemblyList(sessionSettings.ActiveAssemblyList);
+						break;
+					case nameof(SessionSettings.Theme):
+						// update syntax highlighting and force reload (AvalonEdit does not automatically refresh on highlighting change)
+						DecompilerTextView.RegisterHighlighting();
+						DecompileSelectedNodes(DockWorkspace.Instance.ActiveTabPage.GetState() as DecompilerTextViewState);
+						break;
+					case nameof(SessionSettings.CurrentCulture):
+						MessageBox.Show(Properties.Resources.SettingsChangeRestartRequired, "ILSpy");
+						break;
+				}
+			}
+			else if (sender is LanguageSettings)
+			{
+				switch (e.PropertyName)
+				{
+					case nameof(LanguageSettings.Language) or nameof(LanguageSettings.LanguageVersion):
+						DecompileSelectedNodes();
+						break;
+				}
+			}
+		}
+
+		public AssemblyList AssemblyList { get; private set; }
+
+		private SharpTreeNode root;
+		public SharpTreeNode Root {
+			get => root;
+			set => SetProperty(ref root, value);
+		}
+
+		private SharpTreeNode selectedItem;
+		public SharpTreeNode SelectedItem {
+			get => selectedItem;
+			set => SetProperty(ref selectedItem, value);
+		}
+
+		public ObservableCollection<SharpTreeNode> SelectedItems { get; } = [];
+
+		public string[] SelectedPath => GetPathForNode(SelectedItem);
+
+		readonly List<LoadedAssembly> commandLineLoadedAssemblies = [];
+
+		public bool HandleCommandLineArguments(CommandLineArguments args)
+		{
+			LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false);
+			if (args.Language != null)
+				SettingsService.Instance.SessionSettings.LanguageSettings.Language = Languages.GetLanguage(args.Language);
+			return true;
+		}
+
+		/// <summary>
+		/// Called on startup or when passed arguments via WndProc from a second instance.
+		/// In the format case, spySettings is non-null; in the latter it is null.
+		/// </summary>
+		public void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ISettingsProvider spySettings = null)
+		{
+			var sessionSettings = SettingsService.Instance.SessionSettings;
+
+			var relevantAssemblies = commandLineLoadedAssemblies.ToList();
+			commandLineLoadedAssemblies.Clear(); // clear references once we don't need them anymore
+			NavigateOnLaunch(args.NavigateTo, sessionSettings.ActiveTreeViewPath, spySettings, relevantAssemblies);
+			if (args.Search != null)
+			{
+				var searchPane = App.ExportProvider.GetExportedValue<SearchPaneModel>();
+
+				searchPane.SearchTerm = args.Search;
+				searchPane.Show();
+			}
+		}
+
+		public async Task HandleSingleInstanceCommandLineArguments(string[] args)
+		{
+			var cmdArgs = CommandLineArguments.Create(args);
+
+			await Dispatcher.InvokeAsync(() => {
+
+				if (!HandleCommandLineArguments(cmdArgs))
+					return;
+
+				var window = Application.Current.MainWindow;
+
+				if (!cmdArgs.NoActivate && window is { WindowState: WindowState.Minimized })
+				{
+					window.WindowState = WindowState.Normal;
+				}
+
+				HandleCommandLineArgumentsAfterShowList(cmdArgs);
+			});
+		}
+
+		public async void NavigateOnLaunch(string navigateTo, string[] activeTreeViewPath, ISettingsProvider spySettings, List<LoadedAssembly> relevantAssemblies)
+		{
+			var initialSelection = SelectedItem;
+			if (navigateTo != null)
+			{
+				bool found = false;
+				if (navigateTo.StartsWith("N:", StringComparison.Ordinal))
+				{
+					string namespaceName = navigateTo.Substring(2);
+					foreach (LoadedAssembly asm in relevantAssemblies)
+					{
+						AssemblyTreeNode asmNode = assemblyListTreeNode.FindAssemblyNode(asm);
+						if (asmNode != null)
+						{
+							// FindNamespaceNode() blocks the UI if the assembly is not yet loaded,
+							// so use an async wait instead.
+							await asm.GetMetadataFileAsync().Catch<Exception>(ex => { });
+							NamespaceTreeNode nsNode = asmNode.FindNamespaceNode(namespaceName);
+							if (nsNode != null)
+							{
+								found = true;
+								if (SelectedItem == initialSelection)
+								{
+									SelectNode(nsNode);
+								}
+								break;
+							}
+						}
+					}
+				}
+				else if (navigateTo == "none")
+				{
+					// Don't navigate anywhere; start empty.
+					// Used by ILSpy VS addin, it'll send us the real location to navigate to via IPC.
+					found = true;
+				}
+				else
+				{
+					IEntity mr = await Task.Run(() => FindEntityInRelevantAssemblies(navigateTo, relevantAssemblies));
+					// Make sure we wait for assemblies being loaded...
+					// BeginInvoke in LoadedAssembly.LookupReferencedAssemblyInternal
+					await Dispatcher.InvokeAsync(delegate { }, DispatcherPriority.Normal);
+					if (mr != null && mr.ParentModule?.MetadataFile != null)
+					{
+						found = true;
+						if (SelectedItem == initialSelection)
+						{
+							await JumpToReferenceAsync(mr);
+						}
+					}
+				}
+				if (!found && SelectedItem == initialSelection)
+				{
+					AvalonEditTextOutput output = new AvalonEditTextOutput();
+					output.Write($"Cannot find '{navigateTo}' in command line specified assemblies.");
+					DockWorkspace.Instance.ShowText(output);
+				}
+			}
+			else if (relevantAssemblies.Count == 1)
+			{
+				// NavigateTo == null and an assembly was given on the command-line:
+				// Select the newly loaded assembly
+				AssemblyTreeNode asmNode = assemblyListTreeNode.FindAssemblyNode(relevantAssemblies[0]);
+				if (asmNode != null && SelectedItem == initialSelection)
+				{
+					SelectNode(asmNode);
+				}
+			}
+			else if (spySettings != null)
+			{
+				SharpTreeNode node = null;
+				if (activeTreeViewPath?.Length > 0)
+				{
+					foreach (var asm in AssemblyList.GetAssemblies())
+					{
+						if (asm.FileName == activeTreeViewPath[0])
+						{
+							// FindNodeByPath() blocks the UI if the assembly is not yet loaded,
+							// so use an async wait instead.
+							await asm.GetMetadataFileAsync().Catch<Exception>(ex => { });
+						}
+					}
+					node = FindNodeByPath(activeTreeViewPath, true);
+				}
+				if (SelectedItem == initialSelection)
+				{
+					if (node != null)
+					{
+						SelectNode(node);
+
+						// only if not showing the about page, perform the update check:
+						await MainWindow.Instance.ShowMessageIfUpdatesAvailableAsync(spySettings);
+					}
+					else
+					{
+						DockWorkspace.Instance.ActiveTabPage.ShowTextView(AboutPage.Display);
+					}
+				}
+			}
+		}
+
+		public static IEntity FindEntityInRelevantAssemblies(string navigateTo, IEnumerable<LoadedAssembly> relevantAssemblies)
+		{
+			ITypeReference typeRef;
+			IMemberReference memberRef = null;
+			if (navigateTo.StartsWith("T:", StringComparison.Ordinal))
+			{
+				typeRef = IdStringProvider.ParseTypeName(navigateTo);
+			}
+			else
+			{
+				memberRef = IdStringProvider.ParseMemberIdString(navigateTo);
+				typeRef = memberRef.DeclaringTypeReference;
+			}
+			foreach (LoadedAssembly asm in relevantAssemblies.ToList())
+			{
+				var module = asm.GetMetadataFileOrNull();
+				if (CanResolveTypeInPEFile(module, typeRef, out var typeHandle))
+				{
+					ICompilation compilation = typeHandle.Kind == HandleKind.ExportedType
+						? new DecompilerTypeSystem(module, module.GetAssemblyResolver())
+						: new SimpleCompilation((PEFile)module, MinimalCorlib.Instance);
+					return memberRef == null
+						? typeRef.Resolve(new SimpleTypeResolveContext(compilation)) as ITypeDefinition
+						: memberRef.Resolve(new SimpleTypeResolveContext(compilation));
+				}
+			}
+			return null;
+		}
+
+		static bool CanResolveTypeInPEFile(MetadataFile module, ITypeReference typeRef, out EntityHandle typeHandle)
+		{
+			// We intentionally ignore reference assemblies, so that the loop continues looking for another assembly that might have a usable definition.
+			if (module.IsReferenceAssembly())
+			{
+				typeHandle = default;
+				return false;
+			}
+
+			switch (typeRef)
+			{
+				case GetPotentiallyNestedClassTypeReference topLevelType:
+					typeHandle = topLevelType.ResolveInPEFile(module);
+					return !typeHandle.IsNil;
+				case NestedTypeReference nestedType:
+					if (!CanResolveTypeInPEFile(module, nestedType.DeclaringTypeReference, out typeHandle))
+						return false;
+					if (typeHandle.Kind == HandleKind.ExportedType)
+						return true;
+					var typeDef = module.Metadata.GetTypeDefinition((TypeDefinitionHandle)typeHandle);
+					typeHandle = typeDef.GetNestedTypes().FirstOrDefault(t => {
+						var td = module.Metadata.GetTypeDefinition(t);
+						var typeName = ReflectionHelper.SplitTypeParameterCountFromReflectionName(module.Metadata.GetString(td.Name), out int typeParameterCount);
+						return nestedType.AdditionalTypeParameterCount == typeParameterCount && nestedType.Name == typeName;
+					});
+					return !typeHandle.IsNil;
+				default:
+					typeHandle = default;
+					return false;
+			}
+		}
+
+		public void Initialize()
+		{
+			this.AssemblyList = SettingsService.Instance.LoadInitialAssemblyList();
+
+			HandleCommandLineArguments(App.CommandLineArguments);
+
+			var loadPreviousAssemblies = SettingsService.Instance.MiscSettings.LoadPreviousAssemblies;
+			if (AssemblyList.GetAssemblies().Length == 0
+				&& AssemblyList.ListName == AssemblyListManager.DefaultListName
+				&& loadPreviousAssemblies)
+			{
+				LoadInitialAssemblies();
+			}
+
+			ShowAssemblyList(this.AssemblyList);
+
+			var sessionSettings = SettingsService.Instance.SessionSettings;
+			if (sessionSettings.ActiveAutoLoadedAssembly != null
+				&& File.Exists(sessionSettings.ActiveAutoLoadedAssembly))
+			{
+				this.AssemblyList.Open(sessionSettings.ActiveAutoLoadedAssembly, true);
+			}
+
+			Dispatcher.BeginInvoke(DispatcherPriority.Loaded, OpenAssemblies);
+		}
+
+		void OpenAssemblies()
+		{
+			HandleCommandLineArgumentsAfterShowList(App.CommandLineArguments, SettingsService.Instance.SpySettings);
+
+			AvalonEditTextOutput output = new();
+			if (FormatExceptions(App.StartupExceptions.ToArray(), output))
+				DockWorkspace.Instance.ShowText(output);
+		}
+
+		static bool FormatExceptions(App.ExceptionData[] exceptions, ITextOutput output)
+		{
+			var stringBuilder = new StringBuilder();
+			var result = exceptions.FormatExceptions(stringBuilder);
+			if (result)
+			{
+				output.Write(stringBuilder.ToString());
+			}
+			return result;
+		}
+
+		public void ShowAssemblyList(string name)
+		{
+			AssemblyList list = SettingsService.Instance.AssemblyListManager.LoadList(name);
+			//Only load a new list when it is a different one
+			if (list.ListName != AssemblyList.ListName)
+			{
+				ShowAssemblyList(list);
+				SelectNode(Root);
+			}
+		}
+
+		void ShowAssemblyList(AssemblyList assemblyList)
+		{
+			history.Clear();
+			if (this.AssemblyList != null)
+			{
+				this.AssemblyList.CollectionChanged -= assemblyList_CollectionChanged;
+			}
+
+			this.AssemblyList = assemblyList;
+
+			assemblyList.CollectionChanged += assemblyList_CollectionChanged;
+
+			assemblyListTreeNode = new(assemblyList) {
+				Select = x => SelectNode(x)
+			};
+
+			Root = assemblyListTreeNode;
+
+			if (assemblyList.ListName == AssemblyListManager.DefaultListName)
+#if DEBUG
+				this.Title = $"ILSpy {DecompilerVersionInfo.FullVersion}";
+#else
+				this.Title = "ILSpy";
+#endif
+			else
+#if DEBUG
+				this.Title = $"ILSpy {DecompilerVersionInfo.FullVersion} - " + assemblyList.ListName;
+#else
+				this.Title = "ILSpy - " + assemblyList.ListName;
+#endif
+		}
+
+		void assemblyList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+		{
+			if (e.Action == NotifyCollectionChangedAction.Reset)
+			{
+				history.RemoveAll(_ => true);
+			}
+			if (e.OldItems != null)
+			{
+				var oldAssemblies = new HashSet<LoadedAssembly>(e.OldItems.Cast<LoadedAssembly>());
+				history.RemoveAll(n => n.TreeNodes.Any(
+					nd => nd.AncestorsAndSelf().OfType<AssemblyTreeNode>().Any(
+						a => oldAssemblies.Contains(a.LoadedAssembly))));
+			}
+
+			MessageBus.Send(this, new CurrentAssemblyListChangedEventArgs(e));
+		}
+
+		void LoadInitialAssemblies()
+		{
+			// Called when loading an empty assembly list; so that
+			// the user can see something initially.
+			System.Reflection.Assembly[] initialAssemblies = {
+				typeof(object).Assembly,
+				typeof(Uri).Assembly,
+				typeof(System.Linq.Enumerable).Assembly,
+				typeof(System.Xml.XmlDocument).Assembly,
+				typeof(System.Windows.Markup.MarkupExtension).Assembly,
+				typeof(System.Windows.Rect).Assembly,
+				typeof(System.Windows.UIElement).Assembly,
+				typeof(System.Windows.FrameworkElement).Assembly
+			};
+			foreach (System.Reflection.Assembly asm in initialAssemblies)
+				AssemblyList.OpenAssembly(asm.Location);
+		}
+
+		public AssemblyTreeNode FindAssemblyNode(LoadedAssembly asm)
+		{
+			return assemblyListTreeNode.FindAssemblyNode(asm);
+		}
+
+		#region Node Selection
+
+		public void SelectNode(SharpTreeNode node, bool inNewTabPage = false)
+		{
+			if (node == null)
+				return;
+
+			if (node.AncestorsAndSelf().Any(item => item.IsHidden))
+			{
+				MessageBox.Show(Properties.Resources.NavigationFailed, "ILSpy", MessageBoxButton.OK, MessageBoxImage.Exclamation);
+				return;
+			}
+
+			if (inNewTabPage)
+			{
+				DockWorkspace.Instance.AddTabPage();
+				SelectedItem = null;
+			}
+
+			if (SelectedItem == node)
+			{
+				Dispatcher.BeginInvoke(RefreshDecompiledView);
+			}
+			else
+			{
+				activeView?.ScrollIntoView(node);
+				SelectedItem = node;
+			}
+		}
+
+		internal void SelectNodes(IEnumerable<SharpTreeNode> nodes)
+		{
+			// Ensure nodes exist
+			var nodesList = nodes.Select(n => FindNodeByPath(GetPathForNode(n), true))
+				.Where(n => n != null)
+				.ToArray();
+
+			if (!nodesList.Any() || nodesList.Any(n => n.AncestorsAndSelf().Any(a => a.IsHidden)))
+			{
+				return;
+			}
+
+			if (SelectedItems.SequenceEqual(nodesList))
+			{
+				Dispatcher.BeginInvoke(RefreshDecompiledView);
+				return;
+			}
+
+			SelectedItems.Clear();
+			SelectedItems.AddRange(nodesList);
+		}
+
+		/// <summary>
+		/// Retrieves a node using the .ToString() representations of its ancestors.
+		/// </summary>
+		public SharpTreeNode FindNodeByPath(string[] path, bool returnBestMatch)
+		{
+			if (path == null)
+				return null;
+			SharpTreeNode node = Root;
+			SharpTreeNode bestMatch = node;
+			foreach (var element in path)
+			{
+				if (node == null)
+					break;
+				bestMatch = node;
+				node.EnsureLazyChildren();
+				if (node is ILSpyTreeNode ilSpyTreeNode)
+					ilSpyTreeNode.EnsureChildrenFiltered();
+				node = node.Children.FirstOrDefault(c => c.ToString() == element);
+			}
+			if (returnBestMatch)
+				return node ?? bestMatch;
+			else
+				return node;
+		}
+
+		/// <summary>
+		/// Gets the .ToString() representation of the node's ancestors.
+		/// </summary>
+		public static string[] GetPathForNode(SharpTreeNode node)
+		{
+			if (node == null)
+				return null;
+			List<string> path = new List<string>();
+			while (node.Parent != null)
+			{
+				path.Add(node.ToString());
+				node = node.Parent;
+			}
+			path.Reverse();
+			return path.ToArray();
+		}
+
+		public ILSpyTreeNode FindTreeNode(object reference)
+		{
+			switch (reference)
+			{
+				case LoadedAssembly lasm:
+					return assemblyListTreeNode.FindAssemblyNode(lasm);
+				case MetadataFile asm:
+					return assemblyListTreeNode.FindAssemblyNode(asm);
+				case Resource res:
+					return assemblyListTreeNode.FindResourceNode(res);
+				case ValueTuple<Resource, string> resName:
+					return assemblyListTreeNode.FindResourceNode(resName.Item1, resName.Item2);
+				case ITypeDefinition type:
+					return assemblyListTreeNode.FindTypeNode(type);
+				case IField fd:
+					return assemblyListTreeNode.FindFieldNode(fd);
+				case IMethod md:
+					return assemblyListTreeNode.FindMethodNode(md);
+				case IProperty pd:
+					return assemblyListTreeNode.FindPropertyNode(pd);
+				case IEvent ed:
+					return assemblyListTreeNode.FindEventNode(ed);
+				case INamespace nd:
+					return assemblyListTreeNode.FindNamespaceNode(nd);
+				default:
+					return null;
+			}
+		}
+
+		private void JumpToReference(object sender, NavigateToReferenceEventArgs e)
+		{
+			JumpToReferenceAsync(e.Reference, e.InNewTabPage).HandleExceptions();
+		}
+
+		/// <summary>
+		/// Jumps to the specified reference.
+		/// </summary>
+		/// <returns>
+		/// Returns a task that will signal completion when the decompilation of the jump target has finished.
+		/// The task will be marked as canceled if the decompilation is canceled.
+		/// </returns>
+		private Task JumpToReferenceAsync(object reference, bool inNewTabPage = false)
+		{
+			var decompilationTask = Task.CompletedTask;
+
+			switch (reference)
+			{
+				case Decompiler.Disassembler.OpCodeInfo opCode:
+					MainWindow.OpenLink(opCode.Link);
+					break;
+				case EntityReference unresolvedEntity:
+					string protocol = unresolvedEntity.Protocol;
+					var file = unresolvedEntity.ResolveAssembly(AssemblyList);
+					if (file == null)
+					{
+						break;
+					}
+					if (protocol != "decompile")
+					{
+						var protocolHandlers = App.ExportProvider.GetExportedValues<IProtocolHandler>();
+						foreach (var handler in protocolHandlers)
+						{
+							var node = handler.Resolve(protocol, file, unresolvedEntity.Handle, out bool newTabPage);
+							if (node != null)
+							{
+								SelectNode(node, newTabPage || inNewTabPage);
+								return decompilationTask;
+							}
+						}
+					}
+					var possibleToken = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolvedEntity.Handle));
+					if (possibleToken != null)
+					{
+						var typeSystem = new DecompilerTypeSystem(file, file.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached);
+						reference = typeSystem.MainModule.ResolveEntity(possibleToken.Value);
+						goto default;
+					}
+					break;
+				default:
+					ILSpyTreeNode treeNode = FindTreeNode(reference);
+					if (treeNode != null)
+						SelectNode(treeNode, inNewTabPage);
+					break;
+			}
+			return decompilationTask;
+		}
+
+		#endregion
+
+		public void LoadAssemblies(IEnumerable<string> fileNames, List<LoadedAssembly> loadedAssemblies = null, bool focusNode = true)
+		{
+			using (Keyboard.FocusedElement.PreserveFocus(!focusNode))
+			{
+				AssemblyTreeNode lastNode = null;
+
+				foreach (string file in fileNames)
+				{
+					var assembly = AssemblyList.OpenAssembly(file);
+
+					if (loadedAssemblies != null)
+					{
+						loadedAssemblies.Add(assembly);
+					}
+					else
+					{
+						var node = assemblyListTreeNode.FindAssemblyNode(assembly);
+						if (node != null && focusNode)
+						{
+							lastNode = node;
+							SelectedItems.Add(node);
+						}
+					}
+				}
+				if (focusNode && lastNode != null)
+				{
+					activeView?.FocusNode(lastNode);
+				}
+			}
+		}
+
+		#region Decompile (TreeView_SelectionChanged)
+
+		void TreeView_SelectionChanged()
+		{
+			if (SelectedItems.Count > 0)
+			{
+				if (!isNavigatingHistory)
+				{
+					var activeTabPage = DockWorkspace.Instance.ActiveTabPage;
+					var currentState = activeTabPage.GetState();
+					if (currentState != null)
+						history.UpdateCurrent(new NavigationState(activeTabPage, currentState));
+					history.Record(new NavigationState(activeTabPage, SelectedItems));
+				}
+
+				var delayDecompilationRequestDueToContextMenu = Mouse.RightButton == MouseButtonState.Pressed;
+
+				if (!delayDecompilationRequestDueToContextMenu)
+				{
+					DecompileSelectedNodes();
+				}
+				else
+				{
+					ContextMenuProvider.ContextMenuClosed += ContextMenuClosed;
+				}
+			}
+
+			MessageBus.Send(this, new AssemblyTreeSelectionChangedEventArgs());
+
+			return;
+
+			void ContextMenuClosed(object sender, EventArgs e)
+			{
+				ContextMenuProvider.ContextMenuClosed -= ContextMenuClosed;
+
+				Dispatcher.BeginInvoke(DispatcherPriority.Background, () => {
+					if (Mouse.RightButton != MouseButtonState.Pressed)
+					{
+						DecompileSelectedNodes(DockWorkspace.Instance.ActiveTabPage.GetState() as DecompilerTextViewState);
+					}
+				});
+			}
+		}
+
+		private void DecompileSelectedNodes(DecompilerTextViewState newState = null)
+		{
+			var activeTabPage = DockWorkspace.Instance.ActiveTabPage;
+
+			activeTabPage.SupportsLanguageSwitching = true;
+
+			if (SelectedItems.Count == 1)
+			{
+				if (SelectedItem is ILSpyTreeNode node && node.View(activeTabPage))
+					return;
+			}
+			if (newState?.ViewedUri != null)
+			{
+				NavigateTo(new(newState.ViewedUri, null), recordHistory: false);
+				return;
+			}
+
+			var options = SettingsService.Instance.CreateDecompilationOptions(activeTabPage);
+			options.TextViewState = newState;
+			activeTabPage.ShowTextViewAsync(textView => textView.DecompileAsync(this.CurrentLanguage, this.SelectedNodes, options));
+		}
+
+		public void RefreshDecompiledView()
+		{
+			DecompileSelectedNodes();
+		}
+
+		public Language CurrentLanguage => SettingsService.Instance.SessionSettings.LanguageSettings.Language;
+
+		public LanguageVersion CurrentLanguageVersion => SettingsService.Instance.SessionSettings.LanguageSettings.LanguageVersion;
+
+		public IEnumerable<ILSpyTreeNode> SelectedNodes {
+			get {
+				return GetTopLevelSelection().OfType<ILSpyTreeNode>();
+			}
+		}
+
+		#endregion
+
+		public void NavigateHistory(bool forward)
+		{
+			isNavigatingHistory = true;
+			this.Dispatcher.BeginInvoke(DispatcherPriority.Background, () => isNavigatingHistory = false);
+
+			TabPageModel tabPage = DockWorkspace.Instance.ActiveTabPage;
+			var state = tabPage.GetState();
+			if (state != null)
+				history.UpdateCurrent(new NavigationState(tabPage, state));
+			var newState = forward ? history.GoForward() : history.GoBack();
+
+			DockWorkspace.Instance.ActiveTabPage = newState.TabPage;
+
+			SelectNodes(newState.TreeNodes);
+		}
+
+		public bool CanNavigateBack => history.CanNavigateBack;
+
+		public bool CanNavigateForward => history.CanNavigateForward;
+
+		internal void NavigateTo(RequestNavigateEventArgs e, bool recordHistory = true, bool inNewTabPage = false)
+		{
+			if (e.Uri.Scheme == "resource")
+			{
+				if (inNewTabPage)
+				{
+					DockWorkspace.Instance.AddTabPage();
+				}
+
+				if (e.Uri.Host == "aboutpage")
+				{
+					RecordHistory();
+					DockWorkspace.Instance.ActiveTabPage.ShowTextView(AboutPage.Display);
+					e.Handled = true;
+					return;
+				}
+
+				AvalonEditTextOutput output = new AvalonEditTextOutput {
+					Address = e.Uri,
+					Title = e.Uri.AbsolutePath,
+					EnableHyperlinks = true
+				};
+				using (Stream s = typeof(App).Assembly.GetManifestResourceStream(typeof(App), e.Uri.AbsolutePath))
+				{
+					using (StreamReader r = new StreamReader(s))
+					{
+						string line;
+						while ((line = r.ReadLine()) != null)
+						{
+							output.Write(line);
+							output.WriteLine();
+						}
+					}
+				}
+				RecordHistory();
+				DockWorkspace.Instance.ShowText(output);
+				e.Handled = true;
+			}
+
+			void RecordHistory()
+			{
+				if (!recordHistory)
+					return;
+				TabPageModel tabPage = DockWorkspace.Instance.ActiveTabPage;
+				var currentState = tabPage.GetState();
+				if (currentState != null)
+					history.UpdateCurrent(new NavigationState(tabPage, currentState));
+
+				UnselectAll();
+
+				history.Record(new NavigationState(tabPage, new ViewState { ViewedUri = e.Uri }));
+			}
+		}
+
+		public void Refresh()
+		{
+			using (Keyboard.FocusedElement.PreserveFocus())
+			{
+				var path = GetPathForNode(SelectedItem);
+				ShowAssemblyList(SettingsService.Instance.AssemblyListManager.LoadList(AssemblyList.ListName));
+				SelectNode(FindNodeByPath(path, true), inNewTabPage: false);
+			}
+		}
+
+		private void UnselectAll()
+		{
+			SelectedItems.Clear();
+		}
+
+		public IEnumerable<SharpTreeNode> GetTopLevelSelection()
+		{
+			var selection = this.SelectedItems;
+			var selectionHash = new HashSet<SharpTreeNode>(selection);
+
+			return selection.Where(item => item.Ancestors().All(a => !selectionHash.Contains(a)));
+		}
+
+		public void SetActiveView(AssemblyListPane activeView)
+		{
+			this.activeView = activeView;
+		}
+
+		public void SortAssemblyList()
+		{
+			using (activeView?.LockUpdates())
+			{
+				AssemblyList.Sort(AssemblyComparer.Instance);
+			}
+		}
+
+		private class AssemblyComparer : IComparer<LoadedAssembly>
+		{
+			public static readonly AssemblyComparer Instance = new();
+			int IComparer<LoadedAssembly>.Compare(LoadedAssembly x, LoadedAssembly y)
+			{
+				return string.Compare(x?.ShortName, y?.ShortName, StringComparison.CurrentCulture);
+			}
+		}
+
+		public void CollapseAll()
+		{
+			using (activeView.LockUpdates())
+			{
+				CollapseChildren(Root);
+			}
+		}
+
+		static void CollapseChildren(SharpTreeNode node)
+		{
+			foreach (var child in node.Children)
+			{
+				if (!child.IsExpanded)
+					continue;
+
+				CollapseChildren(child);
+				child.IsExpanded = false;
+			}
+		}
+
+		public void OpenFiles(string[] fileNames, bool focusNode = true)
+		{
+			if (fileNames == null)
+				throw new ArgumentNullException(nameof(fileNames));
+
+			if (focusNode)
+				UnselectAll();
+
+			LoadAssemblies(fileNames, focusNode: focusNode);
+		}
+	}
+}
diff --git a/ILSpy/Commands/BrowseBackCommand.cs b/ILSpy/Commands/BrowseBackCommand.cs
index cf424cbd4..ae53e19ef 100644
--- a/ILSpy/Commands/BrowseBackCommand.cs
+++ b/ILSpy/Commands/BrowseBackCommand.cs
@@ -19,6 +19,7 @@
 using System.ComponentModel.Composition;
 using System.Windows.Input;
 
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Properties;
 
 namespace ICSharpCode.ILSpy
@@ -27,9 +28,30 @@ namespace ICSharpCode.ILSpy
 	[PartCreationPolicy(CreationPolicy.Shared)]
 	sealed class BrowseBackCommand : CommandWrapper
 	{
-		public BrowseBackCommand()
+		readonly AssemblyTreeModel assemblyTreeModel;
+
+		[ImportingConstructor]
+		public BrowseBackCommand(AssemblyTreeModel assemblyTreeModel)
 			: base(NavigationCommands.BrowseBack)
 		{
+			this.assemblyTreeModel = assemblyTreeModel;
+		}
+
+		protected override void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
+		{
+			base.OnCanExecute(sender, e);
+
+			e.Handled = true;
+			e.CanExecute = assemblyTreeModel.CanNavigateBack;
+		}
+
+		protected override void OnExecute(object sender, ExecutedRoutedEventArgs e)
+		{
+			if (assemblyTreeModel.CanNavigateBack)
+			{
+				e.Handled = true;
+				assemblyTreeModel.NavigateHistory(false);
+			}
 		}
 	}
 }
diff --git a/ILSpy/Commands/BrowseForwardCommand.cs b/ILSpy/Commands/BrowseForwardCommand.cs
index 3a8276457..ea86ebc11 100644
--- a/ILSpy/Commands/BrowseForwardCommand.cs
+++ b/ILSpy/Commands/BrowseForwardCommand.cs
@@ -19,6 +19,7 @@
 using System.ComponentModel.Composition;
 using System.Windows.Input;
 
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Properties;
 
 namespace ICSharpCode.ILSpy
@@ -27,9 +28,31 @@ namespace ICSharpCode.ILSpy
 	[PartCreationPolicy(CreationPolicy.Shared)]
 	sealed class BrowseForwardCommand : CommandWrapper
 	{
-		public BrowseForwardCommand()
+		private readonly AssemblyTreeModel assemblyTreeModel;
+
+		[ImportingConstructor]
+		public BrowseForwardCommand(AssemblyTreeModel assemblyTreeModel)
 			: base(NavigationCommands.BrowseForward)
 		{
+			this.assemblyTreeModel = assemblyTreeModel;
+		}
+
+		protected override void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
+		{
+			base.OnCanExecute(sender, e);
+
+			e.Handled = true;
+			e.CanExecute = assemblyTreeModel.CanNavigateForward;
 		}
+
+		protected override void OnExecute(object sender, ExecutedRoutedEventArgs e)
+		{
+			if (assemblyTreeModel.CanNavigateForward)
+			{
+				e.Handled = true;
+				assemblyTreeModel.NavigateHistory(true);
+			}
+		}
+
 	}
 }
diff --git a/ILSpy/Commands/CheckForUpdatesCommand.cs b/ILSpy/Commands/CheckForUpdatesCommand.cs
index c5d1ee0bd..f783933dd 100644
--- a/ILSpy/Commands/CheckForUpdatesCommand.cs
+++ b/ILSpy/Commands/CheckForUpdatesCommand.cs
@@ -20,7 +20,6 @@
 using System.ComponentModel.Composition;
 
 using ICSharpCode.ILSpy.Properties;
-using ICSharpCode.ILSpyX.Settings;
 
 namespace ICSharpCode.ILSpy
 {
@@ -35,7 +34,7 @@ namespace ICSharpCode.ILSpy
 
 		public override async void Execute(object? parameter)
 		{
-			await MainWindow.Instance.ShowMessageIfUpdatesAvailableAsync(ILSpySettings.Load(), forceCheck: true);
+			await MainWindow.Instance.ShowMessageIfUpdatesAvailableAsync(SettingsService.Instance.SpySettings, forceCheck: true);
 		}
 	}
 }
diff --git a/ILSpy/Commands/CommandWrapper.cs b/ILSpy/Commands/CommandWrapper.cs
index 30bd37599..ad68ac75b 100644
--- a/ILSpy/Commands/CommandWrapper.cs
+++ b/ILSpy/Commands/CommandWrapper.cs
@@ -17,26 +17,28 @@
 // DEALINGS IN THE SOFTWARE.
 
 using System;
+using System.Windows;
 using System.Windows.Input;
 
 namespace ICSharpCode.ILSpy
 {
-	class CommandWrapper : ICommand
+	abstract class CommandWrapper : ICommand
 	{
 		private readonly ICommand wrappedCommand;
 
-		public CommandWrapper(ICommand wrappedCommand)
+		protected CommandWrapper(ICommand wrappedCommand)
 		{
 			this.wrappedCommand = wrappedCommand;
+
+			Application.Current.MainWindow?.CommandBindings.Add(new CommandBinding(wrappedCommand, OnExecute, OnCanExecute));
 		}
 
 		public static ICommand Unwrap(ICommand command)
 		{
-			CommandWrapper? w = command as CommandWrapper;
-			if (w != null)
+			if (command is CommandWrapper w)
 				return w.wrappedCommand;
-			else
-				return command;
+
+			return command;
 		}
 
 		public event EventHandler? CanExecuteChanged {
@@ -53,5 +55,12 @@ namespace ICSharpCode.ILSpy
 		{
 			return wrappedCommand.CanExecute(parameter);
 		}
+
+		protected abstract void OnExecute(object sender, ExecutedRoutedEventArgs e);
+
+		protected virtual void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
+		{
+			e.CanExecute = true;
+		}
 	}
 }
diff --git a/ILSpy/Commands/DecompileAllCommand.cs b/ILSpy/Commands/DecompileAllCommand.cs
index b86693760..4f147b06b 100644
--- a/ILSpy/Commands/DecompileAllCommand.cs
+++ b/ILSpy/Commands/DecompileAllCommand.cs
@@ -26,9 +26,9 @@ using System.Linq;
 using System.Threading.Tasks;
 
 using ICSharpCode.Decompiler;
+using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.Properties;
 using ICSharpCode.ILSpy.TextView;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 
 using TomsToolbox.Essentials;
@@ -49,7 +49,7 @@ namespace ICSharpCode.ILSpy
 			Docking.DockWorkspace.Instance.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
 				AvalonEditTextOutput output = new AvalonEditTextOutput();
 				Parallel.ForEach(
-					Partitioner.Create(MainWindow.Instance.CurrentAssemblyList.GetAssemblies(), loadBalance: true),
+					Partitioner.Create(MainWindow.Instance.AssemblyTreeModel.AssemblyList.GetAssemblies(), loadBalance: true),
 					new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = ct },
 					delegate (LoadedAssembly asm) {
 						if (!asm.HasLoadError)
@@ -60,7 +60,7 @@ namespace ICSharpCode.ILSpy
 							{
 								try
 								{
-									var options = MainWindow.Instance.CreateDecompilationOptions();
+									var options = SettingsService.Instance.CreateDecompilationOptions(DockWorkspace.Instance.ActiveTabPage);
 									options.CancellationToken = ct;
 									options.FullDecompilation = true;
 									new CSharpLanguage().DecompileAssembly(asm, new PlainTextOutput(writer), options);
@@ -96,9 +96,10 @@ namespace ICSharpCode.ILSpy
 		{
 			const int numRuns = 100;
 			var language = SettingsService.Instance.SessionSettings.LanguageSettings.Language;
-			var nodes = MainWindow.Instance.SelectedNodes.ToArray();
-			var options = MainWindow.Instance.CreateDecompilationOptions();
-			Docking.DockWorkspace.Instance.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
+			var nodes = MainWindow.Instance.AssemblyTreeModel.SelectedNodes.ToArray();
+			DockWorkspace dockWorkspace = DockWorkspace.Instance;
+			var options = SettingsService.Instance.CreateDecompilationOptions(dockWorkspace.ActiveTabPage);
+			dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
 				options.CancellationToken = ct;
 				Stopwatch w = Stopwatch.StartNew();
 				for (int i = 0; i < numRuns; ++i)
@@ -113,7 +114,7 @@ namespace ICSharpCode.ILSpy
 				double msPerRun = w.Elapsed.TotalMilliseconds / numRuns;
 				output.Write($"Average time: {msPerRun.ToString("f1")}ms\n");
 				return output;
-			}, ct)).Then(output => Docking.DockWorkspace.Instance.ShowText(output)).HandleExceptions();
+			}, ct)).Then(output => dockWorkspace.ShowText(output)).HandleExceptions();
 		}
 	}
 }
diff --git a/ILSpy/Commands/DecompileCommand.cs b/ILSpy/Commands/DecompileCommand.cs
index 07ce981fb..152e7cd52 100644
--- a/ILSpy/Commands/DecompileCommand.cs
+++ b/ILSpy/Commands/DecompileCommand.cs
@@ -66,7 +66,7 @@ namespace ICSharpCode.ILSpy.Commands
 				selection = entity;
 			}
 			if (selection != null)
-				MainWindow.Instance.JumpToReference(selection);
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(selection));
 		}
 	}
 }
diff --git a/ILSpy/Commands/DecompileInNewViewCommand.cs b/ILSpy/Commands/DecompileInNewViewCommand.cs
index 07e9bebcb..fd83244eb 100644
--- a/ILSpy/Commands/DecompileInNewViewCommand.cs
+++ b/ILSpy/Commands/DecompileInNewViewCommand.cs
@@ -25,9 +25,10 @@ using System.Windows.Threading;
 using ICSharpCode.Decompiler.TypeSystem;
 using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.Properties;
-using ICSharpCode.ILSpy.TextView;
 using ICSharpCode.ILSpy.TreeNodes;
 
+using TomsToolbox.Essentials;
+
 namespace ICSharpCode.ILSpy.Commands
 {
 	[ExportContextMenuEntry(Header = nameof(Resources.DecompileToNewPanel), InputGestureText = "MMB", Icon = "images/Search", Category = nameof(Resources.Analyze), Order = 90)]
@@ -53,18 +54,18 @@ namespace ICSharpCode.ILSpy.Commands
 		{
 			if (context.SelectedTreeNodes != null)
 			{
-				if (context.TreeView != MainWindow.Instance.AssemblyTreeView)
+				if (context.TreeView.DataContext != MainWindow.Instance.AssemblyTreeModel)
 				{
-					return context.SelectedTreeNodes.OfType<IMemberTreeNode>().Select(FindTreeNode).Where(n => n != null);
+					return context.SelectedTreeNodes.OfType<IMemberTreeNode>().Select(FindTreeNode).ExceptNullItems();
 				}
 				else
 				{
-					return context.SelectedTreeNodes.OfType<ILSpyTreeNode>().Where(n => n != null);
+					return context.SelectedTreeNodes.OfType<ILSpyTreeNode>();
 				}
 			}
 			else if (context.Reference?.Reference is IEntity entity)
 			{
-				if (MainWindow.Instance.FindTreeNode(entity) is ILSpyTreeNode node)
+				if (MainWindow.Instance.AssemblyTreeModel.FindTreeNode(entity) is { } node)
 				{
 					return new[] { node };
 				}
@@ -75,7 +76,7 @@ namespace ICSharpCode.ILSpy.Commands
 			{
 				if (node is ILSpyTreeNode ilspyNode)
 					return ilspyNode;
-				return MainWindow.Instance.FindTreeNode(node.Member);
+				return MainWindow.Instance.AssemblyTreeModel.FindTreeNode(node.Member);
 			}
 		}
 
@@ -84,8 +85,9 @@ namespace ICSharpCode.ILSpy.Commands
 			if (nodes.Length == 0)
 				return;
 
-			MainWindow.Instance.SelectNodes(nodes, inNewTabPage: true);
-			MainWindow.Instance.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)MainWindow.Instance.RefreshDecompiledView);
+			DockWorkspace.Instance.AddTabPage();
+
+			MainWindow.Instance.AssemblyTreeModel.SelectNodes(nodes);
 		}
 	}
 }
diff --git a/ILSpy/Commands/DisassembleAllCommand.cs b/ILSpy/Commands/DisassembleAllCommand.cs
index 8907dba99..1686f4bcc 100644
--- a/ILSpy/Commands/DisassembleAllCommand.cs
+++ b/ILSpy/Commands/DisassembleAllCommand.cs
@@ -26,7 +26,6 @@ using System.Threading.Tasks;
 
 using ICSharpCode.ILSpy.Properties;
 using ICSharpCode.ILSpy.TextView;
-using ICSharpCode.ILSpyX;
 
 namespace ICSharpCode.ILSpy
 {
@@ -41,12 +40,14 @@ namespace ICSharpCode.ILSpy
 
 		public override void Execute(object? parameter)
 		{
-			Docking.DockWorkspace.Instance.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
-				AvalonEditTextOutput output = new AvalonEditTextOutput();
+			var dockWorkspace = Docking.DockWorkspace.Instance;
+
+			dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
+				AvalonEditTextOutput output = new();
 				Parallel.ForEach(
-					Partitioner.Create(MainWindow.Instance.CurrentAssemblyList.GetAssemblies(), loadBalance: true),
-					new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = ct },
-					delegate (LoadedAssembly asm) {
+					Partitioner.Create(MainWindow.Instance.AssemblyTreeModel.AssemblyList.GetAssemblies(), loadBalance: true),
+					new() { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = ct },
+					asm => {
 						if (!asm.HasLoadError)
 						{
 							Stopwatch w = Stopwatch.StartNew();
@@ -55,7 +56,7 @@ namespace ICSharpCode.ILSpy
 							{
 								try
 								{
-									var options = MainWindow.Instance.CreateDecompilationOptions();
+									var options = SettingsService.Instance.CreateDecompilationOptions(dockWorkspace.ActiveTabPage);
 									options.FullDecompilation = true;
 									options.CancellationToken = ct;
 									new ILLanguage().DecompileAssembly(asm, new Decompiler.PlainTextOutput(writer), options);
@@ -79,7 +80,7 @@ namespace ICSharpCode.ILSpy
 						}
 					});
 				return output;
-			}, ct)).Then(output => Docking.DockWorkspace.Instance.ShowText(output)).HandleExceptions();
+			}, ct)).Then(dockWorkspace.ShowText).HandleExceptions();
 		}
 	}
 }
diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs
index 245fc7663..1c82faf12 100644
--- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs
+++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs
@@ -29,6 +29,7 @@ using ICSharpCode.Decompiler.CSharp;
 using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
 using ICSharpCode.Decompiler.DebugInfo;
 using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.Properties;
 using ICSharpCode.ILSpy.TextView;
 using ICSharpCode.ILSpy.TreeNodes;
@@ -73,9 +74,10 @@ namespace ICSharpCode.ILSpy
 			dlg.InitialDirectory = Path.GetDirectoryName(assembly.FileName);
 			if (dlg.ShowDialog() != true)
 				return;
-			DecompilationOptions options = MainWindow.Instance.CreateDecompilationOptions();
+			DockWorkspace dockWorkspace = DockWorkspace.Instance;
+			DecompilationOptions options = SettingsService.Instance.CreateDecompilationOptions(dockWorkspace.ActiveTabPage);
 			string fileName = dlg.FileName;
-			Docking.DockWorkspace.Instance.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
+			dockWorkspace.RunWithCancellation(ct => Task<AvalonEditTextOutput>.Factory.StartNew(() => {
 				AvalonEditTextOutput output = new AvalonEditTextOutput();
 				Stopwatch stopwatch = Stopwatch.StartNew();
 				options.CancellationToken = ct;
@@ -110,14 +112,14 @@ namespace ICSharpCode.ILSpy
 	{
 		public override bool CanExecute(object? parameter)
 		{
-			return MainWindow.Instance.SelectedNodes?.Count() == 1
-				&& MainWindow.Instance.SelectedNodes?.FirstOrDefault() is AssemblyTreeNode tn
+			return MainWindow.Instance.AssemblyTreeModel.SelectedNodes?.Count() == 1
+				&& MainWindow.Instance.AssemblyTreeModel.SelectedNodes?.FirstOrDefault() is AssemblyTreeNode tn
 				&& !tn.LoadedAssembly.HasLoadError;
 		}
 
 		public override void Execute(object? parameter)
 		{
-			var assembly = (MainWindow.Instance.SelectedNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly;
+			var assembly = (MainWindow.Instance.AssemblyTreeModel.SelectedNodes?.FirstOrDefault() as AssemblyTreeNode)?.LoadedAssembly;
 			if (assembly == null)
 				return;
 			GeneratePdbContextMenuEntry.GeneratePdbForAssembly(assembly);
diff --git a/ILSpy/Commands/OpenCommand.cs b/ILSpy/Commands/OpenCommand.cs
index 7c66ac43a..06ac419e9 100644
--- a/ILSpy/Commands/OpenCommand.cs
+++ b/ILSpy/Commands/OpenCommand.cs
@@ -19,8 +19,11 @@
 using System.ComponentModel.Composition;
 using System.Windows.Input;
 
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Properties;
 
+using Microsoft.Win32;
+
 namespace ICSharpCode.ILSpy
 {
 	[ExportToolbarCommand(ToolTip = nameof(Resources.Open), ToolbarIcon = "Images/Open", ToolbarCategory = nameof(Resources.Open), ToolbarOrder = 0)]
@@ -28,9 +31,28 @@ namespace ICSharpCode.ILSpy
 	[PartCreationPolicy(CreationPolicy.Shared)]
 	sealed class OpenCommand : CommandWrapper
 	{
-		public OpenCommand()
+		private readonly AssemblyTreeModel assemblyTreeModel;
+
+		[ImportingConstructor]
+		public OpenCommand(AssemblyTreeModel assemblyTreeModel)
 			: base(ApplicationCommands.Open)
 		{
+			this.assemblyTreeModel = assemblyTreeModel;
+		}
+
+		protected override void OnExecute(object sender, ExecutedRoutedEventArgs e)
+		{
+			e.Handled = true;
+			OpenFileDialog dlg = new OpenFileDialog {
+				Filter = ".NET assemblies|*.dll;*.exe;*.winmd;*.wasm|Nuget Packages (*.nupkg)|*.nupkg|Portable Program Database (*.pdb)|*.pdb|All files|*.*",
+				Multiselect = true,
+				RestoreDirectory = true
+			};
+
+			if (dlg.ShowDialog() == true)
+			{
+				assemblyTreeModel.OpenFiles(dlg.FileNames);
+			}
 		}
 	}
 }
diff --git a/ILSpy/Commands/OpenFromGacCommand.cs b/ILSpy/Commands/OpenFromGacCommand.cs
index a25fa14c0..991e28fe5 100644
--- a/ILSpy/Commands/OpenFromGacCommand.cs
+++ b/ILSpy/Commands/OpenFromGacCommand.cs
@@ -19,6 +19,7 @@
 using System.ComponentModel.Composition;
 
 using ICSharpCode.ILSpy.AppEnv;
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Properties;
 
 namespace ICSharpCode.ILSpy
@@ -27,6 +28,14 @@ namespace ICSharpCode.ILSpy
 	[PartCreationPolicy(CreationPolicy.Shared)]
 	sealed class OpenFromGacCommand : SimpleCommand
 	{
+		private readonly AssemblyTreeModel assemblyTreeModel;
+
+		[ImportingConstructor]
+		public OpenFromGacCommand(AssemblyTreeModel assemblyTreeModel)
+		{
+			this.assemblyTreeModel = assemblyTreeModel;
+		}
+
 		public override bool CanExecute(object? parameter)
 		{
 			return AppEnvironment.IsWindows;
@@ -34,10 +43,14 @@ namespace ICSharpCode.ILSpy
 
 		public override void Execute(object? parameter)
 		{
-			OpenFromGacDialog dlg = new OpenFromGacDialog();
-			dlg.Owner = MainWindow.Instance;
+			OpenFromGacDialog dlg = new OpenFromGacDialog {
+				Owner = MainWindow.Instance
+			};
+
 			if (dlg.ShowDialog() == true)
-				MainWindow.Instance.OpenFiles(dlg.SelectedFileNames);
+			{
+				assemblyTreeModel.OpenFiles(dlg.SelectedFileNames);
+			}
 		}
 	}
 }
diff --git a/ILSpy/Commands/Pdb2XmlCommand.cs b/ILSpy/Commands/Pdb2XmlCommand.cs
index 2ddf9b384..10873c58a 100644
--- a/ILSpy/Commands/Pdb2XmlCommand.cs
+++ b/ILSpy/Commands/Pdb2XmlCommand.cs
@@ -40,14 +40,14 @@ namespace ICSharpCode.ILSpy
 	{
 		public override bool CanExecute(object? parameter)
 		{
-			var selectedNodes = MainWindow.Instance.SelectedNodes;
+			var selectedNodes = MainWindow.Instance.AssemblyTreeModel.SelectedNodes;
 			return selectedNodes?.Any() == true
 				&& selectedNodes.All(n => n is AssemblyTreeNode asm && !asm.LoadedAssembly.HasLoadError);
 		}
 
 		public override void Execute(object? parameter)
 		{
-			Execute(MainWindow.Instance.SelectedNodes.OfType<AssemblyTreeNode>());
+			Execute(MainWindow.Instance.AssemblyTreeModel.SelectedNodes.OfType<AssemblyTreeNode>());
 		}
 
 		internal static void Execute(IEnumerable<AssemblyTreeNode> nodes)
diff --git a/ILSpy/Commands/RefreshCommand.cs b/ILSpy/Commands/RefreshCommand.cs
index cfb03bf34..e01e70f0e 100644
--- a/ILSpy/Commands/RefreshCommand.cs
+++ b/ILSpy/Commands/RefreshCommand.cs
@@ -19,6 +19,7 @@
 using System.ComponentModel.Composition;
 using System.Windows.Input;
 
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Properties;
 
 namespace ICSharpCode.ILSpy
@@ -28,9 +29,18 @@ namespace ICSharpCode.ILSpy
 	[PartCreationPolicy(CreationPolicy.Shared)]
 	sealed class RefreshCommand : CommandWrapper
 	{
-		public RefreshCommand()
+		private readonly AssemblyTreeModel assemblyTreeModel;
+
+		[ImportingConstructor]
+		public RefreshCommand(AssemblyTreeModel assemblyTreeModel)
 			: base(NavigationCommands.Refresh)
 		{
+			this.assemblyTreeModel = assemblyTreeModel;
+		}
+
+		protected override void OnExecute(object sender, ExecutedRoutedEventArgs e)
+		{
+			assemblyTreeModel.Refresh();
 		}
 	}
 }
diff --git a/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs b/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs
index 282c99098..f5e4b93d5 100644
--- a/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs
+++ b/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs
@@ -29,16 +29,16 @@ namespace ICSharpCode.ILSpy
 	{
 		public override bool CanExecute(object? parameter)
 		{
-			return MainWindow.Instance.CurrentAssemblyList?.GetAssemblies().Any(l => l.HasLoadError) == true;
+			return MainWindow.Instance.AssemblyTreeModel.AssemblyList?.GetAssemblies().Any(l => l.HasLoadError) == true;
 		}
 
 		public override void Execute(object? parameter)
 		{
-			foreach (var asm in MainWindow.Instance.CurrentAssemblyList.GetAssemblies())
+			foreach (var asm in MainWindow.Instance.AssemblyTreeModel.AssemblyList.GetAssemblies())
 			{
 				if (!asm.HasLoadError)
 					continue;
-				var node = MainWindow.Instance.AssemblyListTreeNode.FindAssemblyNode(asm);
+				var node = MainWindow.Instance.AssemblyTreeModel.FindAssemblyNode(asm);
 				if (node != null && node.CanDelete())
 					node.Delete();
 			}
@@ -51,12 +51,12 @@ namespace ICSharpCode.ILSpy
 	{
 		public override bool CanExecute(object? parameter)
 		{
-			return MainWindow.Instance.CurrentAssemblyList?.Count > 0;
+			return MainWindow.Instance.AssemblyTreeModel.AssemblyList?.Count > 0;
 		}
 
 		public override void Execute(object? parameter)
 		{
-			MainWindow.Instance.CurrentAssemblyList?.Clear();
+			MainWindow.Instance.AssemblyTreeModel.AssemblyList?.Clear();
 		}
 	}
 }
diff --git a/ILSpy/Commands/SaveCodeContextMenuEntry.cs b/ILSpy/Commands/SaveCodeContextMenuEntry.cs
index d68ad1c7f..5759c3abd 100644
--- a/ILSpy/Commands/SaveCodeContextMenuEntry.cs
+++ b/ILSpy/Commands/SaveCodeContextMenuEntry.cs
@@ -33,6 +33,9 @@ using ICSharpCode.ILSpyX.TreeView;
 
 using Microsoft.Win32;
 
+using ICSharpCode.ILSpy.Docking;
+
+
 namespace ICSharpCode.ILSpy.TextView
 {
 	[ExportContextMenuEntry(Header = nameof(Resources._SaveCode), Category = nameof(Resources.Save), Icon = "Images/Save")]
@@ -61,8 +64,11 @@ namespace ICSharpCode.ILSpy.TextView
 
 		public static void Execute(IReadOnlyList<SharpTreeNode> selectedNodes)
 		{
-			var currentLanguage = SettingsService.Instance.SessionSettings.LanguageSettings.Language;
-			var tabPage = Docking.DockWorkspace.Instance.ActiveTabPage;
+			var settingsService = SettingsService.Instance;
+			var dockWorkspace = Docking.DockWorkspace.Instance;
+
+			var currentLanguage = settingsService.SessionSettings.LanguageSettings.Language;
+			var tabPage = dockWorkspace.ActiveTabPage;
 			tabPage.ShowTextView(textView => {
 				if (selectedNodes.Count == 1 && selectedNodes[0] is ILSpyTreeNode singleSelection)
 				{
@@ -87,7 +93,7 @@ namespace ICSharpCode.ILSpy.TextView
 
 				// Fallback: if nobody was able to handle the request, use default behavior.
 				// try to save all nodes to disk.
-				var options = MainWindow.Instance.CreateDecompilationOptions();
+				var options = settingsService.CreateDecompilationOptions(dockWorkspace.ActiveTabPage);
 				options.FullDecompilation = true;
 				textView.SaveToDisk(currentLanguage, selectedNodes.OfType<ILSpyTreeNode>(), options);
 			});
diff --git a/ILSpy/Commands/SaveCommand.cs b/ILSpy/Commands/SaveCommand.cs
index 8b8871e0a..9e43df69b 100644
--- a/ILSpy/Commands/SaveCommand.cs
+++ b/ILSpy/Commands/SaveCommand.cs
@@ -17,9 +17,12 @@
 // DEALINGS IN THE SOFTWARE.
 
 using System.ComponentModel.Composition;
+using System.Linq;
 using System.Windows.Input;
 
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Properties;
+using ICSharpCode.ILSpy.TextView;
 
 namespace ICSharpCode.ILSpy
 {
@@ -27,9 +30,24 @@ namespace ICSharpCode.ILSpy
 	[PartCreationPolicy(CreationPolicy.Shared)]
 	sealed class SaveCommand : CommandWrapper
 	{
-		public SaveCommand()
+		private AssemblyTreeModel assemblyTreeModel;
+
+		[ImportingConstructor]
+		public SaveCommand(AssemblyTreeModel assemblyTreeModel)
 			: base(ApplicationCommands.Save)
 		{
+			this.assemblyTreeModel = assemblyTreeModel;
+		}
+
+		protected override void OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
+		{
+			e.Handled = true;
+			e.CanExecute = SaveCodeContextMenuEntry.CanExecute(assemblyTreeModel.SelectedNodes.ToList());
+		}
+
+		protected override void OnExecute(object sender, ExecutedRoutedEventArgs e)
+		{
+			SaveCodeContextMenuEntry.Execute(assemblyTreeModel.SelectedNodes.ToList());
 		}
 	}
 }
diff --git a/ILSpy/Commands/SelectPdbContextMenuEntry.cs b/ILSpy/Commands/SelectPdbContextMenuEntry.cs
index 46fa62625..db4fa7373 100644
--- a/ILSpy/Commands/SelectPdbContextMenuEntry.cs
+++ b/ILSpy/Commands/SelectPdbContextMenuEntry.cs
@@ -48,10 +48,10 @@ namespace ICSharpCode.ILSpy
 				await assembly.LoadDebugInfo(dlg.FileName);
 			}
 
-			var node = (AssemblyTreeNode?)MainWindow.Instance.FindNodeByPath(new[] { assembly.FileName }, true);
+			var node = (AssemblyTreeNode)MainWindow.Instance.AssemblyTreeModel.FindNodeByPath(new[] { assembly.FileName }, true);
 			node.UpdateToolTip();
-			MainWindow.Instance.SelectNode(node);
-			MainWindow.Instance.RefreshDecompiledView();
+			MainWindow.Instance.AssemblyTreeModel.SelectNode(node);
+			MainWindow.Instance.AssemblyTreeModel.RefreshDecompiledView();
 		}
 
 		public bool IsEnabled(TextViewContext context) => true;
diff --git a/ILSpy/Commands/SetThemeCommand.cs b/ILSpy/Commands/SetThemeCommand.cs
index 8822ed67a..ee415cc0d 100644
--- a/ILSpy/Commands/SetThemeCommand.cs
+++ b/ILSpy/Commands/SetThemeCommand.cs
@@ -1,5 +1,4 @@
-using ICSharpCode.ILSpy.Util;
-
+
 namespace ICSharpCode.ILSpy.Commands
 {
 	public class SetThemeCommand : SimpleCommand
@@ -7,7 +6,9 @@ namespace ICSharpCode.ILSpy.Commands
 		public override void Execute(object? parameter)
 		{
 			if (parameter is string theme)
+			{
 				SettingsService.Instance.SessionSettings.Theme = theme;
+			}
 		}
 	}
 }
diff --git a/ILSpy/Commands/ShowCFGContextMenuEntry.cs b/ILSpy/Commands/ShowCFGContextMenuEntry.cs
index 9ff313da7..a1a124192 100644
--- a/ILSpy/Commands/ShowCFGContextMenuEntry.cs
+++ b/ILSpy/Commands/ShowCFGContextMenuEntry.cs
@@ -6,7 +6,6 @@ using System.Windows;
 using ICSharpCode.Decompiler.FlowAnalysis;
 using ICSharpCode.Decompiler.IL;
 using ICSharpCode.Decompiler.IL.ControlFlow;
-using ICSharpCode.ILSpy.Util;
 
 namespace ICSharpCode.ILSpy.Commands
 {
diff --git a/ILSpy/Commands/SortAssemblyListCommand.cs b/ILSpy/Commands/SortAssemblyListCommand.cs
index 3e5bce4ae..8f744dadf 100644
--- a/ILSpy/Commands/SortAssemblyListCommand.cs
+++ b/ILSpy/Commands/SortAssemblyListCommand.cs
@@ -29,17 +29,11 @@ namespace ICSharpCode.ILSpy
 	[ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources.SortAssembly_listName), MenuIcon = "Images/Sort", MenuCategory = nameof(Resources.View))]
 	[ExportToolbarCommand(ToolTip = nameof(Resources.SortAssemblyListName), ToolbarIcon = "Images/Sort", ToolbarCategory = nameof(Resources.View))]
 	[PartCreationPolicy(CreationPolicy.Shared)]
-	sealed class SortAssemblyListCommand : SimpleCommand, IComparer<LoadedAssembly>
+	sealed class SortAssemblyListCommand : SimpleCommand
 	{
 		public override void Execute(object? parameter)
 		{
-			using (MainWindow.Instance.AssemblyTreeView.LockUpdates())
-				MainWindow.Instance.CurrentAssemblyList.Sort(this);
-		}
-
-		int IComparer<LoadedAssembly>.Compare(LoadedAssembly? x, LoadedAssembly? y)
-		{
-			return string.Compare(x.ShortName, y.ShortName, StringComparison.CurrentCulture);
+			MainWindow.Instance.AssemblyTreeModel.SortAssemblyList();
 		}
 	}
 
@@ -50,19 +44,8 @@ namespace ICSharpCode.ILSpy
 	{
 		public override void Execute(object? parameter)
 		{
-			using (MainWindow.Instance.AssemblyTreeView.LockUpdates())
-				CollapseChildren(MainWindow.Instance.AssemblyTreeView.Root);
+			MainWindow.Instance.AssemblyTreeModel.CollapseAll();
 
-			void CollapseChildren(SharpTreeNode node)
-			{
-				foreach (var child in node.Children)
-				{
-					if (!child.IsExpanded)
-						continue;
-					CollapseChildren(child);
-					child.IsExpanded = false;
-				}
-			}
 		}
 	}
 }
diff --git a/ILSpy/ContextMenuEntry.cs b/ILSpy/ContextMenuEntry.cs
index 27c7c1b24..f4a96d187 100644
--- a/ILSpy/ContextMenuEntry.cs
+++ b/ILSpy/ContextMenuEntry.cs
@@ -22,8 +22,6 @@ using System.ComponentModel.Composition;
 using System.Linq;
 using System.Windows;
 using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media;
 
 using ICSharpCode.AvalonEdit;
 using ICSharpCode.ILSpy.Controls.TreeView;
@@ -32,6 +30,7 @@ using ICSharpCode.ILSpyX.Search;
 using ICSharpCode.ILSpyX.TreeView;
 
 using TomsToolbox.Composition;
+using TomsToolbox.Essentials;
 
 namespace ICSharpCode.ILSpy
 {
@@ -86,36 +85,40 @@ namespace ICSharpCode.ILSpy
 		/// </summary>
 		public TextViewPosition? Position { get; private set; }
 
-		public Point MousePosition { get; private set; }
+		/// <summary>
+		/// Returns the original source of the context menu event.
+		/// </summary>
+		public DependencyObject OriginalSource { get; private set; }
 
-		public static TextViewContext Create(SharpTreeView? treeView = null, DecompilerTextView? textView = null, ListBox? listBox = null, DataGrid? dataGrid = null)
+		public static TextViewContext Create(ContextMenuEventArgs eventArgs, SharpTreeView?? treeView = null, DecompilerTextView textView = null, ListBox? listBox = null, DataGrid? dataGrid = null)
 		{
 			ReferenceSegment? reference;
-			if (textView != null)
+
+			if (textView is not null)
+			{
 				reference = textView.GetReferenceSegmentAtMousePosition();
-			else if (listBox?.SelectedItem is SearchResult result)
-				reference = new ReferenceSegment { Reference = result.Reference };
-			else if (listBox?.SelectedItem is TreeNodes.IMemberTreeNode provider)
-				reference = new ReferenceSegment { Reference = provider.Member };
-			else if (listBox?.SelectedItem != null)
-				reference = new ReferenceSegment { Reference = listBox.SelectedItem };
-			else if (dataGrid?.SelectedItem is TreeNodes.IMemberTreeNode provider2)
-				reference = new ReferenceSegment { Reference = provider2.Member };
-			else if (dataGrid?.SelectedItem != null)
-				reference = new ReferenceSegment { Reference = dataGrid.SelectedItem };
+			}
 			else
-				reference = null;
-			var position = textView != null ? textView.GetPositionFromMousePosition() : null;
-			var selectedTreeNodes = treeView != null ? treeView.GetTopLevelSelection().ToArray() : null;
-			return new TextViewContext {
+			{
+				reference = (listBox?.SelectedItem ?? dataGrid?.SelectedItem) switch {
+					SearchResult searchResult => new() { Reference = searchResult.Reference },
+					TreeNodes.IMemberTreeNode treeNode => new() { Reference = treeNode.Member }, { } value => new() { Reference = value },
+					_ => null
+				};
+			}
+
+			var position = textView?.GetPositionFromMousePosition();
+			var selectedTreeNodes = treeView?.GetTopLevelSelection().ToArray();
+
+			return new() {
 				ListBox = listBox,
 				DataGrid = dataGrid,
 				TreeView = treeView,
-				SelectedTreeNodes = selectedTreeNodes,
 				TextView = textView,
+				SelectedTreeNodes = selectedTreeNodes,
 				Reference = reference,
 				Position = position,
-				MousePosition = ((Visual?)textView ?? treeView ?? (Visual?)listBox ?? dataGrid).PointToScreen(Mouse.GetPosition((IInputElement?)textView ?? treeView ?? (IInputElement?)listBox ?? dataGrid))
+				OriginalSource = eventArgs.OriginalSource as DependencyObject
 			};
 		}
 	}
@@ -168,6 +171,13 @@ namespace ICSharpCode.ILSpy
 
 	internal class ContextMenuProvider
 	{
+		private static readonly WeakEventSource<EventArgs> ContextMenuClosedEventSource = new();
+
+		public static event EventHandler<EventArgs> ContextMenuClosed {
+			add => ContextMenuClosedEventSource.Subscribe(value);
+			remove => ContextMenuClosedEventSource.Unsubscribe(value);
+		}
+
 		/// <summary>
 		/// Enables extensible context menu support for the specified tree view.
 		/// </summary>
@@ -203,51 +213,54 @@ namespace ICSharpCode.ILSpy
 			dataGrid.ContextMenu = new ContextMenu();
 		}
 
+		readonly Control control;
 		readonly SharpTreeView treeView;
 		readonly DecompilerTextView textView;
 		readonly ListBox listBox;
 		readonly DataGrid dataGrid;
 		readonly IExport<IContextMenuEntry, IContextMenuEntryMetadata>[] entries;
 
-		private ContextMenuProvider()
+		private ContextMenuProvider(Control control)
 		{
 			entries = App.ExportProvider.GetExports<IContextMenuEntry, IContextMenuEntryMetadata>().ToArray();
+
+			this.control = control;
 		}
 
 		ContextMenuProvider(DecompilerTextView textView)
-			: this()
+			: this((Control)textView)
 		{
 			this.textView = textView ?? throw new ArgumentNullException(nameof(textView));
 		}
 
 		ContextMenuProvider(SharpTreeView treeView)
-			: this()
+			: this((Control)treeView)
 		{
 			this.treeView = treeView ?? throw new ArgumentNullException(nameof(treeView));
 		}
 
 		ContextMenuProvider(ListBox listBox)
-			: this()
+			: this((Control)listBox)
 		{
 			this.listBox = listBox ?? throw new ArgumentNullException(nameof(listBox));
 		}
 
 		ContextMenuProvider(DataGrid dataGrid)
-			: this()
+			: this((Control)dataGrid)
 		{
 			this.dataGrid = dataGrid ?? throw new ArgumentNullException(nameof(dataGrid));
 		}
 
 		void treeView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
 		{
-			TextViewContext context = TextViewContext.Create(treeView);
+			var context = TextViewContext.Create(e, treeView: treeView);
 			if (context.SelectedTreeNodes.Length == 0)
 			{
 				e.Handled = true; // don't show the menu
 				return;
 			}
-			ContextMenu menu;
-			if (ShowContextMenu(context, out menu))
+
+			if (ShowContextMenu(context, out var menu))
 				treeView.ContextMenu = menu;
 			else
 				// hide the context menu.
@@ -256,9 +269,8 @@ namespace ICSharpCode.ILSpy
 
 		void textView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
 		{
-			TextViewContext context = TextViewContext.Create(textView: textView);
-			ContextMenu menu;
-			if (ShowContextMenu(context, out menu))
+			var context = TextViewContext.Create(e, textView: textView);
+			if (ShowContextMenu(context, out var menu))
 				textView.ContextMenu = menu;
 			else
 				// hide the context menu.
@@ -267,9 +279,8 @@ namespace ICSharpCode.ILSpy
 
 		void listBox_ContextMenuOpening(object sender, ContextMenuEventArgs e)
 		{
-			TextViewContext context = TextViewContext.Create(listBox: listBox);
-			ContextMenu menu;
-			if (ShowContextMenu(context, out menu))
+			var context = TextViewContext.Create(e, listBox: listBox);
+			if (ShowContextMenu(context, out var menu))
 				listBox.ContextMenu = menu;
 			else
 				// hide the context menu.
@@ -278,9 +289,8 @@ namespace ICSharpCode.ILSpy
 
 		void dataGrid_ContextMenuOpening(object sender, ContextMenuEventArgs e)
 		{
-			TextViewContext context = TextViewContext.Create(dataGrid: dataGrid);
-			ContextMenu menu;
-			if (ShowContextMenu(context, out menu))
+			var context = TextViewContext.Create(e, dataGrid: dataGrid);
+			if (ShowContextMenu(context, out var menu))
 				dataGrid.ContextMenu = menu;
 			else
 				// hide the context menu.
@@ -289,7 +299,18 @@ namespace ICSharpCode.ILSpy
 
 		bool ShowContextMenu(TextViewContext context, out ContextMenu menu)
 		{
+			// Closing event is raised on the control where mouse is clicked, not on the control that opened the menu, so we hook on the global window event.
+			var window = Window.GetWindow(control)!;
+			window.ContextMenuClosing += ContextMenu_Closing;
+
+			void ContextMenu_Closing(object sender, EventArgs e)
+			{
+				window.ContextMenuClosing -= ContextMenu_Closing;
+				ContextMenuClosedEventSource.Raise(this, EventArgs.Empty);
+			}
+
 			menu = new ContextMenu();
+
 			var menuGroups = new Dictionary<string, IExport<IContextMenuEntry, IContextMenuEntryMetadata>[]>();
 			IExport<IContextMenuEntry, IContextMenuEntryMetadata>[]? topLevelGroup = null;
 			foreach (var group in entries.OrderBy(c => c.Metadata.Order).GroupBy(c => c.Metadata.ParentMenuID))
@@ -310,10 +331,10 @@ namespace ICSharpCode.ILSpy
 			{
 				foreach (var category in menuGroup.GroupBy(c => c.Metadata.Category))
 				{
-					bool needSeparatorForCategory = parent.Count > 0;
+					var needSeparatorForCategory = parent.Count > 0;
 					foreach (var entryPair in category)
 					{
-						IContextMenuEntry? entry = entryPair.Value;
+						var entry = entryPair.Value;
 						if (entry.IsVisible(context))
 						{
 							if (needSeparatorForCategory)
@@ -321,8 +342,8 @@ namespace ICSharpCode.ILSpy
 								parent.Add(new Separator());
 								needSeparatorForCategory = false;
 							}
-							MenuItem menuItem = new MenuItem();
-							menuItem.Header = MainWindow.GetResourceString(entryPair.Metadata.Header);
+							var menuItem = new MenuItem();
+							menuItem.Header = ResourceHelper.GetString(entryPair.Metadata.Header);
 							menuItem.InputGestureText = entryPair.Metadata.InputGestureText;
 							if (!string.IsNullOrEmpty(entryPair.Metadata.Icon))
 							{
diff --git a/ILSpy/Controls/TreeView/SharpTreeView.cs b/ILSpy/Controls/TreeView/SharpTreeView.cs
index 7ca753759..12a68bf04 100644
--- a/ILSpy/Controls/TreeView/SharpTreeView.cs
+++ b/ILSpy/Controls/TreeView/SharpTreeView.cs
@@ -158,6 +158,7 @@ namespace ICSharpCode.ILSpy.Controls.TreeView
 			if (flattener != null)
 			{
 				flattener.Stop();
+				flattener.CollectionChanged -= flattener_CollectionChanged;
 			}
 			if (Root != null)
 			{
@@ -818,9 +819,9 @@ namespace ICSharpCode.ILSpy.Controls.TreeView
 		/// </summary>
 		public IEnumerable<SharpTreeNode> GetTopLevelSelection()
 		{
-			var selection = this.SelectedItems.OfType<SharpTreeNode>();
-			var selectionHash = new HashSet<SharpTreeNode>(selection);
-			return selection.Where(item => item.Ancestors().All(a => !selectionHash.Contains(a)));
+			var selection = this.SelectedItems.OfType<SharpTreeNode>().ToHashSet();
+
+			return selection.Where(item => item.Ancestors().All(a => !selection.Contains(a)));
 		}
 
 		#endregion
diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs
index f07026732..b8b328b6f 100644
--- a/ILSpy/DecompilationOptions.cs
+++ b/ILSpy/DecompilationOptions.cs
@@ -23,6 +23,8 @@ using ICSharpCode.Decompiler;
 using ICSharpCode.ILSpy.Options;
 using ICSharpCode.ILSpyX;
 
+using DecompilerSettings = ICSharpCode.ILSpy.Options.DecompilerSettings;
+
 namespace ICSharpCode.ILSpy
 {
 	/// <summary>
@@ -87,7 +89,9 @@ namespace ICSharpCode.ILSpy
 		{
 			if (!Enum.TryParse(version?.Version, out Decompiler.CSharp.LanguageVersion languageVersion))
 				languageVersion = Decompiler.CSharp.LanguageVersion.Latest;
+
 			var newSettings = this.DecompilerSettings = settings.Clone();
+
 			newSettings.SetLanguageVersion(languageVersion);
 			newSettings.ExpandMemberDefinitions = displaySettings.ExpandMemberDefinitions;
 			newSettings.ExpandUsingDeclarations = displaySettings.ExpandUsingDeclarations;
diff --git a/ILSpy/Docking/DockLayoutSettings.cs b/ILSpy/Docking/DockLayoutSettings.cs
index dc3926500..42533f41a 100644
--- a/ILSpy/Docking/DockLayoutSettings.cs
+++ b/ILSpy/Docking/DockLayoutSettings.cs
@@ -19,7 +19,6 @@
 using System;
 using System.IO;
 using System.Linq;
-using System.Xml;
 using System.Xml.Linq;
 
 using AvalonDock.Layout.Serialization;
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index f3de5959f..2b1765b3d 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -33,9 +33,11 @@ using ICSharpCode.AvalonEdit.Highlighting;
 using ICSharpCode.ILSpy.Analyzers;
 using ICSharpCode.ILSpy.Search;
 using ICSharpCode.ILSpy.TextView;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpy.ViewModels;
+using ICSharpCode.ILSpyX.Extensions;
 
+using TomsToolbox.Composition;
+using TomsToolbox.Essentials;
 using TomsToolbox.Wpf;
 
 namespace ICSharpCode.ILSpy.Docking
@@ -44,11 +46,22 @@ namespace ICSharpCode.ILSpy.Docking
 	{
 		private static SessionSettings SessionSettings => SettingsService.Instance.SessionSettings;
 
+		private readonly IExportProvider exportProvider = App.ExportProvider;
+
 		public static readonly DockWorkspace Instance = new();
 
+		private readonly ObservableCollection<TabPageModel> tabPages = [];
+		private readonly ObservableCollection<ToolPaneModel> toolPanes = [];
+
 		private DockWorkspace()
 		{
-			this.TabPages.CollectionChanged += Documents_CollectionChanged;
+			this.tabPages.CollectionChanged += TabPages_CollectionChanged;
+			TabPages = new(tabPages);
+			ToolPanes = new(toolPanes);
+
+			// Make sure there is at least one tab open
+			AddTabPage();
+
 			MessageBus<CurrentAssemblyListChangedEventArgs>.Subscribers += (sender, e) => CurrentAssemblyList_Changed(sender, e);
 		}
 
@@ -58,52 +71,57 @@ namespace ICSharpCode.ILSpy.Docking
 			{
 				return;
 			}
-			foreach (var tab in TabPages.ToArray())
+			foreach (var tab in tabPages.ToArray())
 			{
 				var state = tab.GetState();
-				if (state == null || state.DecompiledNodes == null)
-				{
+				var decompiledNodes = state?.DecompiledNodes;
+				if (decompiledNodes == null)
 					continue;
-				}
-				bool found = false;
-				foreach (var node in state.DecompiledNodes)
-				{
-					var assemblyNode = node.Ancestors().OfType<TreeNodes.AssemblyTreeNode>().LastOrDefault();
-					if (assemblyNode != null && !e.OldItems.Contains(assemblyNode.LoadedAssembly))
-					{
-						found = true;
-						break;
-					}
-				}
-				if (!found && TabPages.Count > 1)
+
+				bool found = decompiledNodes
+					.Select(node => node.Ancestors().OfType<TreeNodes.AssemblyTreeNode>().LastOrDefault())
+					.ExceptNullItems()
+					.Any(assemblyNode => !e.OldItems.Contains(assemblyNode.LoadedAssembly));
+
+				if (!found && tabPages.Count > 1)
 				{
-					TabPages.Remove(tab);
+					tabPages.Remove(tab);
 				}
 			}
 		}
 
-		private void Documents_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+		private void TabPages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
 		{
-			var collection = (PaneCollection<TabPageModel>?)sender;
 			if (e.Action == NotifyCollectionChangedAction.Add)
 			{
-				ActiveTabPage = e.NewItems?[0] as TabPageModel;
+				if (e.NewItems?[0] is TabPageModel model)
+				{
+					ActiveTabPage = model;
+					model.IsActive = true;
+					model.IsVisible = true;
+				}
 			}
 
-			bool canClose = collection.Count > 1;
-			foreach (var item in collection)
+			bool canClose = tabPages.Count > 1;
+
+			foreach (var item in tabPages)
 			{
 				item.IsCloseable = canClose;
 			}
 		}
 
-		public PaneCollection<TabPageModel> TabPages { get; } = new PaneCollection<TabPageModel>();
+		public void AddTabPage(TabPageModel tabPage = null)
+		{
+			tabPages.Add(tabPage ?? new TabPageModel());
+		}
+
+		public ReadOnlyObservableCollection<TabPageModel> TabPages { get; }
 
-		public ObservableCollection<ToolPaneModel> ToolPanes { get; } = new ObservableCollection<ToolPaneModel>();
+		public ReadOnlyObservableCollection<ToolPaneModel> ToolPanes { get; }
 
 		public bool ShowToolPane(string contentId)
 		{
-			var pane = ToolPanes.FirstOrDefault(p => p.ContentId == contentId);
+			var pane = toolPanes.FirstOrDefault(p => p.ContentId == contentId);
 			if (pane != null)
 			{
 				pane.Show();
@@ -115,7 +133,7 @@ namespace ICSharpCode.ILSpy.Docking
 		public void Remove(PaneModel model)
 		{
 			if (model is TabPageModel document)
-				TabPages.Remove(document);
+				tabPages.Remove(document);
 			if (model is ToolPaneModel tool)
 				tool.IsVisible = false;
 		}
@@ -136,20 +154,22 @@ namespace ICSharpCode.ILSpy.Docking
 				{
 					if (state.DecompiledNodes != null)
 					{
-						MainWindow.Instance.SelectNodes(state.DecompiledNodes,
-							inNewTabPage: false, setFocus: true, changingActiveTab: true);
+						MainWindow.Instance.AssemblyTreeModel.SelectNodes(state.DecompiledNodes);
 					}
 					else
 					{
-						MainWindow.Instance.NavigateTo(new(state.ViewedUri, null));
+						MainWindow.Instance.AssemblyTreeModel.NavigateTo(new(state.ViewedUri, null));
 					}
 				}
-				MessageBus.Send(this, new DockWorkspaceActiveTabPageChangedEventArgs());
 			}
 		}
 
 		public void InitializeLayout(DockingManager manager)
 		{
+			var panes = exportProvider.GetExportedValues<ToolPaneModel>("ToolPane").OrderBy(item => item.Title);
+
+			this.toolPanes.AddRange(panes);
+
 			manager.LayoutUpdateStrategy = this;
 			XmlLayoutSerializer serializer = new XmlLayoutSerializer(manager);
 			serializer.LayoutSerializationCallback += LayoutSerializationCallback;
@@ -168,7 +188,7 @@ namespace ICSharpCode.ILSpy.Docking
 			switch (e.Model)
 			{
 				case LayoutAnchorable la:
-					e.Content = ToolPanes.FirstOrDefault(p => p.ContentId == la.ContentId);
+					e.Content = this.toolPanes.FirstOrDefault(p => p.ContentId == la.ContentId);
 					e.Cancel = e.Content == null;
 					la.CanDockAsTabbedDocument = false;
 					if (!e.Cancel)
@@ -200,23 +220,23 @@ namespace ICSharpCode.ILSpy.Docking
 
 		internal void CloseAllTabs()
 		{
-			foreach (var doc in TabPages.ToArray())
+			foreach (var doc in tabPages.ToArray())
 			{
 				if (doc.IsCloseable)
-					TabPages.Remove(doc);
+					tabPages.Remove(doc);
 			}
 		}
 
 		internal void ResetLayout()
 		{
-			foreach (var pane in ToolPanes)
+			foreach (var pane in toolPanes)
 			{
 				pane.IsVisible = false;
 			}
 			CloseAllTabs();
 			SessionSettings.DockLayout.Reset();
 			InitializeLayout(MainWindow.Instance.dockManager);
-			MainWindow.Instance.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)MainWindow.Instance.RefreshDecompiledView);
+			MainWindow.Instance.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)MainWindow.Instance.AssemblyTreeModel.RefreshDecompiledView);
 		}
 
 		static readonly PropertyInfo previousContainerProperty = typeof(LayoutContent).GetProperty("PreviousContainer", BindingFlags.NonPublic | BindingFlags.Instance);
diff --git a/ILSpy/Docking/PaneCollection.cs b/ILSpy/Docking/PaneCollection.cs
deleted file mode 100644
index 0afa5f316..000000000
--- a/ILSpy/Docking/PaneCollection.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) 2019 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.Collections;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Collections.Specialized;
-
-using ICSharpCode.ILSpy.ViewModels;
-
-namespace ICSharpCode.ILSpy.Docking
-{
-	public class PaneCollection<T> : INotifyCollectionChanged, ICollection<T>
-		where T : PaneModel, new()
-	{
-		private ObservableCollection<T> observableCollection = new ObservableCollection<T>();
-
-		public event NotifyCollectionChangedEventHandler? CollectionChanged;
-
-		public PaneCollection()
-		{
-			observableCollection.CollectionChanged += (sender, e) => CollectionChanged?.Invoke(this, e);
-		}
-
-		public void Add(T? item = null)
-		{
-			item ??= new T();
-
-			observableCollection.Add(item);
-
-			item.IsVisible = true;
-			item.IsActive = true;
-		}
-
-		public int Count => observableCollection.Count;
-		public bool IsReadOnly => false;
-		public void Clear() => observableCollection.Clear();
-		public bool Contains(T item) => observableCollection.Contains(item);
-		public void CopyTo(T[] array, int arrayIndex) => observableCollection.CopyTo(array, arrayIndex);
-		public bool Remove(T item) => observableCollection.Remove(item);
-		public IEnumerator<T> GetEnumerator() => observableCollection.GetEnumerator();
-		IEnumerator IEnumerable.GetEnumerator() => observableCollection.GetEnumerator();
-	}
-}
diff --git a/ILSpy/ExtensionMethods.cs b/ILSpy/ExtensionMethods.cs
index 374ad026e..39d623fd9 100644
--- a/ILSpy/ExtensionMethods.cs
+++ b/ILSpy/ExtensionMethods.cs
@@ -27,9 +27,10 @@ using System.Windows.Media;
 
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.Decompiler.TypeSystem;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 
+using TomsToolbox.Essentials;
+
 namespace ICSharpCode.ILSpy
 {
 	/// <summary>
@@ -125,9 +126,9 @@ namespace ICSharpCode.ILSpy
 				for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
 				{
 					DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
-					if (child != null && child is T)
+					if (child is T dependencyObject)
 					{
-						return (T)child;
+						return dependencyObject;
 					}
 
 					T? childItem = FindVisualChild<T>(child);
@@ -178,20 +179,37 @@ namespace ICSharpCode.ILSpy
 				else
 					output.AppendLine("-------------------------------------------------");
 				output.AppendLine("Error(s) loading plugin: " + item.PluginName);
-				if (item.Exception is System.Reflection.ReflectionTypeLoadException)
+				if (item.Exception is System.Reflection.ReflectionTypeLoadException exception)
 				{
-					var e = (System.Reflection.ReflectionTypeLoadException)item.Exception;
-					foreach (var ex in e.LoaderExceptions)
+					foreach (var ex in exception.LoaderExceptions.ExceptNullItems())
 					{
 						output.AppendLine(ex.ToString());
 						output.AppendLine();
 					}
 				}
 				else
+				{
 					output.AppendLine(item.Exception.ToString());
+				}
 			}
 
 			return true;
 		}
+
+		public static IDisposable PreserveFocus(this IInputElement? inputElement, bool preserve = true)
+		{
+			return new RestoreFocusHelper(inputElement, preserve);
+		}
+
+		private sealed class RestoreFocusHelper(IInputElement? inputElement, bool preserve) : IDisposable
+		{
+			public void Dispose()
+			{
+				if (preserve)
+				{
+					inputElement?.Focus();
+				}
+			}
+		}
 	}
 }
diff --git a/ILSpy/GlobalUsings.cs b/ILSpy/GlobalUsings.cs
new file mode 100644
index 000000000..12310c612
--- /dev/null
+++ b/ILSpy/GlobalUsings.cs
@@ -0,0 +1 @@
+global using ICSharpCode.ILSpy.Util;
\ No newline at end of file
diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj
index 7bed5bc81..ee4bc3253 100644
--- a/ILSpy/ILSpy.csproj
+++ b/ILSpy/ILSpy.csproj
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
diff --git a/ILSpy/LanguageSettings.cs b/ILSpy/LanguageSettings.cs
index c4a6da66d..e38d50da0 100644
--- a/ILSpy/LanguageSettings.cs
+++ b/ILSpy/LanguageSettings.cs
@@ -16,22 +16,20 @@
 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
-using System;
 using System.Collections.Generic;
-using System.ComponentModel;
 using System.Linq;
-using System.Runtime.CompilerServices;
 using System.Xml.Linq;
 
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 
+using TomsToolbox.Wpf;
+
 namespace ICSharpCode.ILSpy
 {
 	/// <summary>
 	/// Represents the filters applied to the tree view.
 	/// </summary>
-	public class LanguageSettings : INotifyPropertyChanged
+	public class LanguageSettings : ObservableObject, IChildSettings
 	{
 		/// <summary>
 		/// This dictionary is necessary to remember language versions across language changes. For example, 
@@ -40,14 +38,17 @@ namespace ICSharpCode.ILSpy
 		/// </summary>
 		private readonly Dictionary<Language, LanguageVersion?> languageVersionHistory = new Dictionary<Language, LanguageVersion?>();
 
-		public LanguageSettings(XElement element)
+		public LanguageSettings(XElement element, ISettingsSection parent)
 		{
+			Parent = parent;
 			this.ShowApiLevel = (ApiVisibility?)(int?)element.Element("ShowAPILevel") ?? ApiVisibility.PublicAndInternal;
 			this.Language = Languages.GetLanguage((string)element.Element("Language")) ?? Languages.AllLanguages.First();
 			this.LanguageVersion = Language.LanguageVersions.FirstOrDefault(v => v.Version == (string)element.Element("LanguageVersion"))
 								   ?? Language.LanguageVersions.LastOrDefault();
 		}
 
+		public ISettingsSection Parent { get; }
+
 		public XElement SaveAsXml()
 		{
 			return new XElement(
@@ -173,20 +174,6 @@ namespace ICSharpCode.ILSpy
 			}
 		}
 
-		public event PropertyChangedEventHandler? PropertyChanged;
-
-		protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
-		{
-			if (PropertyChanged != null)
-			{
-				var args = new PropertyChangedEventArgs(propertyName);
-
-				PropertyChanged(this, args);
-				MessageBus.Send(this, new LanguageSettingsChangedEventArgs(args));
-			}
-		}
-
-
 		// This class has been initially called FilterSettings, but then has been Hijacked to store language settings as well.
 		// While the filter settings were some sort of local, the language settings are global. This is a bit of a mess.
 		// There has been a lot of workarounds cloning the FilterSettings to pass them down to the tree nodes, without messing up the global language settings.
diff --git a/ILSpy/Languages/CSharpILMixedLanguage.cs b/ILSpy/Languages/CSharpILMixedLanguage.cs
index ebed5d1c6..6007cc18f 100644
--- a/ILSpy/Languages/CSharpILMixedLanguage.cs
+++ b/ILSpy/Languages/CSharpILMixedLanguage.cs
@@ -34,7 +34,6 @@ using ICSharpCode.Decompiler.Disassembler;
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.Decompiler.TypeSystem;
 using ICSharpCode.Decompiler.Util;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 using ICSharpCode.ILSpyX.Extensions;
 
diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs
index 4ec839456..d261f181e 100644
--- a/ILSpy/Languages/CSharpLanguage.cs
+++ b/ILSpy/Languages/CSharpLanguage.cs
@@ -41,7 +41,6 @@ using ICSharpCode.Decompiler.TypeSystem;
 using ICSharpCode.Decompiler.Util;
 using ICSharpCode.ILSpy.TextView;
 using ICSharpCode.ILSpy.TreeNodes;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 
 using LanguageVersion = ICSharpCode.ILSpyX.LanguageVersion;
@@ -359,15 +358,15 @@ namespace ICSharpCode.ILSpy
 
 		void AddReferenceWarningMessage(MetadataFile module, ITextOutput output)
 		{
-			var loadedAssembly = MainWindow.Instance.CurrentAssemblyList.GetAssemblies().FirstOrDefault(la => la.GetMetadataFileOrNull() == module);
+			var loadedAssembly = MainWindow.Instance.AssemblyTreeModel.AssemblyList.GetAssemblies().FirstOrDefault(la => la.GetMetadataFileOrNull() == module);
 			if (loadedAssembly == null || !loadedAssembly.LoadedAssemblyReferencesInfo.HasErrors)
 				return;
 			string line1 = Properties.Resources.WarningSomeAssemblyReference;
 			string line2 = Properties.Resources.PropertyManuallyMissingReferencesListLoadedAssemblies;
 			AddWarningMessage(module, output, line1, line2, Properties.Resources.ShowAssemblyLoad, Images.ViewCode, delegate {
-				ILSpyTreeNode assemblyNode = MainWindow.Instance.FindTreeNode(module);
+				ILSpyTreeNode assemblyNode = MainWindow.Instance.AssemblyTreeModel.FindTreeNode(module);
 				assemblyNode.EnsureLazyChildren();
-				MainWindow.Instance.SelectNode(assemblyNode.Children.OfType<ReferenceFolderTreeNode>().Single());
+				MainWindow.Instance.AssemblyTreeModel.SelectNode(assemblyNode.Children.OfType<ReferenceFolderTreeNode>().Single());
 			});
 		}
 
diff --git a/ILSpy/Languages/ILLanguage.cs b/ILSpy/Languages/ILLanguage.cs
index 01d032495..b484406d4 100644
--- a/ILSpy/Languages/ILLanguage.cs
+++ b/ILSpy/Languages/ILLanguage.cs
@@ -29,8 +29,8 @@ using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.Decompiler.Solution;
 using ICSharpCode.Decompiler.TypeSystem;
 using ICSharpCode.Decompiler.Util;
+using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.TextView;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 
 namespace ICSharpCode.ILSpy
@@ -198,7 +198,10 @@ namespace ICSharpCode.ILSpy
 		public override RichText? GetRichTextTooltip(IEntity entity)
 		{
 			var output = new AvalonEditTextOutput() { IgnoreNewLineAndIndent = true };
-			var disasm = CreateDisassembler(output, MainWindow.Instance.CreateDecompilationOptions());
+			var settingsService = SettingsService.Instance;
+			var dockWorkspace = DockWorkspace.Instance;
+
+			var disasm = CreateDisassembler(output, settingsService.CreateDecompilationOptions(dockWorkspace.ActiveTabPage));
 			MetadataFile? module = entity.ParentModule?.MetadataFile;
 			if (module == null)
 			{
diff --git a/ILSpy/Languages/Languages.cs b/ILSpy/Languages/Languages.cs
index a58df78da..980f90acc 100644
--- a/ILSpy/Languages/Languages.cs
+++ b/ILSpy/Languages/Languages.cs
@@ -16,6 +16,8 @@
 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
+#nullable enable
+
 using System;
 using System.Collections.ObjectModel;
 using System.Linq;
@@ -31,9 +33,9 @@ namespace ICSharpCode.ILSpy
 		/// </summary>
 		public static ReadOnlyCollection<Language> AllLanguages { get; } = Initialize(App.ExportProvider);
 
-		static ReadOnlyCollection<Language> Initialize(IExportProvider ep)
+		static ReadOnlyCollection<Language> Initialize(IExportProvider exportProvider)
 		{
-			var languages = ep.GetExportedValues<Language>().ToList();
+			var languages = exportProvider.GetExportedValues<Language>().ToList();
 
 			languages.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
 #if DEBUG
@@ -52,7 +54,8 @@ namespace ICSharpCode.ILSpy
 			return AllLanguages.FirstOrDefault(l => l.Name == name) ?? AllLanguages.First();
 		}
 
-		static ILLanguage ilLanguage;
+		static ILLanguage? ilLanguage;
+
 		public static ILLanguage ILLanguage {
 			get {
 				if (ilLanguage == null)
diff --git a/ILSpy/MainWindow.xaml b/ILSpy/MainWindow.xaml
index 39407e92e..dcc13af3b 100644
--- a/ILSpy/MainWindow.xaml
+++ b/ILSpy/MainWindow.xaml
@@ -1,72 +1,25 @@
 <?xml version="1.0" encoding="utf-8"?>
-
-<Window
-	x:Class="ICSharpCode.ILSpy.MainWindow"
-	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-	xmlns:tv="clr-namespace:ICSharpCode.ILSpy.Controls.TreeView"
-	xmlns:local="clr-namespace:ICSharpCode.ILSpy"
-	xmlns:avalondock="https://github.com/Dirkster99/AvalonDock"
-	xmlns:controls="clr-namespace:ICSharpCode.ILSpy.Controls"
-	xmlns:docking="clr-namespace:ICSharpCode.ILSpy.Docking"
-	xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
-	Title="ILSpy"
-	MinWidth="250"
-	MinHeight="200"
-	UseLayoutRounding="True"
-	TextOptions.TextFormattingMode="Display"
-	xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d:DesignHeight="500" d:DesignWidth="500"
-	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
-	xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
-	xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes"
-	xmlns:toms="urn:TomsToolbox"
-	xmlns:viewModels="clr-namespace:ICSharpCode.ILSpy.ViewModels"
-	xmlns:treeNodes="clr-namespace:ICSharpCode.ILSpy.TreeNodes"
-	xmlns:util="clr-namespace:ICSharpCode.ILSpy.Util"
-	d:DataContext="{d:DesignInstance local:MainWindowViewModel}">
+<Window x:Class="ICSharpCode.ILSpy.MainWindow"
+		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+		xmlns:local="clr-namespace:ICSharpCode.ILSpy"
+		xmlns:avalondock="https://github.com/Dirkster99/AvalonDock"
+		xmlns:controls="clr-namespace:ICSharpCode.ILSpy.Controls"
+		xmlns:docking="clr-namespace:ICSharpCode.ILSpy.Docking"
+		xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
+		Title="ILSpy"
+		MinWidth="250"
+		MinHeight="200"
+		UseLayoutRounding="True"
+		TextOptions.TextFormattingMode="Display"
+		xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d:DesignHeight="500" d:DesignWidth="500"
+		xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
+		xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
+		xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes"
+		xmlns:toms="urn:TomsToolbox"
+		xmlns:viewModels="clr-namespace:ICSharpCode.ILSpy.ViewModels"
+		d:DataContext="{d:DesignInstance local:MainWindowViewModel}">
 	<Window.Resources>
-		<tv:SharpTreeView x:Key="AssemblyTreeView"
-		                  AutomationProperties.Name="Assemblies and Classes"
-		                  SelectionChanged="TreeView_SelectionChanged"
-		                  ShowRoot="False"
-		                  AllowDropOrder="True"
-		                  AllowDrop="True"
-		                  BorderThickness="0" Visibility="Visible">
-			<tv:SharpTreeView.ItemContainerStyle>
-				<Style TargetType="tv:SharpTreeViewItem">
-					<Setter Property="Template">
-						<Setter.Value>
-							<ControlTemplate TargetType="{x:Type tv:SharpTreeViewItem}" d:DataContext="{d:DesignInstance treeNodes:ILSpyTreeNode}">
-								<Border Background="Transparent">
-									<Border Background="{TemplateBinding Background}">
-										<tv:SharpTreeNodeView x:Name="nodeView" HorizontalAlignment="Left" />
-									</Border>
-								</Border>
-								<ControlTemplate.Triggers>
-									<DataTrigger Binding="{Binding IsAutoLoaded}" Value="True">
-										<Setter Property="Foreground" Value="SteelBlue" />
-									</DataTrigger>
-									<DataTrigger Binding="{Binding IsPublicAPI}" Value="False">
-										<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
-									</DataTrigger>
-									<Trigger Property="IsSelected" Value="True">
-										<Setter TargetName="nodeView" Property="TextBackground" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
-										<Setter TargetName="nodeView" Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
-									</Trigger>
-									<Trigger Property="IsEnabled" Value="False">
-										<Setter TargetName="nodeView" Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
-									</Trigger>
-								</ControlTemplate.Triggers>
-							</ControlTemplate>
-						</Setter.Value>
-					</Setter>
-				</Style>
-			</tv:SharpTreeView.ItemContainerStyle>
-		</tv:SharpTreeView>
-
-		<DataTemplate DataType="{x:Type viewModels:AssemblyListPaneModel}">
-			<ContentControl Content="{StaticResource AssemblyTreeView}" />
-		</DataTemplate>
 
 		<DataTemplate DataType="{x:Type viewModels:TabPageModel}">
 			<ContentPresenter Content="{Binding Content}" />
@@ -76,40 +29,15 @@
 			<ContentPresenter Content="{Binding Content}" />
 		</DataTemplate>
 
-		<toms:BindingRelay x:Key="WindowBinding" DataContext="{Binding}" />
-
 	</Window.Resources>
 
 	<b:Interaction.Behaviors>
 		<themes:WindowStyleManagerBehavior />
 	</b:Interaction.Behaviors>
 
-	<Window.CommandBindings>
-		<CommandBinding
-			Command="Open"
-			Executed="OpenCommandExecuted" />
-		<CommandBinding
-			Command="Refresh"
-			Executed="RefreshCommandExecuted" />
-		<CommandBinding
-			Command="Save"
-			CanExecute="SaveCommandCanExecute"
-			Executed="SaveCommandExecuted" />
-		<CommandBinding
-			Command="BrowseBack"
-			CanExecute="BackCommandCanExecute"
-			Executed="BackCommandExecuted" />
-		<CommandBinding
-			Command="BrowseForward"
-			CanExecute="ForwardCommandCanExecute"
-			Executed="ForwardCommandExecuted" />
-		<CommandBinding
-			Command="Search"
-			Executed="SearchCommandExecuted" />
-	</Window.CommandBindings>
-
 	<Window.InputBindings>
 		<KeyBinding Key="R" Modifiers="Control" Command="{x:Static local:ILSpyCommands.Analyze}" />
+		<KeyBinding Key="Z" Modifiers="Control" Command="{x:Static NavigationCommands.BrowseBack}" />
 	</Window.InputBindings>
 
 	<Window.TaskbarItemInfo>
@@ -122,9 +50,12 @@
 			<MenuItem Header="{x:Static properties:Resources._File}" Tag="_File" />
 			<!-- contents of file menu are added using MEF -->
 			<MenuItem Header="{x:Static properties:Resources._View}" Tag="_View">
-				<MenuItem Header="{x:Static properties:Resources.Show_publiconlyTypesMembers}" IsCheckable="True" IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisPublicOnly}" />
-				<MenuItem Header="{x:Static properties:Resources.Show_internalTypesMembers}" IsCheckable="True" IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisPublicAndInternal}" />
-				<MenuItem Header="{x:Static properties:Resources.Show_allTypesAndMembers}" IsCheckable="True" IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisAll}" />
+				<MenuItem Header="{x:Static properties:Resources.Show_publiconlyTypesMembers}" IsCheckable="True"
+						  IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisPublicOnly}" />
+				<MenuItem Header="{x:Static properties:Resources.Show_internalTypesMembers}" IsCheckable="True"
+						  IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisPublicAndInternal}" />
+				<MenuItem Header="{x:Static properties:Resources.Show_allTypesAndMembers}" IsCheckable="True"
+						  IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisAll}" />
 				<Separator />
 				<MenuItem Header="{x:Static properties:Resources.Theme}" ItemsSource="{x:Static themes:ThemeManager.AllThemes}">
 					<MenuItem.ItemContainerStyle>
@@ -137,7 +68,7 @@
 								<Setter.Value>
 									<MultiBinding Converter="{x:Static toms:BinaryOperationConverter.Equality}" Mode="OneWay">
 										<Binding />
-										<Binding Source="{StaticResource WindowBinding}" Path="DataContext.SessionSettings.Theme" />
+										<Binding Path="DataContext.SessionSettings.Theme" RelativeSource="{RelativeSource FindAncestor, AncestorType=Window}"/>
 									</MultiBinding>
 								</Setter.Value>
 							</Setter>
@@ -145,9 +76,12 @@
 					</MenuItem.ItemContainerStyle>
 				</MenuItem>
 				<MenuItem Header="{x:Static properties:Resources.UILanguage}">
-					<MenuItem Header="{x:Static properties:Resources.UILanguage_System}" IsCheckable="True" IsChecked="{Binding SessionSettings.CurrentCulture, Converter={controls:CultureSelectionConverter}, ConverterParameter={x:Null}}" />
-					<MenuItem Header="English" IsCheckable="True" IsChecked="{Binding SessionSettings.CurrentCulture, Converter={controls:CultureSelectionConverter}, ConverterParameter=en-US}" />
-					<MenuItem Header="中文" IsCheckable="True" IsChecked="{Binding SessionSettings.CurrentCulture, Converter={controls:CultureSelectionConverter}, ConverterParameter=zh-Hans}" />
+					<MenuItem Header="{x:Static properties:Resources.UILanguage_System}" IsCheckable="True"
+							  IsChecked="{Binding SessionSettings.CurrentCulture, Converter={controls:CultureSelectionConverter}, ConverterParameter={x:Null}}" />
+					<MenuItem Header="English" IsCheckable="True"
+							  IsChecked="{Binding SessionSettings.CurrentCulture, Converter={controls:CultureSelectionConverter}, ConverterParameter=en-US}" />
+					<MenuItem Header="中文" IsCheckable="True"
+							  IsChecked="{Binding SessionSettings.CurrentCulture, Converter={controls:CultureSelectionConverter}, ConverterParameter=zh-Hans}" />
 				</MenuItem>
 			</MenuItem>
 			<MenuItem Header="{x:Static properties:Resources._Window}" Tag="_Window" />
@@ -183,52 +117,52 @@
 			<!-- 'Open' toolbar category is inserted here -->
 			<Separator />
 			<ComboBox Name="assemblyListComboBox" Width="100" MaxDropDownHeight="Auto"
-			          ItemsSource="{Binding AssemblyListManager.AssemblyLists}"
-			          ToolTip="{x:Static properties:Resources.SelectAssemblyListDropdownTooltip}"
-			          SelectedItem="{Binding SessionSettings.ActiveAssemblyList}" />
+					  ItemsSource="{Binding AssemblyListManager.AssemblyLists}"
+					  ToolTip="{x:Static properties:Resources.SelectAssemblyListDropdownTooltip}"
+					  SelectedItem="{Binding SessionSettings.ActiveAssemblyList}" />
 			<Button Command="{x:Static local:ILSpyCommands.ManageAssemblyListsCommand}"
-			        ToolTip="{x:Static properties:Resources.ManageAssemblyLists}">
+					ToolTip="{x:Static properties:Resources.ManageAssemblyLists}">
 				<Image Width="16" Height="16" Source="{controls:XamlResource Images/AssemblyList}"
-				       Style="{StaticResource DarkModeAwareImageStyle}" />
+					   Style="{StaticResource DarkModeAwareImageStyle}" />
 			</Button>
 			<Separator />
 			<CheckBox IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisPublicOnly}"
-			          ToolTip="{x:Static properties:Resources.ShowPublicOnlyTypesMembers}">
+					  ToolTip="{x:Static properties:Resources.ShowPublicOnlyTypesMembers}">
 				<Image Width="16" Height="16" Source="{controls:XamlResource Images/ShowPublicOnly}"
-				       Style="{StaticResource DarkModeAwareImageStyle}" />
+					   Style="{StaticResource DarkModeAwareImageStyle}" />
 			</CheckBox>
 			<CheckBox IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisPublicAndInternal}"
-			          ToolTip="{x:Static properties:Resources.ShowInternalTypesMembers}">
+					  ToolTip="{x:Static properties:Resources.ShowInternalTypesMembers}">
 				<Image Width="16" Height="16" Source="{controls:XamlResource Images/ShowPrivateInternal}"
-				       Style="{StaticResource DarkModeAwareImageStyle}" />
+					   Style="{StaticResource DarkModeAwareImageStyle}" />
 			</CheckBox>
 			<CheckBox IsChecked="{Binding SessionSettings.LanguageSettings.ApiVisAll}"
-			          ToolTip="{x:Static properties:Resources.ShowAllTypesAndMembers}">
+					  ToolTip="{x:Static properties:Resources.ShowAllTypesAndMembers}">
 				<Image Width="16" Height="16" Source="{controls:XamlResource Images/ShowAll}"
-				       Style="{StaticResource DarkModeAwareImageStyle}" />
+					   Style="{StaticResource DarkModeAwareImageStyle}" />
 			</CheckBox>
 			<Separator />
 			<ComboBox Name="languageComboBox" DisplayMemberPath="Name" Width="100" MaxDropDownHeight="Auto"
-			          IsEnabled="{Binding Workspace.ActiveTabPage.SupportsLanguageSwitching}"
-			          ItemsSource="{x:Static local:Languages.AllLanguages}"
-			          ToolTip="{x:Static properties:Resources.SelectLanguageDropdownTooltip}"
-			          SelectedItem="{Binding SessionSettings.LanguageSettings.Language}" />
+					  IsEnabled="{Binding Workspace.ActiveTabPage.SupportsLanguageSwitching}"
+					  ItemsSource="{x:Static local:Languages.AllLanguages}"
+					  ToolTip="{x:Static properties:Resources.SelectLanguageDropdownTooltip}"
+					  SelectedItem="{Binding SessionSettings.LanguageSettings.Language}" />
 			<ComboBox Name="languageVersionComboBox" DisplayMemberPath="DisplayName" Width="120" MaxDropDownHeight="Auto"
-			          ToolTip="{x:Static properties:Resources.SelectVersionDropdownTooltip}"
-			          Visibility="{Binding SelectedItem.HasLanguageVersions, ElementName=languageComboBox, Converter={toms:BooleanToVisibilityConverter}}"
-			          IsEnabled="{Binding Workspace.ActiveTabPage.SupportsLanguageSwitching}"
-			          ItemsSource="{Binding SelectedItem.LanguageVersions, ElementName=languageComboBox, UpdateSourceTrigger=PropertyChanged}"
-			          SelectedItem="{Binding SessionSettings.LanguageSettings.LanguageVersion, UpdateSourceTrigger=PropertyChanged}" />
+					  ToolTip="{x:Static properties:Resources.SelectVersionDropdownTooltip}"
+					  Visibility="{Binding SelectedItem.HasLanguageVersions, ElementName=languageComboBox, Converter={toms:BooleanToVisibilityConverter}}"
+					  IsEnabled="{Binding Workspace.ActiveTabPage.SupportsLanguageSwitching}"
+					  ItemsSource="{Binding SelectedItem.LanguageVersions, ElementName=languageComboBox, UpdateSourceTrigger=PropertyChanged}"
+					  SelectedItem="{Binding SessionSettings.LanguageSettings.LanguageVersion, UpdateSourceTrigger=PropertyChanged}" />
 		</ToolBar>
 		<!-- Update panel -->
 		<Border DockPanel.Dock="Top" BorderBrush="Black" BorderThickness="1" Name="updatePanel" Visibility="Collapsed">
 			<DockPanel KeyboardNavigation.TabNavigation="Contained">
-				<Button DockPanel.Dock="Right" Click="updatePanelCloseButtonClick" MinWidth="0">X</Button>
+				<Button DockPanel.Dock="Right" Click="UpdatePanelCloseButtonClick" MinWidth="0">X</Button>
 				<StackPanel Orientation="Horizontal">
 					<TextBlock Name="updatePanelMessage" Margin="4,0" VerticalAlignment="Center"
-					           Text="{x:Static properties:Resources.ILSpyVersionAvailable}" />
-					<Button Name="downloadOrCheckUpdateButton" Click="downloadOrCheckUpdateButtonClick"
-					        Content="{x:Static properties:Resources.Download}" />
+							   Text="{x:Static properties:Resources.ILSpyVersionAvailable}" />
+					<Button Name="downloadOrCheckUpdateButton" Click="DownloadOrCheckUpdateButtonClick"
+							Content="{x:Static properties:Resources.Download}" />
 				</StackPanel>
 			</DockPanel>
 		</Border>
@@ -236,19 +170,19 @@
 		<StatusBar x:Name="statusBar" DockPanel.Dock="Bottom" Height="26" Visibility="Collapsed">
 			<StatusBarItem DockPanel.Dock="Right">
 				<TextBlock VerticalAlignment="Center"
-				           HorizontalAlignment="Right"
-				           x:Name="statusLabel"
-				           ToolTip="{x:Static properties:Resources.Status}"
-				           Text="{x:Static properties:Resources.StandBy}" />
+						   HorizontalAlignment="Right"
+						   x:Name="statusLabel"
+						   ToolTip="{x:Static properties:Resources.Status}"
+						   Text="{x:Static properties:Resources.StandBy}" />
 			</StatusBarItem>
 		</StatusBar>
 
 		<avalondock:DockingManager x:Name="dockManager"
-		                           DataContext="{Binding Workspace}"
-		                           AnchorablesSource="{Binding ToolPanes}"
-		                           DocumentsSource="{Binding TabPages}"
-		                           ActiveContent="{Binding ActiveTabPage, Mode=TwoWay, Converter={docking:TabPageGuardConverter}}"
-		                           AllowMixedOrientation="True">
+								   DataContext="{Binding Workspace}"
+								   AnchorablesSource="{Binding ToolPanes}"
+								   DocumentsSource="{Binding TabPages}"
+								   ActiveContent="{Binding ActiveTabPage, Mode=TwoWay, Converter={docking:TabPageGuardConverter}}"
+								   AllowMixedOrientation="True">
 
 			<avalondock:DockingManager.DocumentHeaderTemplate>
 				<DataTemplate DataType="{x:Type viewModels:PaneModel}">
diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs
index cdd8cb429..f3668f6f0 100644
--- a/ILSpy/MainWindow.xaml.cs
+++ b/ILSpy/MainWindow.xaml.cs
@@ -17,52 +17,26 @@
 // DEALINGS IN THE SOFTWARE.
 
 using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Diagnostics;
-using System.IO;
+using System.Drawing;
 using System.Linq;
-using System.Reflection.Metadata;
-using System.Reflection.Metadata.Ecma335;
-using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
 using System.Windows.Input;
 using System.Windows.Media;
-using System.Windows.Navigation;
-using System.Windows.Threading;
 
 using AvalonDock.Layout.Serialization;
 
-using ICSharpCode.Decompiler;
-using ICSharpCode.Decompiler.Documentation;
-using ICSharpCode.Decompiler.Metadata;
-using ICSharpCode.Decompiler.TypeSystem;
-using ICSharpCode.Decompiler.TypeSystem.Implementation;
-using ICSharpCode.ILSpy.AppEnv;
-using ICSharpCode.ILSpy.Commands;
-using ICSharpCode.ILSpy.Controls.TreeView;
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Docking;
-using ICSharpCode.ILSpy.Options;
-using ICSharpCode.ILSpy.Search;
-using ICSharpCode.ILSpy.TextView;
-using ICSharpCode.ILSpy.Themes;
 using ICSharpCode.ILSpy.TreeNodes;
 using ICSharpCode.ILSpy.Updates;
-using ICSharpCode.ILSpy.Util;
-using ICSharpCode.ILSpy.ViewModels;
-using ICSharpCode.ILSpyX;
-using ICSharpCode.ILSpyX.Extensions;
 using ICSharpCode.ILSpyX.FileLoaders;
 using ICSharpCode.ILSpyX.Settings;
 using ICSharpCode.ILSpyX.TreeView;
 
-using Microsoft.Win32;
-
-using TomsToolbox.Composition;
+using Screen = System.Windows.Forms.Screen;
 
 namespace ICSharpCode.ILSpy
 {
@@ -71,99 +45,36 @@ namespace ICSharpCode.ILSpy
 	/// </summary>
 	partial class MainWindow : Window
 	{
-		bool refreshInProgress, changingActiveTab;
-		readonly NavigationHistory<NavigationState> history = new NavigationHistory<NavigationState>();
-
-		AssemblyList assemblyList;
-		AssemblyListTreeNode assemblyListTreeNode;
-
 		static MainWindow instance;
 
+		private readonly MainWindowViewModel mainWindowViewModel = new();
+
 		public static MainWindow Instance {
 			get { return instance; }
 		}
 
-		public SharpTreeView? AssemblyTreeView {
+		public AssemblyTreeModel AssemblyTreeModel {
 			get {
-				return FindResource("AssemblyTreeView") as SharpTreeView;
+				return App.ExportProvider.GetExportedValue<AssemblyTreeModel>();
 			}
 		}
 
-		public DecompilationOptions CreateDecompilationOptions()
-		{
-			var decompilerView = DockWorkspace.Instance.ActiveTabPage.Content as IProgress<DecompilationProgress>;
-			return new DecompilationOptions(CurrentLanguageVersion, SettingsService.Instance.DecompilerSettings, SettingsService.Instance.DisplaySettings) { Progress = decompilerView };
-		}
-
 		public MainWindow()
 		{
 			instance = this;
 
-			var sessionSettings = SettingsService.Instance.SessionSettings;
-
 			// Make sure Images are initialized on the UI thread.
 			this.Icon = Images.ILSpyIcon;
 
-			this.DataContext = new MainWindowViewModel {
-				Workspace = DockWorkspace.Instance,
-				SessionSettings = sessionSettings,
-				AssemblyListManager = SettingsService.Instance.AssemblyListManager
-			};
+			this.DataContext = mainWindowViewModel;
 
-			SettingsService.Instance.AssemblyListManager.CreateDefaultAssemblyLists();
-			if (!string.IsNullOrEmpty(sessionSettings.CurrentCulture))
-			{
-				System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(sessionSettings.CurrentCulture);
-			}
 			InitializeComponent();
-			InitToolPanes();
-			DockWorkspace.Instance.InitializeLayout(dockManager);
-
-			MessageBus<SessionSettingsChangedEventArgs>.Subscribers += (sender, e) => SessionSettings_PropertyChanged(sender, e);
-			MessageBus<LanguageSettingsChangedEventArgs>.Subscribers += (sender, e) => LanguageSettings_PropertyChanged(sender, e);
-			MessageBus<DockWorkspaceActiveTabPageChangedEventArgs>.Subscribers += DockWorkspace_ActiveTabPageChanged;
-
-			InitMainMenu();
-			InitWindowMenu();
-			InitToolbar();
-			InitFileLoaders();
-			ContextMenuProvider.Add(AssemblyTreeView);
-
-			this.Loaded += MainWindow_Loaded;
-		}
 
-		private void DockWorkspace_ActiveTabPageChanged(object? sender, EventArgs e)
-		{
-			DockWorkspace dock = DockWorkspace.Instance;
+			mainWindowViewModel.Workspace.InitializeLayout(dockManager);
 
-			var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == nameof(Properties.Resources._Window));
-			foreach (MenuItem menuItem in windowMenuItem.Items.OfType<MenuItem>())
-			{
-				if (menuItem.IsCheckable && menuItem.Tag is TabPageModel)
-				{
-					menuItem.IsChecked = menuItem.Tag == dock.ActiveTabPage;
-				}
-			}
-		}
+			MenuService.Instance.Init(mainMenu, toolBar, InputBindings);
 
-		private void SessionSettings_PropertyChanged(object sender, PropertyChangedEventArgs e)
-		{
-			var sessionSettings = SettingsService.Instance.SessionSettings;
-
-			switch (e.PropertyName)
-			{
-				case nameof(SessionSettings.ActiveAssemblyList):
-					ShowAssemblyList(sessionSettings.ActiveAssemblyList);
-					break;
-				case nameof(SessionSettings.Theme):
-					// update syntax highlighting and force reload (AvalonEdit does not automatically refresh on highlighting change)
-					DecompilerTextView.RegisterHighlighting();
-					DecompileSelectedNodes(DockWorkspace.Instance.ActiveTabPage.GetState() as DecompilerTextViewState);
-					break;
-				case nameof(SessionSettings.CurrentCulture):
-					MessageBox.Show(Properties.Resources.SettingsChangeRestartRequired, "ILSpy");
-					break;
-			}
+			InitFileLoaders();
 		}
 
 		void SetWindowBounds(Rect bounds)
@@ -174,367 +85,6 @@ namespace ICSharpCode.ILSpy
 			this.Height = bounds.Height;
 		}
 
-		#region Toolbar extensibility
-
-		void InitToolbar()
-		{
-			int navigationPos = 0;
-			int openPos = 1;
-			var toolbarCommands = App.ExportProvider.GetExports<ICommand, IToolbarCommandMetadata>("ToolbarCommand");
-			foreach (var commandGroup in toolbarCommands.OrderBy(c => c.Metadata.ToolbarOrder).GroupBy(c => Properties.Resources.ResourceManager.GetString(c.Metadata.ToolbarCategory)))
-			{
-				if (commandGroup.Key == Properties.Resources.ResourceManager.GetString("Navigation"))
-				{
-					foreach (var command in commandGroup)
-					{
-						toolBar.Items.Insert(navigationPos++, MakeToolbarItem(command));
-						openPos++;
-					}
-				}
-				else if (commandGroup.Key == Properties.Resources.ResourceManager.GetString("Open"))
-				{
-					foreach (var command in commandGroup)
-					{
-						toolBar.Items.Insert(openPos++, MakeToolbarItem(command));
-					}
-				}
-				else
-				{
-					toolBar.Items.Add(new Separator());
-					foreach (var command in commandGroup)
-					{
-						toolBar.Items.Add(MakeToolbarItem(command));
-					}
-				}
-			}
-
-		}
-
-		Button MakeToolbarItem(IExport<ICommand, IToolbarCommandMetadata> command)
-		{
-			return new Button {
-				Style = ThemeManager.Current.CreateToolBarButtonStyle(),
-				Command = CommandWrapper.Unwrap(command.Value),
-				ToolTip = Properties.Resources.ResourceManager.GetString(command.Metadata.ToolTip),
-				Tag = command.Metadata.Tag,
-				Content = new Image {
-					Width = 16,
-					Height = 16,
-					Source = Images.Load(command.Value, command.Metadata.ToolbarIcon)
-				}
-			};
-		}
-		#endregion
-
-		#region Main Menu extensibility
-
-		void InitMainMenu()
-		{
-			var mainMenuCommands = App.ExportProvider.GetExports<ICommand, IMainMenuCommandMetadata>("MainMenuCommand");
-			// Start by constructing the individual flat menus
-			var parentMenuItems = new Dictionary<string, MenuItem>();
-			var menuGroups = mainMenuCommands.OrderBy(c => c.Metadata.MenuOrder).GroupBy(c => c.Metadata.ParentMenuID);
-			foreach (var menu in menuGroups)
-			{
-				// Get or add the target menu item and add all items grouped by menu category
-				var parentMenuItem = GetOrAddParentMenuItem(menu.Key, menu.Key);
-				foreach (var category in menu.GroupBy(c => c.Metadata.MenuCategory))
-				{
-					if (parentMenuItem.Items.Count > 0)
-					{
-						parentMenuItem.Items.Add(new Separator() { Tag = category.Key });
-					}
-					foreach (var entry in category)
-					{
-						if (menuGroups.Any(g => g.Key == entry.Metadata.MenuID))
-						{
-							var menuItem = GetOrAddParentMenuItem(entry.Metadata.MenuID, entry.Metadata.Header);
-							// replace potential dummy text with real name
-							menuItem.Header = GetResourceString(entry.Metadata.Header);
-							parentMenuItem.Items.Add(menuItem);
-						}
-						else
-						{
-							MenuItem menuItem = new MenuItem();
-							menuItem.Command = CommandWrapper.Unwrap(entry.Value);
-							menuItem.Tag = entry.Metadata.MenuID;
-							menuItem.Header = GetResourceString(entry.Metadata.Header);
-							if (!string.IsNullOrEmpty(entry.Metadata.MenuIcon))
-							{
-								menuItem.Icon = new Image {
-									Width = 16,
-									Height = 16,
-									Source = Images.Load(entry.Value, entry.Metadata.MenuIcon)
-								};
-							}
-
-							menuItem.IsEnabled = entry.Metadata.IsEnabled;
-							if (entry.Value is ToggleableCommand toggle)
-							{
-								menuItem.IsCheckable = true;
-								menuItem.SetBinding(MenuItem.IsCheckedProperty, new Binding("IsChecked") { Source = entry.Value, Mode = BindingMode.OneWay });
-							}
-
-							menuItem.InputGestureText = entry.Metadata.InputGestureText;
-							parentMenuItem.Items.Add(menuItem);
-						}
-					}
-				}
-			}
-
-			foreach (var (key, item) in parentMenuItems)
-			{
-				if (item.Parent == null)
-				{
-					mainMenu.Items.Add(item);
-				}
-			}
-
-			MenuItem GetOrAddParentMenuItem(string menuID, string resourceKey)
-			{
-				if (!parentMenuItems.TryGetValue(menuID, out var parentMenuItem))
-				{
-					var topLevelMenuItem = mainMenu.Items.OfType<MenuItem>().FirstOrDefault(m => (string)m.Tag == menuID);
-					if (topLevelMenuItem == null)
-					{
-						parentMenuItem = new MenuItem();
-						parentMenuItem.Header = GetResourceString(resourceKey);
-						parentMenuItem.Tag = menuID;
-						parentMenuItems.Add(menuID, parentMenuItem);
-					}
-					else
-					{
-						parentMenuItems.Add(menuID, topLevelMenuItem);
-						parentMenuItem = topLevelMenuItem;
-					}
-				}
-				return parentMenuItem;
-			}
-		}
-
-		internal static string? GetResourceString(string key)
-		{
-			if (string.IsNullOrEmpty(key))
-			{
-				return null;
-			}
-			string? value = Properties.Resources.ResourceManager.GetString(key);
-			if (!string.IsNullOrEmpty(value))
-			{
-				return value;
-			}
-			return key;
-		}
-		#endregion
-
-		#region Tool Pane extensibility
-
-		private void InitToolPanes()
-		{
-			var toolPanes = App.ExportProvider.GetExportedValues<ToolPaneModel>("ToolPane").OrderBy(item => item.Title);
-
-			DockWorkspace.Instance.ToolPanes.AddRange(toolPanes);
-		}
-
-		private void InitWindowMenu()
-		{
-			var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == nameof(Properties.Resources._Window));
-			Separator separatorBeforeTools, separatorBeforeDocuments;
-			windowMenuItem.Items.Add(separatorBeforeTools = new Separator());
-			windowMenuItem.Items.Add(separatorBeforeDocuments = new Separator());
-
-			var dock = DockWorkspace.Instance;
-			dock.ToolPanes.CollectionChanged += ToolsChanged;
-			dock.TabPages.CollectionChanged += TabsChanged;
-
-			ToolsChanged(dock.ToolPanes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
-			TabsChanged(dock.TabPages, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
-
-			void ToolsChanged(object? sender, NotifyCollectionChangedEventArgs e)
-			{
-				int endIndex = windowMenuItem.Items.IndexOf(separatorBeforeDocuments);
-				int startIndex = windowMenuItem.Items.IndexOf(separatorBeforeTools) + 1;
-				int insertionIndex;
-				switch (e.Action)
-				{
-					case NotifyCollectionChangedAction.Add:
-						insertionIndex = Math.Min(endIndex, startIndex + e.NewStartingIndex);
-						foreach (ToolPaneModel pane in e.NewItems)
-						{
-							MenuItem menuItem = CreateMenuItem(pane);
-							windowMenuItem.Items.Insert(insertionIndex, menuItem);
-							insertionIndex++;
-						}
-						break;
-					case NotifyCollectionChangedAction.Remove:
-						foreach (ToolPaneModel pane in e.OldItems)
-						{
-							for (int i = endIndex - 1; i >= startIndex; i--)
-							{
-								MenuItem item = (MenuItem)windowMenuItem.Items[i];
-								if (pane == item.Tag)
-								{
-									windowMenuItem.Items.RemoveAt(i);
-									item.Tag = null;
-									endIndex--;
-									break;
-								}
-							}
-						}
-						break;
-					case NotifyCollectionChangedAction.Replace:
-						break;
-					case NotifyCollectionChangedAction.Move:
-						break;
-					case NotifyCollectionChangedAction.Reset:
-						for (int i = endIndex - 1; i >= startIndex; i--)
-						{
-							MenuItem item = (MenuItem)windowMenuItem.Items[0];
-							item.Tag = null;
-							windowMenuItem.Items.RemoveAt(i);
-							endIndex--;
-						}
-						insertionIndex = endIndex;
-						foreach (ToolPaneModel pane in dock.ToolPanes)
-						{
-							MenuItem menuItem = CreateMenuItem(pane);
-							windowMenuItem.Items.Insert(insertionIndex, menuItem);
-							insertionIndex++;
-						}
-						break;
-				}
-
-				MenuItem CreateMenuItem(ToolPaneModel pane)
-				{
-					MenuItem menuItem = new MenuItem();
-					menuItem.Command = pane.AssociatedCommand ?? new ToolPaneCommand(pane.ContentId);
-					menuItem.Header = pane.Title;
-					menuItem.Tag = pane;
-					var shortcutKey = pane.ShortcutKey;
-					if (shortcutKey != null)
-					{
-						InputBindings.Add(new InputBinding(menuItem.Command, shortcutKey));
-						menuItem.InputGestureText = shortcutKey.GetDisplayStringForCulture(System.Globalization.CultureInfo.CurrentUICulture);
-					}
-					if (!string.IsNullOrEmpty(pane.Icon))
-					{
-						menuItem.Icon = new Image {
-							Width = 16,
-							Height = 16,
-							Source = Images.Load(pane, pane.Icon)
-						};
-					}
-
-					return menuItem;
-				}
-			}
-
-			void TabsChanged(object? sender, NotifyCollectionChangedEventArgs e)
-			{
-				int endIndex = windowMenuItem.Items.Count;
-				int startIndex = windowMenuItem.Items.IndexOf(separatorBeforeDocuments) + 1;
-				int insertionIndex;
-				switch (e.Action)
-				{
-					case NotifyCollectionChangedAction.Add:
-						insertionIndex = Math.Min(endIndex, startIndex + e.NewStartingIndex);
-						foreach (TabPageModel pane in e.NewItems)
-						{
-							MenuItem menuItem = CreateMenuItem(pane);
-							pane.PropertyChanged += TabPageChanged;
-							windowMenuItem.Items.Insert(insertionIndex, menuItem);
-							insertionIndex++;
-						}
-						break;
-					case NotifyCollectionChangedAction.Remove:
-						foreach (TabPageModel pane in e.OldItems)
-						{
-							for (int i = endIndex - 1; i >= startIndex; i--)
-							{
-								MenuItem item = (MenuItem)windowMenuItem.Items[i];
-								if (pane == item.Tag)
-								{
-									windowMenuItem.Items.RemoveAt(i);
-									pane.PropertyChanged -= TabPageChanged;
-									item.Tag = null;
-									endIndex--;
-									break;
-								}
-							}
-						}
-						break;
-					case NotifyCollectionChangedAction.Replace:
-						break;
-					case NotifyCollectionChangedAction.Move:
-						break;
-					case NotifyCollectionChangedAction.Reset:
-						for (int i = endIndex - 1; i >= startIndex; i--)
-						{
-							MenuItem item = (MenuItem)windowMenuItem.Items[i];
-							windowMenuItem.Items.RemoveAt(i);
-							((TabPageModel)item.Tag).PropertyChanged -= TabPageChanged;
-							endIndex--;
-						}
-						insertionIndex = endIndex;
-						foreach (TabPageModel pane in dock.TabPages)
-						{
-							MenuItem menuItem = CreateMenuItem(pane);
-							pane.PropertyChanged += TabPageChanged;
-							windowMenuItem.Items.Insert(insertionIndex, menuItem);
-							insertionIndex++;
-						}
-						break;
-				}
-
-				MenuItem CreateMenuItem(TabPageModel pane)
-				{
-					MenuItem menuItem = new MenuItem();
-					menuItem.Command = new TabPageCommand(pane);
-					menuItem.Header = pane.Title.Length > 20 ? pane.Title.Substring(20) + "..." : pane.Title;
-					menuItem.Tag = pane;
-					menuItem.IsCheckable = true;
-
-					return menuItem;
-				}
-			}
-
-			static void TabPageChanged(object? sender, PropertyChangedEventArgs e)
-			{
-				var windowMenuItem = Instance.mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == nameof(Properties.Resources._Window));
-				foreach (MenuItem menuItem in windowMenuItem.Items.OfType<MenuItem>())
-				{
-					if (menuItem.IsCheckable && menuItem.Tag == sender)
-					{
-						string title = ((TabPageModel)sender).Title;
-						menuItem.Header = title.Length > 20 ? title.Substring(0, 20) + "..." : title;
-					}
-				}
-			}
-		}
-
-		public void ShowInTopPane(string title, object content)
-		{
-			var model = DockWorkspace.Instance.ToolPanes.OfType<LegacyToolPaneModel>().FirstOrDefault(p => p.Content == content);
-			if (model == null)
-			{
-				model = new LegacyToolPaneModel(title, content, LegacyToolPaneLocation.Top);
-				DockWorkspace.Instance.ToolPanes.Add(model);
-			}
-			model.Show();
-		}
-
-		public void ShowInBottomPane(string title, object content)
-		{
-			var model = DockWorkspace.Instance.ToolPanes.OfType<LegacyToolPaneModel>().FirstOrDefault(p => p.Content == content);
-			if (model == null)
-			{
-				model = new LegacyToolPaneModel(title, content, LegacyToolPaneLocation.Bottom);
-				DockWorkspace.Instance.ToolPanes.Add(model);
-			}
-			model.Show();
-		}
-		#endregion
-
 		#region File Loader extensibility
 
 		void InitFileLoaders()
@@ -549,27 +99,22 @@ namespace ICSharpCode.ILSpy
 		#endregion
 
 		#region Message Hook
+
 		protected override void OnSourceInitialized(EventArgs e)
 		{
 			base.OnSourceInitialized(e);
-			PresentationSource source = PresentationSource.FromVisual(this);
+
+			var source = PresentationSource.FromVisual(this);
 
 			var sessionSettings = SettingsService.Instance.SessionSettings;
 
 			// Validate and Set Window Bounds
-			Rect bounds = Rect.Transform(sessionSettings.WindowBounds, source.CompositionTarget.TransformToDevice);
-			var boundsRect = new System.Drawing.Rectangle((int)bounds.Left, (int)bounds.Top, (int)bounds.Width, (int)bounds.Height);
-			bool boundsOK = false;
-			foreach (var screen in System.Windows.Forms.Screen.AllScreens)
-			{
-				var intersection = System.Drawing.Rectangle.Intersect(boundsRect, screen.WorkingArea);
-				if (intersection.Width > 10 && intersection.Height > 10)
-					boundsOK = true;
-			}
-			if (boundsOK)
-				SetWindowBounds(sessionSettings.WindowBounds);
-			else
-				SetWindowBounds(SessionSettings.DefaultWindowBounds);
+			var windowBounds = Rect.Transform(sessionSettings.WindowBounds, source?.CompositionTarget?.TransformToDevice ?? Matrix.Identity);
+			var boundsRect = new Rectangle((int)windowBounds.Left, (int)windowBounds.Top, (int)windowBounds.Width, (int)windowBounds.Height);
+
+			bool areBoundsValid = Screen.AllScreens.Any(screen => Rectangle.Intersect(boundsRect, screen.WorkingArea) is { Width: > 10, Height: > 10 });
+
+			SetWindowBounds(areBoundsValid ? sessionSettings.WindowBounds : SessionSettings.DefaultWindowBounds);
 
 			this.WindowState = sessionSettings.WindowState;
 		}
@@ -599,281 +144,11 @@ namespace ICSharpCode.ILSpy
 			}
 		}
 
-		public AssemblyList CurrentAssemblyList {
-			get { return assemblyList; }
-		}
-
-		List<LoadedAssembly> commandLineLoadedAssemblies = new List<LoadedAssembly>();
-
-		internal async Task HandleSingleInstanceCommandLineArguments(string[] args)
-		{
-			var cmdArgs = CommandLineArguments.Create(args);
-
-			await Dispatcher.InvokeAsync(() => {
-				if (HandleCommandLineArguments(cmdArgs))
-				{
-					if (!cmdArgs.NoActivate && WindowState == WindowState.Minimized)
-						WindowState = WindowState.Normal;
-
-					HandleCommandLineArgumentsAfterShowList(cmdArgs);
-				}
-			});
-		}
-
-		bool HandleCommandLineArguments(CommandLineArguments args)
-		{
-			LoadAssemblies(args.AssembliesToLoad, commandLineLoadedAssemblies, focusNode: false);
-			if (args.Language != null)
-				SettingsService.Instance.SessionSettings.LanguageSettings.Language = Languages.GetLanguage(args.Language);
-			return true;
-		}
-
-		/// <summary>
-		/// Called on startup or when passed arguments via WndProc from a second instance.
-		/// In the format case, spySettings is non-null; in the latter it is null.
-		/// </summary>
-		void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ILSpySettings? spySettings = null)
-		{
-			var sessionSettings = SettingsService.Instance.SessionSettings;
-
-			var relevantAssemblies = commandLineLoadedAssemblies.ToList();
-			commandLineLoadedAssemblies.Clear(); // clear references once we don't need them anymore
-			NavigateOnLaunch(args.NavigateTo, sessionSettings.ActiveTreeViewPath, spySettings, relevantAssemblies);
-			if (args.Search != null)
-			{
-				var searchPane = App.ExportProvider.GetExportedValue<SearchPaneModel>();
-
-				searchPane.SearchTerm = args.Search;
-				searchPane.Show();
-			}
-		}
-
-		async void NavigateOnLaunch(string navigateTo, string[] activeTreeViewPath, ILSpySettings? spySettings, List<LoadedAssembly> relevantAssemblies)
-		{
-			var initialSelection = AssemblyTreeView.SelectedItem;
-			if (navigateTo != null)
-			{
-				bool found = false;
-				if (navigateTo.StartsWith("N:", StringComparison.Ordinal))
-				{
-					string namespaceName = navigateTo.Substring(2);
-					foreach (LoadedAssembly asm in relevantAssemblies)
-					{
-						AssemblyTreeNode? asmNode = assemblyListTreeNode.FindAssemblyNode(asm);
-						if (asmNode != null)
-						{
-							// FindNamespaceNode() blocks the UI if the assembly is not yet loaded,
-							// so use an async wait instead.
-							await asm.GetMetadataFileAsync().Catch<Exception>(ex => { });
-							NamespaceTreeNode? nsNode = asmNode.FindNamespaceNode(namespaceName);
-							if (nsNode != null)
-							{
-								found = true;
-								if (AssemblyTreeView.SelectedItem == initialSelection)
-								{
-									SelectNode(nsNode);
-								}
-								break;
-							}
-						}
-					}
-				}
-				else if (navigateTo == "none")
-				{
-					// Don't navigate anywhere; start empty.
-					// Used by ILSpy VS addin, it'll send us the real location to navigate to via IPC.
-					found = true;
-				}
-				else
-				{
-					IEntity? mr = await Task.Run(() => FindEntityInRelevantAssemblies(navigateTo, relevantAssemblies));
-					// Make sure we wait for assemblies being loaded...
-					// BeginInvoke in LoadedAssembly.LookupReferencedAssemblyInternal
-					await Dispatcher.InvokeAsync(delegate { }, DispatcherPriority.Normal);
-					if (mr != null && mr.ParentModule.MetadataFile != null)
-					{
-						found = true;
-						if (AssemblyTreeView.SelectedItem == initialSelection)
-						{
-							JumpToReference(mr);
-						}
-					}
-				}
-				if (!found && AssemblyTreeView.SelectedItem == initialSelection)
-				{
-					AvalonEditTextOutput output = new AvalonEditTextOutput();
-					output.Write(string.Format("Cannot find '{0}' in command line specified assemblies.", navigateTo));
-					DockWorkspace.Instance.ShowText(output);
-				}
-			}
-			else if (relevantAssemblies.Count == 1)
-			{
-				// NavigateTo == null and an assembly was given on the command-line:
-				// Select the newly loaded assembly
-				AssemblyTreeNode? asmNode = assemblyListTreeNode.FindAssemblyNode(relevantAssemblies[0]);
-				if (asmNode != null && AssemblyTreeView.SelectedItem == initialSelection)
-				{
-					SelectNode(asmNode);
-				}
-			}
-			else if (spySettings != null)
-			{
-				SharpTreeNode? node = null;
-				if (activeTreeViewPath?.Length > 0)
-				{
-					foreach (var asm in CurrentAssemblyList.GetAssemblies())
-					{
-						if (asm.FileName == activeTreeViewPath[0])
-						{
-							// FindNodeByPath() blocks the UI if the assembly is not yet loaded,
-							// so use an async wait instead.
-							await asm.GetMetadataFileAsync().Catch<Exception>(ex => { });
-						}
-					}
-					node = FindNodeByPath(activeTreeViewPath, true);
-				}
-				if (AssemblyTreeView.SelectedItem == initialSelection)
-				{
-					if (node != null)
-					{
-						SelectNode(node);
-
-						// only if not showing the about page, perform the update check:
-						await ShowMessageIfUpdatesAvailableAsync(spySettings);
-					}
-					else
-					{
-						DockWorkspace.Instance.ActiveTabPage.ShowTextView(AboutPage.Display);
-					}
-				}
-			}
-		}
-
-		internal static IEntity? FindEntityInRelevantAssemblies(string navigateTo, IEnumerable<LoadedAssembly> relevantAssemblies)
-		{
-			ITypeReference? typeRef = null;
-			IMemberReference? memberRef = null;
-			if (navigateTo.StartsWith("T:", StringComparison.Ordinal))
-			{
-				typeRef = IdStringProvider.ParseTypeName(navigateTo);
-			}
-			else
-			{
-				memberRef = IdStringProvider.ParseMemberIdString(navigateTo);
-				typeRef = memberRef.DeclaringTypeReference;
-			}
-			foreach (LoadedAssembly asm in relevantAssemblies.ToList())
-			{
-				var module = asm.GetMetadataFileOrNull();
-				if (CanResolveTypeInPEFile(module, typeRef, out var typeHandle))
-				{
-					ICompilation compilation = typeHandle.Kind == HandleKind.ExportedType
-						? new DecompilerTypeSystem(module, module.GetAssemblyResolver())
-						: new SimpleCompilation((PEFile)module, MinimalCorlib.Instance);
-					return memberRef == null
-						? typeRef.Resolve(new SimpleTypeResolveContext(compilation)) as ITypeDefinition
-						: (IEntity?)memberRef.Resolve(new SimpleTypeResolveContext(compilation));
-				}
-			}
-			return null;
-		}
-
-		static bool CanResolveTypeInPEFile(MetadataFile module, ITypeReference typeRef, out EntityHandle typeHandle)
-		{
-			// We intentionally ignore reference assemblies, so that the loop continues looking for another assembly that might have a usable definition.
-			if (module.IsReferenceAssembly())
-			{
-				typeHandle = default;
-				return false;
-			}
-
-			switch (typeRef)
-			{
-				case GetPotentiallyNestedClassTypeReference topLevelType:
-					typeHandle = topLevelType.ResolveInPEFile(module);
-					return !typeHandle.IsNil;
-				case NestedTypeReference nestedType:
-					if (!CanResolveTypeInPEFile(module, nestedType.DeclaringTypeReference, out typeHandle))
-						return false;
-					if (typeHandle.Kind == HandleKind.ExportedType)
-						return true;
-					var typeDef = module.Metadata.GetTypeDefinition((TypeDefinitionHandle)typeHandle);
-					typeHandle = typeDef.GetNestedTypes().FirstOrDefault(t => {
-						var td = module.Metadata.GetTypeDefinition(t);
-						var typeName = ReflectionHelper.SplitTypeParameterCountFromReflectionName(module.Metadata.GetString(td.Name), out int typeParameterCount);
-						return nestedType.AdditionalTypeParameterCount == typeParameterCount && nestedType.Name == typeName;
-					});
-					return !typeHandle.IsNil;
-				default:
-					typeHandle = default;
-					return false;
-			}
-		}
-
-		void MainWindow_Loaded(object sender, RoutedEventArgs e)
-		{
-			DockWorkspace.Instance.TabPages.Add();
-
-			var loadPreviousAssemblies = Options.MiscSettingsPanel.CurrentMiscSettings.LoadPreviousAssemblies;
-
-			var sessionSettings = SettingsService.Instance.SessionSettings;
-
-			if (loadPreviousAssemblies)
-			{
-				// Load AssemblyList only in Loaded event so that WPF is initialized before we start the CPU-heavy stuff.
-				// This makes the UI come up a bit faster.
-				this.assemblyList = SettingsService.Instance.AssemblyListManager.LoadList(sessionSettings.ActiveAssemblyList);
-			}
-			else
-			{
-				SettingsService.Instance.AssemblyListManager.ClearAll();
-				this.assemblyList = SettingsService.Instance.AssemblyListManager.CreateList(AssemblyListManager.DefaultListName);
-			}
-
-			HandleCommandLineArguments(App.CommandLineArguments);
-
-			if (assemblyList.GetAssemblies().Length == 0
-				&& assemblyList.ListName == AssemblyListManager.DefaultListName
-				&& loadPreviousAssemblies)
-			{
-				LoadInitialAssemblies();
-			}
-
-			ShowAssemblyList(this.assemblyList);
-
-			if (sessionSettings.ActiveAutoLoadedAssembly != null
-				&& File.Exists(sessionSettings.ActiveAutoLoadedAssembly))
-			{
-				this.assemblyList.Open(sessionSettings.ActiveAutoLoadedAssembly, true);
-			}
-
-			Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => OpenAssemblies(SettingsService.Instance.SpySettings)));
-		}
-
-		void OpenAssemblies(ILSpySettings spySettings)
-		{
-			HandleCommandLineArgumentsAfterShowList(App.CommandLineArguments, spySettings);
-
-			AvalonEditTextOutput output = new AvalonEditTextOutput();
-			if (FormatExceptions(App.StartupExceptions.ToArray(), output))
-				DockWorkspace.Instance.ShowText(output);
-		}
-
-		static bool FormatExceptions(App.ExceptionData[] exceptions, ITextOutput output)
-		{
-			var stringBuilder = new StringBuilder();
-			var result = exceptions.FormatExceptions(stringBuilder);
-			if (result)
-			{
-				output.Write(stringBuilder.ToString());
-			}
-			return result;
-		}
-
 		#region Update Check
+
 		string updateAvailableDownloadUrl;
 
-		public async Task ShowMessageIfUpdatesAvailableAsync(ILSpySettings spySettings, bool forceCheck = false)
+		public async Task ShowMessageIfUpdatesAvailableAsync(ISettingsProvider spySettings, bool forceCheck = false)
 		{
 			string? downloadUrl;
 			if (forceCheck)
@@ -889,21 +164,21 @@ namespace ICSharpCode.ILSpy
 			AdjustUpdateUIAfterCheck(downloadUrl, forceCheck);
 		}
 
-		void updatePanelCloseButtonClick(object sender, RoutedEventArgs e)
+		void UpdatePanelCloseButtonClick(object sender, RoutedEventArgs e)
 		{
 			updatePanel.Visibility = Visibility.Collapsed;
 		}
 
-		async void downloadOrCheckUpdateButtonClick(object sender, RoutedEventArgs e)
+		async void DownloadOrCheckUpdateButtonClick(object sender, RoutedEventArgs e)
 		{
 			if (updateAvailableDownloadUrl != null)
 			{
-				MainWindow.OpenLink(updateAvailableDownloadUrl);
+				OpenLink(updateAvailableDownloadUrl);
 			}
 			else
 			{
 				updatePanel.Visibility = Visibility.Collapsed;
-				string? downloadUrl = await NotifyOfUpdatesStrategy.CheckForUpdatesAsync(ILSpySettings.Load());
+				string? downloadUrl = await NotifyOfUpdatesStrategy.CheckForUpdatesAsync(SettingsService.Instance.SpySettings);
 				AdjustUpdateUIAfterCheck(downloadUrl, true);
 			}
 		}
@@ -923,327 +198,8 @@ namespace ICSharpCode.ILSpy
 				downloadOrCheckUpdateButton.Content = Properties.Resources.CheckAgain;
 			}
 		}
-		#endregion
-
-		public void ShowAssemblyList(string name)
-		{
-			AssemblyList list = SettingsService.Instance.AssemblyListManager.LoadList(name);
-			//Only load a new list when it is a different one
-			if (list.ListName != CurrentAssemblyList.ListName)
-			{
-				ShowAssemblyList(list);
-				SelectNode(AssemblyTreeView.Root);
-			}
-		}
 
-		void ShowAssemblyList(AssemblyList assemblyList)
-		{
-			history.Clear();
-			if (this.assemblyList != null)
-			{
-				this.assemblyList.CollectionChanged -= assemblyList_Assemblies_CollectionChanged;
-			}
-
-			this.assemblyList = assemblyList;
-			assemblyList.CollectionChanged += assemblyList_Assemblies_CollectionChanged;
-
-			assemblyListTreeNode = new AssemblyListTreeNode(assemblyList);
-			assemblyListTreeNode.Select = x => SelectNode(x, inNewTabPage: false);
-			AssemblyTreeView.Root = assemblyListTreeNode;
-
-			if (assemblyList.ListName == AssemblyListManager.DefaultListName)
-#if DEBUG
-				this.Title = $"ILSpy {DecompilerVersionInfo.FullVersion}";
-#else
-				this.Title = "ILSpy";
-#endif
-			else
-#if DEBUG
-				this.Title = $"ILSpy {DecompilerVersionInfo.FullVersion} - " + assemblyList.ListName;
-#else
-				this.Title = "ILSpy - " + assemblyList.ListName;
-#endif
-		}
-
-		void assemblyList_Assemblies_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
-		{
-			if (e.Action == NotifyCollectionChangedAction.Reset)
-			{
-				history.RemoveAll(_ => true);
-			}
-			if (e.OldItems != null)
-			{
-				var oldAssemblies = new HashSet<LoadedAssembly>(e.OldItems.Cast<LoadedAssembly>());
-				history.RemoveAll(n => n.TreeNodes.Any(
-					nd => nd.AncestorsAndSelf().OfType<AssemblyTreeNode>().Any(
-						a => oldAssemblies.Contains(a.LoadedAssembly))));
-			}
-
-			MessageBus.Send(this, new CurrentAssemblyListChangedEventArgs(e));
-		}
-
-		void LoadInitialAssemblies()
-		{
-			// Called when loading an empty assembly list; so that
-			// the user can see something initially.
-			System.Reflection.Assembly[] initialAssemblies = {
-				typeof(object).Assembly,
-				typeof(Uri).Assembly,
-				typeof(System.Linq.Enumerable).Assembly,
-				typeof(System.Xml.XmlDocument).Assembly,
-				typeof(System.Windows.Markup.MarkupExtension).Assembly,
-				typeof(System.Windows.Rect).Assembly,
-				typeof(System.Windows.UIElement).Assembly,
-				typeof(System.Windows.FrameworkElement).Assembly
-			};
-			foreach (System.Reflection.Assembly asm in initialAssemblies)
-				assemblyList.OpenAssembly(asm.Location);
-		}
-
-		void LanguageSettings_PropertyChanged(object sender, PropertyChangedEventArgs e)
-		{
-			if (e.PropertyName == "Language" || e.PropertyName == "LanguageVersion")
-			{
-				DecompileSelectedNodes(recordHistory: false);
-			}
-		}
-
-		internal AssemblyListTreeNode AssemblyListTreeNode {
-			get { return assemblyListTreeNode; }
-		}
-
-		#region Node Selection
-
-		public void SelectNode(SharpTreeNode obj)
-		{
-			SelectNode(obj, false);
-		}
-
-		public void SelectNode(SharpTreeNode obj, bool inNewTabPage)
-		{
-			SelectNode(obj, inNewTabPage, true);
-		}
-
-		public void SelectNode(SharpTreeNode obj, bool inNewTabPage, bool setFocus)
-		{
-			if (obj != null)
-			{
-				if (!obj.AncestorsAndSelf().Any(node => node.IsHidden))
-				{
-					if (inNewTabPage)
-					{
-						DockWorkspace.Instance.TabPages.Add();
-						AssemblyTreeView.SelectedItem = null;
-					}
-
-					// Set both the selection and focus to ensure that keyboard navigation works as expected.
-					if (setFocus)
-					{
-						AssemblyTreeView.FocusNode(obj);
-					}
-					else
-					{
-						AssemblyTreeView.ScrollIntoView(obj);
-					}
-					AssemblyTreeView.SelectedItem = obj;
-				}
-				else
-				{
-					MessageBox.Show(Properties.Resources.NavigationFailed, "ILSpy", MessageBoxButton.OK, MessageBoxImage.Exclamation);
-				}
-			}
-		}
-
-		public void SelectNodes(IEnumerable<SharpTreeNode> nodes)
-		{
-			SelectNodes(nodes, false);
-		}
-
-		public void SelectNodes(IEnumerable<SharpTreeNode> nodes, bool inNewTabPage)
-		{
-			SelectNodes(nodes, inNewTabPage, true);
-		}
-
-		public void SelectNodes(IEnumerable<SharpTreeNode> nodes, bool inNewTabPage, bool setFocus)
-		{
-			SelectNodes(nodes, inNewTabPage, setFocus, false);
-		}
-
-		internal void SelectNodes(IEnumerable<SharpTreeNode> nodes, bool inNewTabPage,
-			bool setFocus, bool changingActiveTab)
-		{
-			if (inNewTabPage)
-			{
-				DockWorkspace.Instance.TabPages.Add();
-			}
-
-			// Ensure nodes exist
-			var nodesList = nodes.Select(n => FindNodeByPath(GetPathForNode(n), true))
-				.Where(n => n != null).ToArray();
-
-			if (!nodesList.Any() || !nodesList.All(n => !n.AncestorsAndSelf().Any(a => a.IsHidden)))
-			{
-				return;
-			}
-
-			this.changingActiveTab = changingActiveTab || inNewTabPage;
-			try
-			{
-				if (setFocus)
-				{
-					AssemblyTreeView.FocusNode(nodesList[0]);
-				}
-				else
-				{
-					AssemblyTreeView.ScrollIntoView(nodesList[0]);
-				}
-
-				AssemblyTreeView.SetSelectedNodes(nodesList);
-			}
-			finally
-			{
-				this.changingActiveTab = false;
-			}
-		}
-
-		/// <summary>
-		/// Retrieves a node using the .ToString() representations of its ancestors.
-		/// </summary>
-		public SharpTreeNode? FindNodeByPath(string[] path, bool returnBestMatch)
-		{
-			if (path == null)
-				return null;
-			SharpTreeNode? node = AssemblyTreeView.Root;
-			SharpTreeNode? bestMatch = node;
-			foreach (var element in path)
-			{
-				if (node == null)
-					break;
-				bestMatch = node;
-				node.EnsureLazyChildren();
-				var ilSpyTreeNode = node as ILSpyTreeNode;
-				if (ilSpyTreeNode != null)
-					ilSpyTreeNode.EnsureChildrenFiltered();
-				node = node.Children.FirstOrDefault(c => c.ToString() == element);
-			}
-			if (returnBestMatch)
-				return node ?? bestMatch;
-			else
-				return node;
-		}
-
-		/// <summary>
-		/// Gets the .ToString() representation of the node's ancestors.
-		/// </summary>
-		public static string[]? GetPathForNode(SharpTreeNode node)
-		{
-			if (node == null)
-				return null;
-			List<string> path = new List<string>();
-			while (node.Parent != null)
-			{
-				path.Add(node.ToString());
-				node = node.Parent;
-			}
-			path.Reverse();
-			return path.ToArray();
-		}
-
-		public ILSpyTreeNode? FindTreeNode(object reference)
-		{
-			switch (reference)
-			{
-				case LoadedAssembly lasm:
-					return assemblyListTreeNode.FindAssemblyNode(lasm);
-				case MetadataFile asm:
-					return assemblyListTreeNode.FindAssemblyNode(asm);
-				case Resource res:
-					return assemblyListTreeNode.FindResourceNode(res);
-				case ValueTuple<Resource, string> resName:
-					return assemblyListTreeNode.FindResourceNode(resName.Item1, resName.Item2);
-				case ITypeDefinition type:
-					return assemblyListTreeNode.FindTypeNode(type);
-				case IField fd:
-					return assemblyListTreeNode.FindFieldNode(fd);
-				case IMethod md:
-					return assemblyListTreeNode.FindMethodNode(md);
-				case IProperty pd:
-					return assemblyListTreeNode.FindPropertyNode(pd);
-				case IEvent ed:
-					return assemblyListTreeNode.FindEventNode(ed);
-				case INamespace nd:
-					return AssemblyListTreeNode.FindNamespaceNode(nd);
-				default:
-					return null;
-			}
-		}
-
-		public void JumpToReference(object reference)
-		{
-			JumpToReference(reference, inNewTabPage: false);
-		}
-
-		public void JumpToReference(object reference, bool inNewTabPage)
-		{
-			JumpToReferenceAsync(reference, inNewTabPage).HandleExceptions();
-		}
-
-		/// <summary>
-		/// Jumps to the specified reference.
-		/// </summary>
-		/// <returns>
-		/// Returns a task that will signal completion when the decompilation of the jump target has finished.
-		/// The task will be marked as canceled if the decompilation is canceled.
-		/// </returns>
-		public Task JumpToReferenceAsync(object reference)
-		{
-			return JumpToReferenceAsync(reference, inNewTabPage: false);
-		}
-
-		public Task JumpToReferenceAsync(object reference, bool inNewTabPage)
-		{
-			decompilationTask = TaskHelper.CompletedTask;
-			switch (reference)
-			{
-				case Decompiler.Disassembler.OpCodeInfo opCode:
-					OpenLink(opCode.Link);
-					break;
-				case EntityReference unresolvedEntity:
-					string protocol = unresolvedEntity.Protocol ?? "decompile";
-					var file = unresolvedEntity.ResolveAssembly(assemblyList);
-					if (file == null)
-					{
-						break;
-					}
-					if (protocol != "decompile")
-					{
-						var protocolHandlers = App.ExportProvider.GetExports<IProtocolHandler>();
-						foreach (var handler in protocolHandlers)
-						{
-							var node = handler.Value.Resolve(protocol, file, unresolvedEntity.Handle, out bool newTabPage);
-							if (node != null)
-							{
-								SelectNode(node, newTabPage || inNewTabPage);
-								return decompilationTask;
-							}
-						}
-					}
-					var possibleToken = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolvedEntity.Handle));
-					if (possibleToken != null)
-					{
-						var typeSystem = new DecompilerTypeSystem(file, file.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached);
-						reference = typeSystem.MainModule.ResolveEntity(possibleToken.Value);
-						goto default;
-					}
-					break;
-				default:
-					ILSpyTreeNode? treeNode = FindTreeNode(reference);
-					if (treeNode != null)
-						SelectNode(treeNode, inNewTabPage);
-					break;
-			}
-			return decompilationTask;
-		}
+		#endregion
 
 		public static void OpenLink(string link)
 		{
@@ -1274,356 +230,44 @@ namespace ICSharpCode.ILSpy
 				// just ignore all of them.
 			}
 		}
-		#endregion
-
-		#region Open/Refresh
-		void OpenCommandExecuted(object sender, ExecutedRoutedEventArgs e)
-		{
-			e.Handled = true;
-			OpenFileDialog dlg = new OpenFileDialog();
-			dlg.Filter = ".NET assemblies|*.dll;*.exe;*.winmd;*.wasm|Nuget Packages (*.nupkg)|*.nupkg|Portable Program Database (*.pdb)|*.pdb|All files|*.*";
-			dlg.Multiselect = true;
-			dlg.RestoreDirectory = true;
-			if (dlg.ShowDialog() == true)
-			{
-				OpenFiles(dlg.FileNames);
-			}
-		}
-
-		public void OpenFiles(string[] fileNames, bool focusNode = true)
-		{
-			if (fileNames == null)
-				throw new ArgumentNullException(nameof(fileNames));
-
-			if (focusNode)
-				AssemblyTreeView.UnselectAll();
-
-			LoadAssemblies(fileNames, focusNode: focusNode);
-		}
-
-		void LoadAssemblies(IEnumerable<string> fileNames, List<LoadedAssembly>? loadedAssemblies = null, bool focusNode = true)
-		{
-			SharpTreeNode? lastNode = null;
-			foreach (string file in fileNames)
-			{
-				var asm = assemblyList.OpenAssembly(file);
-				if (asm != null)
-				{
-					if (loadedAssemblies != null)
-					{
-						loadedAssemblies.Add(asm);
-					}
-					else
-					{
-						var node = assemblyListTreeNode.FindAssemblyNode(asm);
-						if (node != null && focusNode)
-						{
-							AssemblyTreeView.SelectedItems.Add(node);
-							lastNode = node;
-						}
-					}
-				}
-
-				if (lastNode != null && focusNode)
-					AssemblyTreeView.FocusNode(lastNode);
-			}
-		}
-
-		void RefreshCommandExecuted(object sender, ExecutedRoutedEventArgs e)
-		{
-			try
-			{
-				refreshInProgress = true;
-				var path = GetPathForNode(AssemblyTreeView.SelectedItem as SharpTreeNode);
-				ShowAssemblyList(SettingsService.Instance.AssemblyListManager.LoadList(assemblyList.ListName));
-				SelectNode(FindNodeByPath(path, true), inNewTabPage: false, AssemblyTreeView.IsFocused);
-			}
-			finally
-			{
-				refreshInProgress = false;
-			}
-		}
-
-		void SearchCommandExecuted(object sender, ExecutedRoutedEventArgs e)
-		{
-			DockWorkspace.Instance.ShowToolPane(SearchPaneModel.PaneContentId);
-		}
-		#endregion
-
-		#region Decompile (TreeView_SelectionChanged)
-		bool delayDecompilationRequestDueToContextMenu;
-
-		protected override void OnContextMenuClosing(ContextMenuEventArgs e)
-		{
-			base.OnContextMenuClosing(e);
-
-			if (delayDecompilationRequestDueToContextMenu)
-			{
-				delayDecompilationRequestDueToContextMenu = false;
-				var state = DockWorkspace.Instance.ActiveTabPage.GetState() as DecompilerTextViewState;
-				DecompileSelectedNodes(state);
-			}
-		}
-
-		void TreeView_SelectionChanged(object sender, SelectionChangedEventArgs e)
-		{
-			DecompilerTextViewState? state = null;
-			if (refreshInProgress || changingActiveTab)
-			{
-				state = DockWorkspace.Instance.ActiveTabPage.GetState() as DecompilerTextViewState;
-			}
-
-			this.delayDecompilationRequestDueToContextMenu = Mouse.RightButton == MouseButtonState.Pressed;
-			if (!changingActiveTab && !delayDecompilationRequestDueToContextMenu)
-			{
-				DecompileSelectedNodes(state);
-			}
-
-			SelectionChanged?.Invoke(sender, e);
-		}
-
-		Task decompilationTask;
-		bool ignoreDecompilationRequests;
-
-		void DecompileSelectedNodes(DecompilerTextViewState? newState = null, bool recordHistory = true)
-		{
-			if (ignoreDecompilationRequests)
-				return;
-
-			if (AssemblyTreeView.SelectedItems.Count == 0 && refreshInProgress)
-				return;
-
-			if (recordHistory)
-			{
-				var tabPage = DockWorkspace.Instance.ActiveTabPage;
-				var currentState = tabPage.GetState();
-				if (currentState != null)
-					history.UpdateCurrent(new NavigationState(tabPage, currentState));
-				history.Record(new NavigationState(tabPage, AssemblyTreeView.SelectedItems.OfType<SharpTreeNode>()));
-			}
-
-			DockWorkspace.Instance.ActiveTabPage.SupportsLanguageSwitching = true;
-
-			if (AssemblyTreeView.SelectedItems.Count == 1)
-			{
-				ILSpyTreeNode? node = AssemblyTreeView.SelectedItem as ILSpyTreeNode;
-				if (node != null && node.View(DockWorkspace.Instance.ActiveTabPage))
-					return;
-			}
-			if (newState?.ViewedUri != null)
-			{
-				NavigateTo(new RequestNavigateEventArgs(newState.ViewedUri, null), recordHistory: false);
-				return;
-			}
-			var options = MainWindow.Instance.CreateDecompilationOptions();
-			options.TextViewState = newState;
-			decompilationTask = DockWorkspace.Instance.ActiveTabPage.ShowTextViewAsync(
-				textView => textView.DecompileAsync(this.CurrentLanguage, this.SelectedNodes, options)
-			);
-		}
-
-		void SaveCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
-		{
-			e.Handled = true;
-			e.CanExecute = SaveCodeContextMenuEntry.CanExecute(SelectedNodes.ToList());
-		}
-
-		void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e)
-		{
-			SaveCodeContextMenuEntry.Execute(SelectedNodes.ToList());
-		}
-
-		public void RefreshDecompiledView()
-		{
-			try
-			{
-				refreshInProgress = true;
-				DecompileSelectedNodes();
-			}
-			finally
-			{
-				refreshInProgress = false;
-			}
-		}
-
-		public Language CurrentLanguage => SettingsService.Instance.SessionSettings.LanguageSettings.Language;
-
-		public LanguageVersion? CurrentLanguageVersion => SettingsService.Instance.SessionSettings.LanguageSettings.LanguageVersion;
-
-		public event SelectionChangedEventHandler SelectionChanged;
-
-		public IEnumerable<ILSpyTreeNode> SelectedNodes {
-			get {
-				return AssemblyTreeView.GetTopLevelSelection().OfType<ILSpyTreeNode>();
-			}
-		}
-		#endregion
-
-		#region Back/Forward navigation
-		void BackCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
-		{
-			e.Handled = true;
-			e.CanExecute = history.CanNavigateBack;
-		}
-
-		void BackCommandExecuted(object sender, ExecutedRoutedEventArgs e)
-		{
-			if (history.CanNavigateBack)
-			{
-				e.Handled = true;
-				NavigateHistory(false);
-			}
-		}
-
-		void ForwardCommandCanExecute(object sender, CanExecuteRoutedEventArgs e)
-		{
-			e.Handled = true;
-			e.CanExecute = history.CanNavigateForward;
-		}
-
-		void ForwardCommandExecuted(object sender, ExecutedRoutedEventArgs e)
-		{
-			if (history.CanNavigateForward)
-			{
-				e.Handled = true;
-				NavigateHistory(true);
-			}
-		}
-
-		void NavigateHistory(bool forward)
-		{
-			TabPageModel tabPage = DockWorkspace.Instance.ActiveTabPage;
-			var state = tabPage.GetState();
-			if (state != null)
-				history.UpdateCurrent(new NavigationState(tabPage, state));
-			var newState = forward ? history.GoForward() : history.GoBack();
-
-			ignoreDecompilationRequests = true;
-			AssemblyTreeView.SelectedItems.Clear();
-			DockWorkspace.Instance.ActiveTabPage = newState.TabPage;
-			foreach (var node in newState.TreeNodes)
-			{
-				AssemblyTreeView.SelectedItems.Add(node);
-			}
-			if (newState.TreeNodes.Any())
-				AssemblyTreeView.FocusNode(newState.TreeNodes.First());
-			ignoreDecompilationRequests = false;
-			DecompileSelectedNodes(newState.ViewState as DecompilerTextViewState, false);
-		}
-		#endregion
-
-		internal void NavigateTo(RequestNavigateEventArgs e, bool recordHistory = true, bool inNewTabPage = false)
-		{
-			if (e.Uri.Scheme == "resource")
-			{
-				if (inNewTabPage)
-				{
-					DockWorkspace.Instance.TabPages.Add();
-				}
-
-				if (e.Uri.Host == "aboutpage")
-				{
-					RecordHistory();
-					DockWorkspace.Instance.ActiveTabPage.ShowTextView(AboutPage.Display);
-					e.Handled = true;
-					return;
-				}
-				AvalonEditTextOutput output = new AvalonEditTextOutput {
-					Address = e.Uri,
-					Title = e.Uri.AbsolutePath,
-					EnableHyperlinks = true
-				};
-				using (Stream? s = typeof(App).Assembly.GetManifestResourceStream(typeof(App), e.Uri.AbsolutePath))
-				{
-					using (StreamReader r = new StreamReader(s))
-					{
-						string? line;
-						while ((line = r.ReadLine()) != null)
-						{
-							output.Write(line);
-							output.WriteLine();
-						}
-					}
-				}
-				RecordHistory();
-				DockWorkspace.Instance.ShowText(output);
-				e.Handled = true;
-			}
-
-			void RecordHistory()
-			{
-				if (!recordHistory)
-					return;
-				TabPageModel tabPage = DockWorkspace.Instance.ActiveTabPage;
-				var currentState = tabPage.GetState();
-				if (currentState != null)
-					history.UpdateCurrent(new NavigationState(tabPage, currentState));
-				ignoreDecompilationRequests = true;
-				UnselectAll();
-				ignoreDecompilationRequests = false;
-				history.Record(new NavigationState(tabPage, new ViewState { ViewedUri = e.Uri }));
-			}
-		}
 
 		protected override void OnStateChanged(EventArgs e)
 		{
 			base.OnStateChanged(e);
 			// store window state in settings only if it's not minimized
-			if (this.WindowState != System.Windows.WindowState.Minimized)
+			if (this.WindowState != WindowState.Minimized)
 				SettingsService.Instance.SessionSettings.WindowState = this.WindowState;
 		}
 
 		protected override void OnClosing(CancelEventArgs e)
 		{
 			base.OnClosing(e);
-			var sessionSettings = SettingsService.Instance.SessionSettings;
 
-			sessionSettings.ActiveAssemblyList = assemblyList.ListName;
-			sessionSettings.ActiveTreeViewPath = GetPathForNode(AssemblyTreeView.SelectedItem as SharpTreeNode);
-			sessionSettings.ActiveAutoLoadedAssembly = GetAutoLoadedAssemblyNode(AssemblyTreeView.SelectedItem as SharpTreeNode);
-			sessionSettings.WindowBounds = this.RestoreBounds;
-			sessionSettings.DockLayout.Serialize(new XmlLayoutSerializer(dockManager));
-			sessionSettings.Save();
-		}
+			var snapshot = SettingsService.Instance.CreateSnapshot();
 
-		private string? GetAutoLoadedAssemblyNode(SharpTreeNode node)
-		{
-			if (node == null)
-				return null;
-			while (!(node is TreeNodes.AssemblyTreeNode) && node.Parent != null)
-			{
-				node = node.Parent;
-			}
-			//this should be an assembly node
-			var assyNode = node as TreeNodes.AssemblyTreeNode;
-			var loadedAssy = assyNode.LoadedAssembly;
-			if (!(loadedAssy.IsLoaded && loadedAssy.IsAutoLoaded))
-				return null;
+			var sessionSettings = snapshot.GetSettings<SessionSettings>();
 
-			return loadedAssy.FileName;
-		}
+			sessionSettings.ActiveAssemblyList = AssemblyTreeModel.AssemblyList.ListName;
+			sessionSettings.ActiveTreeViewPath = AssemblyTreeModel.SelectedPath;
+			sessionSettings.ActiveAutoLoadedAssembly = GetAutoLoadedAssemblyNode(AssemblyTreeModel.SelectedItem);
+			sessionSettings.WindowBounds = this.RestoreBounds;
+			sessionSettings.DockLayout.Serialize(new XmlLayoutSerializer(dockManager));
 
-		public void UnselectAll()
-		{
-			AssemblyTreeView.UnselectAll();
+			snapshot.Save();
 		}
 
-		public void SetStatus(string status, Brush foreground)
+		private static string GetAutoLoadedAssemblyNode(SharpTreeNode node)
 		{
-			if (this.statusBar.Visibility == Visibility.Collapsed)
-				this.statusBar.Visibility = Visibility.Visible;
-			this.statusLabel.Foreground = foreground;
-			this.statusLabel.Text = status;
-		}
+			var assemblyTreeNode = node?
+				.AncestorsAndSelf()
+				.OfType<AssemblyTreeNode>()
+				.FirstOrDefault();
 
-		public ItemCollection GetMainMenuItems()
-		{
-			return mainMenu.Items;
-		}
+			var loadedAssembly = assemblyTreeNode?.LoadedAssembly;
 
-		public ItemCollection GetToolBarItems()
-		{
-			return toolBar.Items;
+			return loadedAssembly is not { IsLoaded: true, IsAutoLoaded: true }
+				? null
+				: loadedAssembly.FileName;
 		}
 	}
 }
diff --git a/ILSpy/MainWindowViewModel.cs b/ILSpy/MainWindowViewModel.cs
index 16b2cc0fd..270f007ec 100644
--- a/ILSpy/MainWindowViewModel.cs
+++ b/ILSpy/MainWindowViewModel.cs
@@ -19,12 +19,14 @@
 using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpyX;
 
+using TomsToolbox.Wpf;
+
 namespace ICSharpCode.ILSpy
 {
-	class MainWindowViewModel
+	class MainWindowViewModel : ObservableObject
 	{
-		public DockWorkspace Workspace { get; set; }
-		public SessionSettings SessionSettings { get; set; }
-		public AssemblyListManager AssemblyListManager { get; set; }
+		public DockWorkspace Workspace => DockWorkspace.Instance;
+		public SessionSettings SessionSettings => SettingsService.Instance.SessionSettings;
+		public AssemblyListManager AssemblyListManager => SettingsService.Instance.AssemblyListManager;
 	}
 }
diff --git a/ILSpy/Metadata/CoffHeaderTreeNode.cs b/ILSpy/Metadata/CoffHeaderTreeNode.cs
index ab6859024..065b1b6c1 100644
--- a/ILSpy/Metadata/CoffHeaderTreeNode.cs
+++ b/ILSpy/Metadata/CoffHeaderTreeNode.cs
@@ -28,6 +28,8 @@ using ICSharpCode.ILSpy.TreeNodes;
 using ICSharpCode.ILSpy.ViewModels;
 using ICSharpCode.ILSpyX.Extensions;
 
+using TomsToolbox.Essentials;
+
 namespace ICSharpCode.ILSpy.Metadata
 {
 	class CoffHeaderTreeNode : ILSpyTreeNode
diff --git a/ILSpy/Metadata/CorTables/ClassLayoutTableTreeNode.cs b/ILSpy/Metadata/CorTables/ClassLayoutTableTreeNode.cs
index 3dc8eeee0..d6b8fd795 100644
--- a/ILSpy/Metadata/CorTables/ClassLayoutTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/ClassLayoutTableTreeNode.cs
@@ -97,7 +97,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference("metadata", classLayout.Parent));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference("metadata", classLayout.Parent)));
 			}
 
 			string? parentTooltip;
diff --git a/ILSpy/Metadata/CorTables/ConstantTableTreeNode.cs b/ILSpy/Metadata/CorTables/ConstantTableTreeNode.cs
index 4ca9f575a..e6ccc60c2 100644
--- a/ILSpy/Metadata/CorTables/ConstantTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/ConstantTableTreeNode.cs
@@ -88,7 +88,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, constant.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, constant.Parent, protocol: "metadata")));
 			}
 
 			string? parentTooltip;
diff --git a/ILSpy/Metadata/CorTables/CustomAttributeTableTreeNode.cs b/ILSpy/Metadata/CorTables/CustomAttributeTableTreeNode.cs
index a687035ad..ca6089cf4 100644
--- a/ILSpy/Metadata/CorTables/CustomAttributeTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/CustomAttributeTableTreeNode.cs
@@ -83,7 +83,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, customAttr.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, customAttr.Parent, protocol: "metadata")));
 			}
 
 			string? parentTooltip;
@@ -94,7 +94,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnConstructorClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, customAttr.Constructor, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, customAttr.Constructor, protocol: "metadata")));
 			}
 
 			string? constructorTooltip;
diff --git a/ILSpy/Metadata/CorTables/DeclSecurityTableTreeNode.cs b/ILSpy/Metadata/CorTables/DeclSecurityTableTreeNode.cs
index cc297a2fd..dce6bd87b 100644
--- a/ILSpy/Metadata/CorTables/DeclSecurityTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/DeclSecurityTableTreeNode.cs
@@ -84,7 +84,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, declSecAttr.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, declSecAttr.Parent, protocol: "metadata")));
 			}
 
 			string? parentTooltip;
diff --git a/ILSpy/Metadata/CorTables/EventMapTableTreeNode.cs b/ILSpy/Metadata/CorTables/EventMapTableTreeNode.cs
index ee7c4359f..b138480f9 100644
--- a/ILSpy/Metadata/CorTables/EventMapTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/EventMapTableTreeNode.cs
@@ -95,7 +95,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, eventMap.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, eventMap.Parent, protocol: "metadata")));
 			}
 
 			string? parentTooltip;
@@ -106,7 +106,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnEventListClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, eventMap.EventList, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, eventMap.EventList, protocol: "metadata")));
 			}
 
 			string? eventListTooltip;
diff --git a/ILSpy/Metadata/CorTables/EventTableTreeNode.cs b/ILSpy/Metadata/CorTables/EventTableTreeNode.cs
index 484346d91..966620c1c 100644
--- a/ILSpy/Metadata/CorTables/EventTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/EventTableTreeNode.cs
@@ -103,7 +103,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnTypeClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, eventDef.Type, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, eventDef.Type, protocol: "metadata")));
 			}
 
 			string? typeTooltip;
diff --git a/ILSpy/Metadata/CorTables/ExportedTypeTableTreeNode.cs b/ILSpy/Metadata/CorTables/ExportedTypeTableTreeNode.cs
index e66d16d22..62e33ec2c 100644
--- a/ILSpy/Metadata/CorTables/ExportedTypeTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/ExportedTypeTableTreeNode.cs
@@ -107,7 +107,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnImplementationClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, type.Implementation, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, type.Implementation, protocol: "metadata")));
 			}
 
 			string? implementationTooltip;
diff --git a/ILSpy/Metadata/CorTables/FieldLayoutTableTreeNode.cs b/ILSpy/Metadata/CorTables/FieldLayoutTableTreeNode.cs
index 117db920d..c76104422 100644
--- a/ILSpy/Metadata/CorTables/FieldLayoutTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/FieldLayoutTableTreeNode.cs
@@ -96,7 +96,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnFieldClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, fieldLayout.Field, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, fieldLayout.Field, protocol: "metadata")));
 			}
 
 			string? fieldTooltip;
diff --git a/ILSpy/Metadata/CorTables/FieldMarshalTableTreeNode.cs b/ILSpy/Metadata/CorTables/FieldMarshalTableTreeNode.cs
index 5edae567c..d1ca21b22 100644
--- a/ILSpy/Metadata/CorTables/FieldMarshalTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/FieldMarshalTableTreeNode.cs
@@ -95,7 +95,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, fieldMarshal.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, fieldMarshal.Parent, protocol: "metadata")));
 			}
 
 			string? parentTooltip;
diff --git a/ILSpy/Metadata/CorTables/FieldRVATableTreeNode.cs b/ILSpy/Metadata/CorTables/FieldRVATableTreeNode.cs
index 02f26f619..10971d52e 100644
--- a/ILSpy/Metadata/CorTables/FieldRVATableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/FieldRVATableTreeNode.cs
@@ -99,7 +99,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnFieldClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, fieldRVA.Field, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, fieldRVA.Field, protocol: "metadata")));
 			}
 
 			string? fieldTooltip;
diff --git a/ILSpy/Metadata/CorTables/GenericParamConstraintTableTreeNode.cs b/ILSpy/Metadata/CorTables/GenericParamConstraintTableTreeNode.cs
index 91bd5ef97..a6b69295e 100644
--- a/ILSpy/Metadata/CorTables/GenericParamConstraintTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/GenericParamConstraintTableTreeNode.cs
@@ -84,7 +84,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnOwnerClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, genericParamConstraint.Parameter, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, genericParamConstraint.Parameter, protocol: "metadata")));
 			}
 
 			string? ownerTooltip;
@@ -108,7 +108,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnTypeClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, genericParamConstraint.Type, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, genericParamConstraint.Type, protocol: "metadata")));
 			}
 
 			string? typeTooltip;
diff --git a/ILSpy/Metadata/CorTables/GenericParamTableTreeNode.cs b/ILSpy/Metadata/CorTables/GenericParamTableTreeNode.cs
index cfdbfaecb..c09cf535c 100644
--- a/ILSpy/Metadata/CorTables/GenericParamTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/GenericParamTableTreeNode.cs
@@ -92,7 +92,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnOwnerClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, genericParam.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, genericParam.Parent, protocol: "metadata")));
 			}
 
 			string? ownerTooltip;
diff --git a/ILSpy/Metadata/CorTables/ImplMapTableTreeNode.cs b/ILSpy/Metadata/CorTables/ImplMapTableTreeNode.cs
index c8d4c0f91..bec563233 100644
--- a/ILSpy/Metadata/CorTables/ImplMapTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/ImplMapTableTreeNode.cs
@@ -113,7 +113,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMemberForwardedClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, implMap.MemberForwarded, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, implMap.MemberForwarded, protocol: "metadata")));
 			}
 
 			string? memberForwardedTooltip;
@@ -124,7 +124,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnImportScopeClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, implMap.ImportScope, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, implMap.ImportScope, protocol: "metadata")));
 			}
 
 			string? importScopeTooltip;
diff --git a/ILSpy/Metadata/CorTables/InterfaceImplTableTreeNode.cs b/ILSpy/Metadata/CorTables/InterfaceImplTableTreeNode.cs
index 5068aead0..b37d097e3 100644
--- a/ILSpy/Metadata/CorTables/InterfaceImplTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/InterfaceImplTableTreeNode.cs
@@ -96,7 +96,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnClassClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, interfaceImpl.Class, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, interfaceImpl.Class, protocol: "metadata")));
 			}
 
 			string? classTooltip;
@@ -107,7 +107,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnInterfaceClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, interfaceImpl.Interface, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, interfaceImpl.Interface, protocol: "metadata")));
 			}
 
 			string? interfaceTooltip;
diff --git a/ILSpy/Metadata/CorTables/ManifestResourceTableTreeNode.cs b/ILSpy/Metadata/CorTables/ManifestResourceTableTreeNode.cs
index a0fd9dc08..0f655e357 100644
--- a/ILSpy/Metadata/CorTables/ManifestResourceTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/ManifestResourceTableTreeNode.cs
@@ -93,7 +93,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnImplementationClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, manifestResource.Implementation, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, manifestResource.Implementation, protocol: "metadata")));
 			}
 
 			string? implementationTooltip;
diff --git a/ILSpy/Metadata/CorTables/MemberRefTableTreeNode.cs b/ILSpy/Metadata/CorTables/MemberRefTableTreeNode.cs
index c084df6bc..daba41c50 100644
--- a/ILSpy/Metadata/CorTables/MemberRefTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/MemberRefTableTreeNode.cs
@@ -83,7 +83,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, memberRef.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, memberRef.Parent, protocol: "metadata")));
 			}
 
 			string? parentTooltip;
diff --git a/ILSpy/Metadata/CorTables/MethodImplTableTreeNode.cs b/ILSpy/Metadata/CorTables/MethodImplTableTreeNode.cs
index 0573fbbb9..3a5de96d0 100644
--- a/ILSpy/Metadata/CorTables/MethodImplTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/MethodImplTableTreeNode.cs
@@ -82,7 +82,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMethodDeclarationClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, methodImpl.MethodDeclaration, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, methodImpl.MethodDeclaration, protocol: "metadata")));
 			}
 
 			string? methodDeclarationTooltip;
@@ -93,7 +93,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMethodBodyClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, methodImpl.MethodBody, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, methodImpl.MethodBody, protocol: "metadata")));
 			}
 
 			string? methodBodyTooltip;
@@ -104,7 +104,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnTypeClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, methodImpl.Type, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, methodImpl.Type, protocol: "metadata")));
 			}
 
 			string? typeTooltip;
diff --git a/ILSpy/Metadata/CorTables/MethodSemanticsTableTreeNode.cs b/ILSpy/Metadata/CorTables/MethodSemanticsTableTreeNode.cs
index 0878e7d76..63270937d 100644
--- a/ILSpy/Metadata/CorTables/MethodSemanticsTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/MethodSemanticsTableTreeNode.cs
@@ -90,7 +90,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMethodClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, method, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, method, protocol: "metadata")));
 			}
 
 			string? methodTooltip;
@@ -101,7 +101,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnAssociationClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, association, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, association, protocol: "metadata")));
 			}
 
 			string? associationTooltip;
diff --git a/ILSpy/Metadata/CorTables/MethodSpecTableTreeNode.cs b/ILSpy/Metadata/CorTables/MethodSpecTableTreeNode.cs
index e77d7fb1e..12a1dae83 100644
--- a/ILSpy/Metadata/CorTables/MethodSpecTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/MethodSpecTableTreeNode.cs
@@ -85,7 +85,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMethodClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, methodSpec.Method, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, methodSpec.Method, protocol: "metadata")));
 			}
 
 			string? methodTooltip;
diff --git a/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs b/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs
index 4d7566526..a351da9b1 100644
--- a/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/MethodTableTreeNode.cs
@@ -118,7 +118,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParamListClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, methodDef.GetParameters().FirstOrDefault(), protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, methodDef.GetParameters().FirstOrDefault(), protocol: "metadata")));
 			}
 
 			string? paramListTooltip;
diff --git a/ILSpy/Metadata/CorTables/NestedClassTableTreeNode.cs b/ILSpy/Metadata/CorTables/NestedClassTableTreeNode.cs
index ecbf99b37..a2af5d434 100644
--- a/ILSpy/Metadata/CorTables/NestedClassTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/NestedClassTableTreeNode.cs
@@ -95,7 +95,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnNestedClassClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, nestedClass.Nested, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, nestedClass.Nested, protocol: "metadata")));
 			}
 
 			string? nestedClassTooltip;
@@ -106,7 +106,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnEnclosingClassClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, nestedClass.Enclosing, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, nestedClass.Enclosing, protocol: "metadata")));
 			}
 
 			string? enclosingClassTooltip;
diff --git a/ILSpy/Metadata/CorTables/PropertyMapTableTreeNode.cs b/ILSpy/Metadata/CorTables/PropertyMapTableTreeNode.cs
index 4addf69bc..70370559b 100644
--- a/ILSpy/Metadata/CorTables/PropertyMapTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/PropertyMapTableTreeNode.cs
@@ -95,7 +95,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, propertyMap.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, propertyMap.Parent, protocol: "metadata")));
 			}
 
 			string? parentTooltip;
@@ -106,7 +106,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnPropertyListClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, propertyMap.PropertyList, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, propertyMap.PropertyList, protocol: "metadata")));
 			}
 
 			string? propertyListTooltip;
diff --git a/ILSpy/Metadata/CorTables/PtrTableTreeNode.cs b/ILSpy/Metadata/CorTables/PtrTableTreeNode.cs
index 53a3a3d70..de76f4018 100644
--- a/ILSpy/Metadata/CorTables/PtrTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/PtrTableTreeNode.cs
@@ -112,7 +112,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnHandleClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, handlePtr.Handle, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, handlePtr.Handle, protocol: "metadata")));
 			}
 
 			string? handleTooltip;
diff --git a/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs b/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs
index f36940c47..053f393aa 100644
--- a/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/TypeDefTableTreeNode.cs
@@ -111,7 +111,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnBaseTypeClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, typeDef.BaseType, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, typeDef.BaseType, protocol: "metadata")));
 			}
 
 			public string? BaseTypeTooltip {
@@ -142,7 +142,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnFieldListClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, typeDef.GetFields().FirstOrDefault(), protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, typeDef.GetFields().FirstOrDefault(), protocol: "metadata")));
 			}
 
 			string? fieldListTooltip;
@@ -160,7 +160,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMethodListClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, typeDef.GetMethods().FirstOrDefault(), protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, typeDef.GetMethods().FirstOrDefault(), protocol: "metadata")));
 			}
 
 			string? methodListTooltip;
diff --git a/ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs b/ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs
index cff75d02d..8457f1007 100644
--- a/ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs
+++ b/ILSpy/Metadata/CorTables/TypeRefTableTreeNode.cs
@@ -83,7 +83,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnResolutionScopeClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, typeRef.ResolutionScope, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, typeRef.ResolutionScope, protocol: "metadata")));
 			}
 
 			string? resolutionScopeTooltip;
diff --git a/ILSpy/Metadata/DebugMetadataTablesTreeNode.cs b/ILSpy/Metadata/DebugMetadataTablesTreeNode.cs
index 9ec211751..d3067d713 100644
--- a/ILSpy/Metadata/DebugMetadataTablesTreeNode.cs
+++ b/ILSpy/Metadata/DebugMetadataTablesTreeNode.cs
@@ -21,7 +21,6 @@ using System.Reflection.Metadata.Ecma335;
 using ICSharpCode.Decompiler;
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.ILSpy.TreeNodes;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpy.ViewModels;
 
 namespace ICSharpCode.ILSpy.Metadata
diff --git a/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs b/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs
index 8715c2f0e..ea4dc5b04 100644
--- a/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs
+++ b/ILSpy/Metadata/DebugTables/CustomDebugInformationTableTreeNode.cs
@@ -188,7 +188,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, debugInfo.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, debugInfo.Parent, protocol: "metadata")));
 			}
 
 			string parentTooltip;
diff --git a/ILSpy/Metadata/DebugTables/ImportScopeTableTreeNode.cs b/ILSpy/Metadata/DebugTables/ImportScopeTableTreeNode.cs
index 750a18ae7..65639ad1f 100644
--- a/ILSpy/Metadata/DebugTables/ImportScopeTableTreeNode.cs
+++ b/ILSpy/Metadata/DebugTables/ImportScopeTableTreeNode.cs
@@ -80,7 +80,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnParentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, localScope.Parent, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, localScope.Parent, protocol: "metadata")));
 			}
 
 			[ColumnInfo("X8", Kind = ColumnKind.HeapOffset)]
diff --git a/ILSpy/Metadata/DebugTables/LocalScopeTableTreeNode.cs b/ILSpy/Metadata/DebugTables/LocalScopeTableTreeNode.cs
index d2f6b0b63..0a4fb3e07 100644
--- a/ILSpy/Metadata/DebugTables/LocalScopeTableTreeNode.cs
+++ b/ILSpy/Metadata/DebugTables/LocalScopeTableTreeNode.cs
@@ -81,7 +81,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMethodClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, localScope.Method, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, localScope.Method, protocol: "metadata")));
 			}
 
 			string? methodTooltip;
@@ -92,7 +92,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnImportScopeClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, localScope.ImportScope, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, localScope.ImportScope, protocol: "metadata")));
 			}
 
 			[ColumnInfo("X8", Kind = ColumnKind.Token)]
@@ -100,7 +100,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnVariableListClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, localScope.GetLocalVariables().FirstOrDefault(), protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, localScope.GetLocalVariables().FirstOrDefault(), protocol: "metadata")));
 			}
 
 			[ColumnInfo("X8", Kind = ColumnKind.Token)]
@@ -108,7 +108,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnConstantListClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, localScope.GetLocalConstants().FirstOrDefault(), protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, localScope.GetLocalConstants().FirstOrDefault(), protocol: "metadata")));
 			}
 
 			public int StartOffset => localScope.StartOffset;
diff --git a/ILSpy/Metadata/DebugTables/MethodDebugInformationTableTreeNode.cs b/ILSpy/Metadata/DebugTables/MethodDebugInformationTableTreeNode.cs
index 4059cce6d..a93054fac 100644
--- a/ILSpy/Metadata/DebugTables/MethodDebugInformationTableTreeNode.cs
+++ b/ILSpy/Metadata/DebugTables/MethodDebugInformationTableTreeNode.cs
@@ -83,7 +83,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnDocumentClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, debugInfo.Document, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, debugInfo.Document, protocol: "metadata")));
 			}
 
 			public string? DocumentTooltip {
@@ -116,7 +116,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnLocalSignatureClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, debugInfo.LocalSignature, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, debugInfo.LocalSignature, protocol: "metadata")));
 			}
 
 			public string? LocalSignatureTooltip {
diff --git a/ILSpy/Metadata/DebugTables/StateMachineMethodTableTreeNode.cs b/ILSpy/Metadata/DebugTables/StateMachineMethodTableTreeNode.cs
index d9efe7ff6..8602a955b 100644
--- a/ILSpy/Metadata/DebugTables/StateMachineMethodTableTreeNode.cs
+++ b/ILSpy/Metadata/DebugTables/StateMachineMethodTableTreeNode.cs
@@ -83,7 +83,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnMoveNextMethodClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, moveNextMethod, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, moveNextMethod, protocol: "metadata")));
 			}
 
 			string? moveNextMethodTooltip;
@@ -94,7 +94,7 @@ namespace ICSharpCode.ILSpy.Metadata
 
 			public void OnKickofMethodClick()
 			{
-				MainWindow.Instance.JumpToReference(new EntityReference(metadataFile, kickoffMethod, protocol: "metadata"));
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(metadataFile, kickoffMethod, protocol: "metadata")));
 			}
 
 			string? kickoffMethodTooltip;
diff --git a/ILSpy/Metadata/GoToTokenCommand.cs b/ILSpy/Metadata/GoToTokenCommand.cs
index 51b3ab038..17c488d5b 100644
--- a/ILSpy/Metadata/GoToTokenCommand.cs
+++ b/ILSpy/Metadata/GoToTokenCommand.cs
@@ -23,12 +23,13 @@ using System.Reflection;
 using System.Reflection.Metadata.Ecma335;
 using System.Windows;
 using System.Windows.Controls;
-using System.Windows.Media;
 
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.ILSpy.Metadata;
 using ICSharpCode.ILSpy.Properties;
 
+using TomsToolbox.Wpf;
+
 namespace ICSharpCode.ILSpy.Commands
 {
 	[ExportContextMenuEntry(Header = nameof(Resources.GoToToken), Order = 10)]
@@ -38,7 +39,7 @@ namespace ICSharpCode.ILSpy.Commands
 		public void Execute(TextViewContext context)
 		{
 			int token = GetSelectedToken(context.DataGrid, out MetadataFile module).Value;
-			MainWindow.Instance.JumpToReference(new EntityReference(module, MetadataTokens.Handle(token), protocol: "metadata"));
+			MessageBus.Send(this, new NavigateToReferenceEventArgs(new EntityReference(module, MetadataTokens.Handle(token), protocol: "metadata")));
 		}
 
 		public bool IsEnabled(TextViewContext context)
@@ -75,7 +76,7 @@ namespace ICSharpCode.ILSpy.Commands
 	{
 		public void Execute(TextViewContext context)
 		{
-			string? content = GetSelectedCellContent(context.DataGrid, context.MousePosition);
+			string? content = GetSelectedCellContent(context.OriginalSource);
 			Clipboard.SetText(content);
 		}
 
@@ -87,21 +88,14 @@ namespace ICSharpCode.ILSpy.Commands
 		public bool IsVisible(TextViewContext context)
 		{
 			return context.DataGrid?.Name == "MetadataView"
-				&& GetSelectedCellContent(context.DataGrid, context.MousePosition) != null;
+				&& GetSelectedCellContent(context.OriginalSource) != null;
 		}
 
-		private string? GetSelectedCellContent(DataGrid grid, Point position)
+		private static string? GetSelectedCellContent(DependencyObject originalSource)
 		{
-			position = grid.PointFromScreen(position);
-			var hit = VisualTreeHelper.HitTest(grid, position);
-			if (hit == null)
-				return null;
-			var cell = hit.VisualHit.GetParent<DataGridCell>();
-			if (cell == null)
-				return null;
-			return cell.DataContext.GetType()
-				.GetProperty(cell.Column.Header.ToString(), BindingFlags.Instance | BindingFlags.Public)
-				.GetValue(cell.DataContext).ToString();
+			var cell = originalSource.AncestorsAndSelf().OfType<DataGridCell>().FirstOrDefault();
+
+			return cell?.Column.OnCopyingCellClipboardContent(cell.DataContext).ToString();
 		}
 	}
 }
diff --git a/ILSpy/Metadata/MetadataProtocolHandler.cs b/ILSpy/Metadata/MetadataProtocolHandler.cs
index a0008d21e..a70cb523b 100644
--- a/ILSpy/Metadata/MetadataProtocolHandler.cs
+++ b/ILSpy/Metadata/MetadataProtocolHandler.cs
@@ -34,7 +34,7 @@ namespace ICSharpCode.ILSpy.Metadata
 			newTabPage = true;
 			if (protocol != "metadata")
 				return null;
-			var assemblyTreeNode = MainWindow.Instance.FindTreeNode(module) as AssemblyTreeNode;
+			var assemblyTreeNode = MainWindow.Instance.AssemblyTreeModel.FindTreeNode(module) as AssemblyTreeNode;
 			if (assemblyTreeNode == null)
 				return null;
 			var mxNode = assemblyTreeNode.Children.OfType<MetadataTreeNode>().FirstOrDefault();
diff --git a/ILSpy/Metadata/MetadataTablesTreeNode.cs b/ILSpy/Metadata/MetadataTablesTreeNode.cs
index 3724ba590..1fb55be81 100644
--- a/ILSpy/Metadata/MetadataTablesTreeNode.cs
+++ b/ILSpy/Metadata/MetadataTablesTreeNode.cs
@@ -23,7 +23,6 @@ using System.Reflection.Metadata.Ecma335;
 using ICSharpCode.Decompiler;
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.ILSpy.TreeNodes;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpy.ViewModels;
 
 namespace ICSharpCode.ILSpy.Metadata
diff --git a/ILSpy/Metadata/OptionalHeaderTreeNode.cs b/ILSpy/Metadata/OptionalHeaderTreeNode.cs
index dc1be30ec..8353ead80 100644
--- a/ILSpy/Metadata/OptionalHeaderTreeNode.cs
+++ b/ILSpy/Metadata/OptionalHeaderTreeNode.cs
@@ -27,6 +27,8 @@ using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.ILSpy.TreeNodes;
 using ICSharpCode.ILSpyX.Extensions;
 
+using TomsToolbox.Essentials;
+
 namespace ICSharpCode.ILSpy.Metadata
 {
 	class OptionalHeaderTreeNode : ILSpyTreeNode
diff --git a/ILSpy/Options/DecompilerSettings.cs b/ILSpy/Options/DecompilerSettings.cs
new file mode 100644
index 000000000..14f1d32e9
--- /dev/null
+++ b/ILSpy/Options/DecompilerSettings.cs
@@ -0,0 +1,52 @@
+using System.ComponentModel;
+
+using ICSharpCode.ILSpyX.Settings;
+using System.Linq;
+using System.Reflection;
+using System.Xml.Linq;
+
+#nullable enable
+
+namespace ICSharpCode.ILSpy.Options
+{
+	public class DecompilerSettings : Decompiler.DecompilerSettings, ISettingsSection
+	{
+		static readonly PropertyInfo[] properties = typeof(Decompiler.DecompilerSettings).GetProperties()
+				.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
+				.ToArray();
+
+		public XName SectionName => "DecompilerSettings";
+
+		public XElement SaveToXml()
+		{
+			var section = new XElement(SectionName);
+
+			foreach (var p in properties)
+			{
+				section.SetAttributeValue(p.Name, p.GetValue(this));
+			}
+
+			return section;
+		}
+
+		public void LoadFromXml(XElement section)
+		{
+			foreach (var p in properties)
+			{
+				var value = (bool?)section.Attribute(p.Name);
+				if (value.HasValue)
+					p.SetValue(this, value.Value);
+			}
+		}
+
+		public new DecompilerSettings Clone()
+		{
+			var section = SaveToXml();
+
+			var newSettings = new DecompilerSettings();
+			newSettings.LoadFromXml(section);
+
+			return newSettings;
+		}
+	}
+}
diff --git a/ILSpy/Options/DecompilerSettingsPanel.xaml.cs b/ILSpy/Options/DecompilerSettingsPanel.xaml.cs
index d530079dc..e65f72087 100644
--- a/ILSpy/Options/DecompilerSettingsPanel.xaml.cs
+++ b/ILSpy/Options/DecompilerSettingsPanel.xaml.cs
@@ -19,47 +19,22 @@
 using System.ComponentModel.Composition;
 using System.Xml.Linq;
 
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX.Settings;
 
+using TomsToolbox.Wpf.Composition.Mef;
+
 namespace ICSharpCode.ILSpy.Options
 {
 	/// <summary>
 	/// Interaction logic for DecompilerSettingsPanel.xaml
 	/// </summary>
-	[ExportOptionPage(Title = nameof(Properties.Resources.Decompiler), Order = 10)]
+	[DataTemplate(typeof(DecompilerSettingsViewModel))]
 	[PartCreationPolicy(CreationPolicy.NonShared)]
-	internal partial class DecompilerSettingsPanel : IOptionPage
+	internal partial class DecompilerSettingsPanel
 	{
 		public DecompilerSettingsPanel()
 		{
 			InitializeComponent();
 		}
-
-		public static Decompiler.DecompilerSettings LoadDecompilerSettings(ILSpySettings settings)
-		{
-			return ISettingsProvider.LoadDecompilerSettings(settings);
-		}
-
-		public void Load(ILSpySettings settings)
-		{
-			this.DataContext = new DecompilerSettingsViewModel(LoadDecompilerSettings(settings));
-		}
-
-		public void Save(XElement root)
-		{
-			var newSettings = ((DecompilerSettingsViewModel)this.DataContext).ToDecompilerSettings();
-			ISettingsProvider.SaveDecompilerSettings(root, newSettings);
-
-			SettingsService.Instance.DecompilerSettings = newSettings;
-			SettingsService.Instance.AssemblyListManager.ApplyWinRTProjections = newSettings.ApplyWindowsRuntimeProjections;
-			SettingsService.Instance.AssemblyListManager.UseDebugSymbols = newSettings.UseDebugSymbols;
-		}
-
-		public void LoadDefaults()
-		{
-			SettingsService.Instance.DecompilerSettings = new();
-			this.DataContext = new DecompilerSettingsViewModel(SettingsService.Instance.DecompilerSettings);
-		}
 	}
 }
diff --git a/ILSpy/Options/DecompilerSettingsViewModel.cs b/ILSpy/Options/DecompilerSettingsViewModel.cs
index af9eeeb10..eb0b9d5a8 100644
--- a/ILSpy/Options/DecompilerSettingsViewModel.cs
+++ b/ILSpy/Options/DecompilerSettingsViewModel.cs
@@ -18,6 +18,7 @@
 
 using System.Collections.Generic;
 using System.ComponentModel;
+using System.ComponentModel.Composition;
 using System.Linq;
 using System.Reflection;
 
@@ -28,44 +29,63 @@ using TomsToolbox.Wpf;
 
 namespace ICSharpCode.ILSpy.Options
 {
-	public sealed class DecompilerSettingsViewModel : ObservableObjectBase
+	[ExportOptionPage(Order = 10)]
+	[PartCreationPolicy(CreationPolicy.NonShared)]
+	public sealed class DecompilerSettingsViewModel : ObservableObjectBase, IOptionPage
 	{
-		public DecompilerSettingsGroupViewModel[] Settings { get; }
+		private static readonly PropertyInfo[] propertyInfos = typeof(Decompiler.DecompilerSettings).GetProperties()
+			.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
+			.ToArray();
 
-		public DecompilerSettingsViewModel(Decompiler.DecompilerSettings settings)
+		public string Title => Resources.Decompiler;
+
+		private DecompilerSettingsGroupViewModel[] settings;
+		public DecompilerSettingsGroupViewModel[] Settings {
+			get => settings;
+			set => SetProperty(ref settings, value);
+		}
+
+		private DecompilerSettings decompilerSettings;
+
+		public void Load(SettingsSnapshot snapshot)
 		{
-			Settings = typeof(Decompiler.DecompilerSettings).GetProperties()
-				.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
-				.Select(p => new DecompilerSettingsItemViewModel(p) { IsEnabled = p.GetValue(settings) is true })
+			decompilerSettings = snapshot.GetSettings<DecompilerSettings>();
+			LoadSettings();
+		}
+
+		private void LoadSettings()
+		{
+			this.Settings = propertyInfos
+				.Select(p => new DecompilerSettingsItemViewModel(p, decompilerSettings))
 				.OrderBy(item => item.Category, NaturalStringComparer.Instance)
 				.GroupBy(p => p.Category)
 				.Select(g => new DecompilerSettingsGroupViewModel(g.Key, g.OrderBy(i => i.Description).ToArray()))
 				.ToArray();
 		}
 
-		public Decompiler.DecompilerSettings ToDecompilerSettings()
+		public void LoadDefaults()
 		{
-			var settings = new Decompiler.DecompilerSettings();
+			var defaults = new Decompiler.DecompilerSettings();
 
-			foreach (var item in Settings.SelectMany(group => group.Settings))
+			foreach (var propertyInfo in propertyInfos)
 			{
-				item.Property.SetValue(settings, item.IsEnabled);
+				propertyInfo.SetValue(decompilerSettings, propertyInfo.GetValue(defaults));
 			}
 
-			return settings;
+			LoadSettings();
 		}
 	}
 
 	public sealed class DecompilerSettingsGroupViewModel : ObservableObjectBase
 	{
-		private bool? _areAllItemsChecked;
+		private bool? areAllItemsChecked;
 
 		public DecompilerSettingsGroupViewModel(string category, DecompilerSettingsItemViewModel[] settings)
 		{
 			Settings = settings;
 			Category = category;
 
-			_areAllItemsChecked = GetAreAllItemsChecked(Settings);
+			areAllItemsChecked = GetAreAllItemsChecked(Settings);
 
 			foreach (DecompilerSettingsItemViewModel viewModel in settings)
 			{
@@ -74,9 +94,9 @@ namespace ICSharpCode.ILSpy.Options
 		}
 
 		public bool? AreAllItemsChecked {
-			get => _areAllItemsChecked;
+			get => areAllItemsChecked;
 			set {
-				SetProperty(ref _areAllItemsChecked, value);
+				SetProperty(ref areAllItemsChecked, value);
 
 				if (!value.HasValue)
 					return;
@@ -114,15 +134,20 @@ namespace ICSharpCode.ILSpy.Options
 		}
 	}
 
-	public sealed class DecompilerSettingsItemViewModel(PropertyInfo property) : ObservableObjectBase
+	public sealed class DecompilerSettingsItemViewModel(PropertyInfo property, DecompilerSettings decompilerSettings) : ObservableObjectBase
 	{
-		private bool _isEnabled;
+		private bool isEnabled = property.GetValue(decompilerSettings) is true;
 
-		public PropertyInfo Property { get; } = property;
+		public PropertyInfo Property => property;
 
 		public bool IsEnabled {
-			get => _isEnabled;
-			set => SetProperty(ref _isEnabled, value);
+			get => isEnabled;
+			set {
+				if (SetProperty(ref isEnabled, value))
+				{
+					property.SetValue(decompilerSettings, value);
+				}
+			}
 		}
 
 		public string Description { get; set; } = GetResourceString(property.GetCustomAttribute<DescriptionAttribute>()?.Description ?? property.Name);
diff --git a/ILSpy/Options/DisplaySettings.cs b/ILSpy/Options/DisplaySettings.cs
index 4ab7a05f2..9eb07b394 100644
--- a/ILSpy/Options/DisplaySettings.cs
+++ b/ILSpy/Options/DisplaySettings.cs
@@ -16,341 +16,190 @@
 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
 using System.Windows.Media;
+using System.Xml.Linq;
 
-using ICSharpCode.ILSpy.Themes;
+using TomsToolbox.Wpf;
 
 namespace ICSharpCode.ILSpy.Options
 {
 	/// <summary>
 	/// Description of DisplaySettings.
 	/// </summary>
-	public class DisplaySettings : INotifyPropertyChanged
+	public class DisplaySettings : ObservableObject, ISettingsSection
 	{
-		public DisplaySettings()
-		{
-			this.theme = ThemeManager.Current.DefaultTheme;
-			this.selectedFont = new FontFamily("Consolas");
-			this.selectedFontSize = 10.0 * 4 / 3;
-			this.sortResults = true;
-			this.indentationUseTabs = true;
-			this.indentationSize = 4;
-			this.indentationTabSize = 4;
-			this.highlightMatchingBraces = true;
-		}
-
-		#region INotifyPropertyChanged implementation
-		public event PropertyChangedEventHandler? PropertyChanged;
-
-		protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
-		{
-			PropertyChanged?.Invoke(this, e);
-		}
-
-		protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
-		{
-			OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
-		}
-		#endregion
-
-		string theme;
-
-		public string Theme {
-			get { return theme; }
-			set {
-				if (theme != value)
-				{
-					theme = value;
-					OnPropertyChanged();
-				}
-			}
-		}
-
 		FontFamily selectedFont;
-
 		public FontFamily SelectedFont {
-			get { return selectedFont; }
-			set {
-				if (selectedFont != value)
-				{
-					selectedFont = value;
-					OnPropertyChanged();
-				}
-			}
+			get => selectedFont;
+			set => SetProperty(ref selectedFont, value);
 		}
 
 		double selectedFontSize;
-
 		public double SelectedFontSize {
-			get { return selectedFontSize; }
-			set {
-				if (selectedFontSize != value)
-				{
-					selectedFontSize = value;
-					OnPropertyChanged();
-				}
-			}
+			get => selectedFontSize;
+			set => SetProperty(ref selectedFontSize, value);
 		}
 
 		bool showLineNumbers;
-
 		public bool ShowLineNumbers {
-			get { return showLineNumbers; }
-			set {
-				if (showLineNumbers != value)
-				{
-					showLineNumbers = value;
-					OnPropertyChanged();
-				}
-			}
+			get => showLineNumbers;
+			set => SetProperty(ref showLineNumbers, value);
 		}
 
 		bool showMetadataTokens;
-
 		public bool ShowMetadataTokens {
-			get { return showMetadataTokens; }
-			set {
-				if (showMetadataTokens != value)
-				{
-					showMetadataTokens = value;
-					OnPropertyChanged();
-				}
-			}
+			get => showMetadataTokens;
+			set => SetProperty(ref showMetadataTokens, value);
 		}
 
 		bool showMetadataTokensInBase10;
-
 		public bool ShowMetadataTokensInBase10 {
-			get { return showMetadataTokensInBase10; }
-			set {
-				if (showMetadataTokensInBase10 != value)
-				{
-					showMetadataTokensInBase10 = value;
-					OnPropertyChanged();
-				}
-			}
+			get => showMetadataTokensInBase10;
+			set => SetProperty(ref showMetadataTokensInBase10, value);
 		}
 
 		bool enableWordWrap;
-
 		public bool EnableWordWrap {
-			get { return enableWordWrap; }
-			set {
-				if (enableWordWrap != value)
-				{
-					enableWordWrap = value;
-					OnPropertyChanged();
-				}
-			}
+			get => enableWordWrap;
+			set => SetProperty(ref enableWordWrap, value);
 		}
 
-		bool sortResults = true;
-
+		bool sortResults;
 		public bool SortResults {
-			get { return sortResults; }
-			set {
-				if (sortResults != value)
-				{
-					sortResults = value;
-					OnPropertyChanged();
-				}
-			}
+			get => sortResults;
+			set => SetProperty(ref sortResults, value);
 		}
 
-		bool foldBraces = false;
-
+		bool foldBraces;
 		public bool FoldBraces {
-			get { return foldBraces; }
-			set {
-				if (foldBraces != value)
-				{
-					foldBraces = value;
-					OnPropertyChanged();
-				}
-			}
+			get => foldBraces;
+			set => SetProperty(ref foldBraces, value);
 		}
 
-		bool expandMemberDefinitions = false;
-
+		bool expandMemberDefinitions;
 		public bool ExpandMemberDefinitions {
-			get { return expandMemberDefinitions; }
-			set {
-				if (expandMemberDefinitions != value)
-				{
-					expandMemberDefinitions = value;
-					OnPropertyChanged();
-				}
-			}
+			get => expandMemberDefinitions;
+			set => SetProperty(ref expandMemberDefinitions, value);
 		}
 
-		bool expandUsingDeclarations = false;
-
+		bool expandUsingDeclarations;
 		public bool ExpandUsingDeclarations {
-			get { return expandUsingDeclarations; }
-			set {
-				if (expandUsingDeclarations != value)
-				{
-					expandUsingDeclarations = value;
-					OnPropertyChanged();
-				}
-			}
+			get => expandUsingDeclarations;
+			set => SetProperty(ref expandUsingDeclarations, value);
 		}
 
 		bool showDebugInfo;
-
 		public bool ShowDebugInfo {
-			get { return showDebugInfo; }
-			set {
-				if (showDebugInfo != value)
-				{
-					showDebugInfo = value;
-					OnPropertyChanged();
-				}
-			}
+			get => showDebugInfo;
+			set => SetProperty(ref showDebugInfo, value);
 		}
 
-		bool indentationUseTabs = true;
-
+		bool indentationUseTabs;
 		public bool IndentationUseTabs {
-			get { return indentationUseTabs; }
-			set {
-				if (indentationUseTabs != value)
-				{
-					indentationUseTabs = value;
-					OnPropertyChanged();
-				}
-			}
+			get => indentationUseTabs;
+			set => SetProperty(ref indentationUseTabs, value);
 		}
 
-		int indentationTabSize = 4;
-
+		int indentationTabSize;
 		public int IndentationTabSize {
-			get { return indentationTabSize; }
-			set {
-				if (indentationTabSize != value)
-				{
-					indentationTabSize = value;
-					OnPropertyChanged();
-				}
-			}
+			get => indentationTabSize;
+			set => SetProperty(ref indentationTabSize, value);
 		}
 
-		int indentationSize = 4;
-
+		int indentationSize;
 		public int IndentationSize {
-			get { return indentationSize; }
-			set {
-				if (indentationSize != value)
-				{
-					indentationSize = value;
-					OnPropertyChanged();
-				}
-			}
+			get => indentationSize;
+			set => SetProperty(ref indentationSize, value);
 		}
 
-		bool highlightMatchingBraces = true;
-
+		bool highlightMatchingBraces;
 		public bool HighlightMatchingBraces {
-			get { return highlightMatchingBraces; }
-			set {
-				if (highlightMatchingBraces != value)
-				{
-					highlightMatchingBraces = value;
-					OnPropertyChanged();
-				}
-			}
+			get => highlightMatchingBraces;
+			set => SetProperty(ref highlightMatchingBraces, value);
 		}
 
-		bool highlightCurrentLine = false;
-
+		bool highlightCurrentLine;
 		public bool HighlightCurrentLine {
-			get { return highlightCurrentLine; }
-			set {
-				if (highlightCurrentLine != value)
-				{
-					highlightCurrentLine = value;
-					OnPropertyChanged();
-				}
-			}
+			get => highlightCurrentLine;
+			set => SetProperty(ref highlightCurrentLine, value);
 		}
 
-		bool hideEmptyMetadataTables = true;
-
+		bool hideEmptyMetadataTables;
 		public bool HideEmptyMetadataTables {
-			get { return hideEmptyMetadataTables; }
-			set {
-				if (hideEmptyMetadataTables != value)
-				{
-					hideEmptyMetadataTables = value;
-					OnPropertyChanged();
-				}
-			}
+			get => hideEmptyMetadataTables;
+			set => SetProperty(ref hideEmptyMetadataTables, value);
 		}
 
-		bool useNestedNamespaceNodes = true;
-
+		bool useNestedNamespaceNodes;
 		public bool UseNestedNamespaceNodes {
-			get { return useNestedNamespaceNodes; }
-			set {
-				if (useNestedNamespaceNodes != value)
-				{
-					useNestedNamespaceNodes = value;
-					OnPropertyChanged();
-				}
-			}
+			get => useNestedNamespaceNodes;
+			set => SetProperty(ref useNestedNamespaceNodes, value);
 		}
 
 		private bool styleWindowTitleBar;
-
 		public bool StyleWindowTitleBar {
-			get { return styleWindowTitleBar; }
-			set {
-				if (styleWindowTitleBar != value)
-				{
-					styleWindowTitleBar = value;
-					OnPropertyChanged();
-				}
-			}
+			get => styleWindowTitleBar;
+			set => SetProperty(ref styleWindowTitleBar, value);
 		}
 
 		private bool showRawOffsetsAndBytesBeforeInstruction;
-
 		public bool ShowRawOffsetsAndBytesBeforeInstruction {
-			get { return showRawOffsetsAndBytesBeforeInstruction; }
-			set {
-				if (showRawOffsetsAndBytesBeforeInstruction != value)
-				{
-					showRawOffsetsAndBytesBeforeInstruction = value;
-					OnPropertyChanged();
-				}
-			}
+			get => showRawOffsetsAndBytesBeforeInstruction;
+			set => SetProperty(ref showRawOffsetsAndBytesBeforeInstruction, value);
 		}
 
-		public void CopyValues(DisplaySettings s)
+		public XName SectionName => "DisplaySettings";
+
+		public void LoadFromXml(XElement section)
+		{
+			SelectedFont = new FontFamily((string)section.Attribute("Font") ?? "Consolas");
+			SelectedFontSize = (double?)section.Attribute("FontSize") ?? 10.0 * 4 / 3;
+			ShowLineNumbers = (bool?)section.Attribute("ShowLineNumbers") ?? false;
+			ShowMetadataTokens = (bool?)section.Attribute("ShowMetadataTokens") ?? false;
+			ShowMetadataTokensInBase10 = (bool?)section.Attribute("ShowMetadataTokensInBase10") ?? false;
+			ShowDebugInfo = (bool?)section.Attribute("ShowDebugInfo") ?? false;
+			EnableWordWrap = (bool?)section.Attribute("EnableWordWrap") ?? false;
+			SortResults = (bool?)section.Attribute("SortResults") ?? true;
+			FoldBraces = (bool?)section.Attribute("FoldBraces") ?? false;
+			ExpandMemberDefinitions = (bool?)section.Attribute("ExpandMemberDefinitions") ?? false;
+			ExpandUsingDeclarations = (bool?)section.Attribute("ExpandUsingDeclarations") ?? false;
+			IndentationUseTabs = (bool?)section.Attribute("IndentationUseTabs") ?? true;
+			IndentationSize = (int?)section.Attribute("IndentationSize") ?? 4;
+			IndentationTabSize = (int?)section.Attribute("IndentationTabSize") ?? 4;
+			HighlightMatchingBraces = (bool?)section.Attribute("HighlightMatchingBraces") ?? true;
+			HighlightCurrentLine = (bool?)section.Attribute("HighlightCurrentLine") ?? false;
+			HideEmptyMetadataTables = (bool?)section.Attribute("HideEmptyMetadataTables") ?? true;
+			UseNestedNamespaceNodes = (bool?)section.Attribute("UseNestedNamespaceNodes") ?? false;
+			ShowRawOffsetsAndBytesBeforeInstruction = (bool?)section.Attribute("ShowRawOffsetsAndBytesBeforeInstruction") ?? false;
+			StyleWindowTitleBar = (bool?)section.Attribute("StyleWindowTitleBar") ?? false;
+		}
+
+		public XElement SaveToXml()
 		{
-			this.Theme = s.Theme;
-			this.SelectedFont = s.selectedFont;
-			this.SelectedFontSize = s.selectedFontSize;
-			this.ShowLineNumbers = s.showLineNumbers;
-			this.ShowMetadataTokens = s.showMetadataTokens;
-			this.ShowMetadataTokensInBase10 = s.showMetadataTokensInBase10;
-			this.ShowDebugInfo = s.showDebugInfo;
-			this.EnableWordWrap = s.enableWordWrap;
-			this.SortResults = s.sortResults;
-			this.FoldBraces = s.foldBraces;
-			this.ExpandMemberDefinitions = s.expandMemberDefinitions;
-			this.ExpandUsingDeclarations = s.expandUsingDeclarations;
-			this.IndentationUseTabs = s.indentationUseTabs;
-			this.IndentationTabSize = s.indentationTabSize;
-			this.IndentationSize = s.indentationSize;
-			this.HighlightMatchingBraces = s.highlightMatchingBraces;
-			this.HighlightCurrentLine = s.highlightCurrentLine;
-			this.HideEmptyMetadataTables = s.hideEmptyMetadataTables;
-			this.UseNestedNamespaceNodes = s.useNestedNamespaceNodes;
-			this.ShowRawOffsetsAndBytesBeforeInstruction = s.showRawOffsetsAndBytesBeforeInstruction;
-			this.StyleWindowTitleBar = s.styleWindowTitleBar;
+			var section = new XElement(SectionName);
+
+			section.SetAttributeValue("Font", SelectedFont.Source);
+			section.SetAttributeValue("FontSize", SelectedFontSize);
+			section.SetAttributeValue("ShowLineNumbers", ShowLineNumbers);
+			section.SetAttributeValue("ShowMetadataTokens", ShowMetadataTokens);
+			section.SetAttributeValue("ShowMetadataTokensInBase10", ShowMetadataTokensInBase10);
+			section.SetAttributeValue("ShowDebugInfo", ShowDebugInfo);
+			section.SetAttributeValue("EnableWordWrap", EnableWordWrap);
+			section.SetAttributeValue("SortResults", SortResults);
+			section.SetAttributeValue("FoldBraces", FoldBraces);
+			section.SetAttributeValue("ExpandMemberDefinitions", ExpandMemberDefinitions);
+			section.SetAttributeValue("ExpandUsingDeclarations", ExpandUsingDeclarations);
+			section.SetAttributeValue("IndentationUseTabs", IndentationUseTabs);
+			section.SetAttributeValue("IndentationSize", IndentationSize);
+			section.SetAttributeValue("IndentationTabSize", IndentationTabSize);
+			section.SetAttributeValue("HighlightMatchingBraces", HighlightMatchingBraces);
+			section.SetAttributeValue("HighlightCurrentLine", HighlightCurrentLine);
+			section.SetAttributeValue("HideEmptyMetadataTables", HideEmptyMetadataTables);
+			section.SetAttributeValue("UseNestedNamespaceNodes", UseNestedNamespaceNodes);
+			section.SetAttributeValue("ShowRawOffsetsAndBytesBeforeInstruction", ShowRawOffsetsAndBytesBeforeInstruction);
+			section.SetAttributeValue("StyleWindowTitleBar", StyleWindowTitleBar);
+
+			return section;
 		}
 	}
 }
diff --git a/ILSpy/Options/DisplaySettingsPanel.xaml b/ILSpy/Options/DisplaySettingsPanel.xaml
index 1170a8f5b..9fb50d7a9 100644
--- a/ILSpy/Options/DisplaySettingsPanel.xaml
+++ b/ILSpy/Options/DisplaySettingsPanel.xaml
@@ -3,19 +3,15 @@
 			 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 			 xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
 			 xmlns:local="clr-namespace:ICSharpCode.ILSpy.Options"
-			 xmlns:system="clr-namespace:System;assembly=mscorlib"
 			 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
 			 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 			 xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes"
-			 d:DataContext="{d:DesignInstance local:DisplaySettings}">
-	<UserControl.Resources>
-		<local:FontSizeConverter x:Key="fontSizeConv" />
-	</UserControl.Resources>
+			 d:DataContext="{d:DesignInstance local:DisplaySettingsViewModel}">
 	<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
 		<StackPanel Orientation="Vertical">
 			<DockPanel>
 				<Label DockPanel.Dock="Left" Content="{x:Static properties:Resources.DisplaySettingsPanel_Theme}" />
-				<ComboBox ItemsSource="{x:Static themes:ThemeManager.AllThemes}" SelectedItem="{Binding Theme}" VerticalContentAlignment="Center" Margin="0,0,8,0" />
+				<ComboBox ItemsSource="{x:Static themes:ThemeManager.AllThemes}" SelectedItem="{Binding SessionSettings.Theme}" VerticalContentAlignment="Center" Margin="0,0,8,0" />
 			</DockPanel>
 			<GroupBox Header="{x:Static properties:Resources.Font}">
 				<Grid>
@@ -30,78 +26,58 @@
 						<RowDefinition Height="50" />
 					</Grid.RowDefinitions>
 					<Label Margin="3,0" Content="{x:Static properties:Resources.DisplaySettingsPanel_Font}"></Label>
-					<ComboBox x:Name="fontSelector" VerticalContentAlignment="Center" SelectedItem="{Binding SelectedFont}" Grid.Column="1">
+					<ComboBox Grid.Row="0" ItemsSource="{Binding FontFamilies}" VerticalContentAlignment="Center" SelectedItem="{Binding Settings.SelectedFont}" Grid.Column="1">
 						<ComboBox.ItemTemplate>
 							<DataTemplate>
 								<TextBlock Text="{Binding Source}" />
 							</DataTemplate>
 						</ComboBox.ItemTemplate>
 					</ComboBox>
-					<Label Grid.Column="2" Margin="3,0" Content="{x:Static properties:Resources.Size}"></Label>
-					<ComboBox Grid.Column="3" Text="{Binding SelectedFontSize, Converter={StaticResource fontSizeConv}}" IsEditable="True" Margin="3,0">
-						<system:Int32>6</system:Int32>
-						<system:Int32>7</system:Int32>
-						<system:Int32>8</system:Int32>
-						<system:Int32>9</system:Int32>
-						<system:Int32>10</system:Int32>
-						<system:Int32>11</system:Int32>
-						<system:Int32>12</system:Int32>
-						<system:Int32>13</system:Int32>
-						<system:Int32>14</system:Int32>
-						<system:Int32>15</system:Int32>
-						<system:Int32>16</system:Int32>
-						<system:Int32>17</system:Int32>
-						<system:Int32>18</system:Int32>
-						<system:Int32>19</system:Int32>
-						<system:Int32>20</system:Int32>
-						<system:Int32>21</system:Int32>
-						<system:Int32>22</system:Int32>
-						<system:Int32>23</system:Int32>
-						<system:Int32>24</system:Int32>
-					</ComboBox>
-					<Border Grid.Row="1" Grid.ColumnSpan="4" BorderBrush="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" BorderThickness="1" Margin="3,5">
-						<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="AaBbCcXxYyZz" FontFamily="{Binding SelectedFont}" FontSize="{Binding SelectedFontSize}" />
+					<Label Grid.Row="0" Grid.Column="2" Margin="3,0" Content="{x:Static properties:Resources.Size}"></Label>
+					<ComboBox Grid.Row="0" Grid.Column="3" ItemsSource="{Binding FontSizes}" Text="{Binding Settings.SelectedFontSize, Converter={local:FontSizeConverter}}" IsEditable="True" Margin="3,0" />
+					<Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="4" BorderBrush="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" BorderThickness="1" Margin="3,5">
+						<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="AaBbCcXxYyZz" FontFamily="{Binding Settings.SelectedFont}" FontSize="{Binding Settings.SelectedFontSize}" />
 					</Border>
 				</Grid>
 			</GroupBox>
 			<GroupBox Header="{x:Static properties:Resources.Indentation}">
 				<StackPanel Margin="3">
-					<CheckBox IsChecked="{Binding IndentationUseTabs}" Content="{x:Static properties:Resources.UseTabsInsteadOfSpaces}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.IndentationUseTabs}" Content="{x:Static properties:Resources.UseTabsInsteadOfSpaces}"></CheckBox>
 					<StackPanel Orientation="Horizontal">
 						<Label Content="{x:Static properties:Resources.TabSize}"></Label>
-						<TextBox x:Name="tabSizeTextBox" Width="50" Margin="3" Text="{Binding IndentationTabSize}" PreviewTextInput="TextBox_PreviewTextInput" />
+						<TextBox x:Name="tabSizeTextBox" Width="50" Margin="3" Text="{Binding Settings.IndentationTabSize}" PreviewTextInput="TextBox_PreviewTextInput" />
 					</StackPanel>
 					<StackPanel Orientation="Horizontal">
 						<Label Content="{x:Static properties:Resources.IndentSize}"></Label>
-						<TextBox x:Name="indentSizeTextBox" Width="50" Margin="3" Text="{Binding IndentationSize}" PreviewTextInput="TextBox_PreviewTextInput" />
+						<TextBox x:Name="indentSizeTextBox" Width="50" Margin="3" Text="{Binding Settings.IndentationSize}" PreviewTextInput="TextBox_PreviewTextInput" />
 					</StackPanel>
 				</StackPanel>
 			</GroupBox>
 			<GroupBox Header="{x:Static properties:Resources.DecompilationViewOptions}">
 				<StackPanel Margin="3">
-					<CheckBox IsChecked="{Binding ShowLineNumbers}" Content="{x:Static properties:Resources.ShowLineNumbers}"></CheckBox>
-					<CheckBox IsChecked="{Binding EnableWordWrap}" Content="{x:Static properties:Resources.EnableWordWrap}"></CheckBox>
-					<CheckBox IsChecked="{Binding FoldBraces}" Content="{x:Static properties:Resources.EnableFoldingBlocksBraces}"></CheckBox>
-					<CheckBox IsChecked="{Binding HighlightMatchingBraces}" Content="{x:Static properties:Resources.HighlightMatchingBraces}"></CheckBox>
-					<CheckBox IsChecked="{Binding HighlightCurrentLine}" Content="{x:Static properties:Resources.HighlightCurrentLine}"></CheckBox>
-					<CheckBox IsChecked="{Binding ExpandMemberDefinitions}" Content="{x:Static properties:Resources.ExpandMemberDefinitionsAfterDecompilation}"></CheckBox>
-					<CheckBox IsChecked="{Binding ExpandUsingDeclarations}" Content="{x:Static properties:Resources.ExpandUsingDeclarationsAfterDecompilation}"></CheckBox>
-					<CheckBox IsChecked="{Binding ShowDebugInfo}" Content="{x:Static properties:Resources.ShowInfoFromDebugSymbolsAvailable}"></CheckBox>
-					<CheckBox IsChecked="{Binding ShowRawOffsetsAndBytesBeforeInstruction}" Content="{x:Static properties:Resources.ShowRawOffsetsAndBytesBeforeInstruction}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.ShowLineNumbers}" Content="{x:Static properties:Resources.ShowLineNumbers}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.EnableWordWrap}" Content="{x:Static properties:Resources.EnableWordWrap}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.FoldBraces}" Content="{x:Static properties:Resources.EnableFoldingBlocksBraces}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.HighlightMatchingBraces}" Content="{x:Static properties:Resources.HighlightMatchingBraces}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.HighlightCurrentLine}" Content="{x:Static properties:Resources.HighlightCurrentLine}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.ExpandMemberDefinitions}" Content="{x:Static properties:Resources.ExpandMemberDefinitionsAfterDecompilation}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.ExpandUsingDeclarations}" Content="{x:Static properties:Resources.ExpandUsingDeclarationsAfterDecompilation}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.ShowDebugInfo}" Content="{x:Static properties:Resources.ShowInfoFromDebugSymbolsAvailable}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.ShowRawOffsetsAndBytesBeforeInstruction}" Content="{x:Static properties:Resources.ShowRawOffsetsAndBytesBeforeInstruction}"></CheckBox>
 				</StackPanel>
 			</GroupBox>
 			<GroupBox Header="{x:Static properties:Resources.TreeViewOptions}">
 				<StackPanel Margin="3">
-					<CheckBox IsChecked="{Binding ShowMetadataTokens}" Content="{x:Static properties:Resources.ShowMetadataTokens}"></CheckBox>
-					<CheckBox IsChecked="{Binding ShowMetadataTokensInBase10}" Content="{x:Static properties:Resources.ShowMetadataTokensInBase10}"></CheckBox>
-					<CheckBox IsChecked="{Binding HideEmptyMetadataTables}" Content="{x:Static properties:Resources.HideEmptyMetadataTables}"></CheckBox>
-					<CheckBox IsChecked="{Binding UseNestedNamespaceNodes}" Content="{x:Static properties:Resources.UseNestedNamespaceNodes}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.ShowMetadataTokens}" Content="{x:Static properties:Resources.ShowMetadataTokens}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.ShowMetadataTokensInBase10}" Content="{x:Static properties:Resources.ShowMetadataTokensInBase10}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.HideEmptyMetadataTables}" Content="{x:Static properties:Resources.HideEmptyMetadataTables}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.UseNestedNamespaceNodes}" Content="{x:Static properties:Resources.UseNestedNamespaceNodes}"></CheckBox>
 				</StackPanel>
 			</GroupBox>
 			<GroupBox Header="{x:Static properties:Resources.OtherOptions}">
 				<StackPanel Margin="3">
-					<CheckBox IsChecked="{Binding SortResults}" Content="{x:Static properties:Resources.SortResultsFitness}"></CheckBox>
-					<CheckBox IsChecked="{Binding StyleWindowTitleBar}" Content="{x:Static properties:Resources.StyleTheWindowTitleBar}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.SortResults}" Content="{x:Static properties:Resources.SortResultsFitness}"></CheckBox>
+					<CheckBox IsChecked="{Binding Settings.StyleWindowTitleBar}" Content="{x:Static properties:Resources.StyleTheWindowTitleBar}"></CheckBox>
 				</StackPanel>
 			</GroupBox>
 		</StackPanel>
diff --git a/ILSpy/Options/DisplaySettingsPanel.xaml.cs b/ILSpy/Options/DisplaySettingsPanel.xaml.cs
index 7101665c7..3b51a16ba 100644
--- a/ILSpy/Options/DisplaySettingsPanel.xaml.cs
+++ b/ILSpy/Options/DisplaySettingsPanel.xaml.cs
@@ -27,17 +27,19 @@ using System.Windows.Media;
 using System.Windows.Threading;
 using System.Xml.Linq;
 
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX.Settings;
 
+using TomsToolbox.Wpf.Composition.Mef;
+using TomsToolbox.Wpf.Converters;
+
 namespace ICSharpCode.ILSpy.Options
 {
 	/// <summary>
 	/// Interaction logic for DisplaySettingsPanel.xaml
 	/// </summary>
-	[ExportOptionPage(Title = nameof(Properties.Resources.Display), Order = 20)]
 	[PartCreationPolicy(CreationPolicy.NonShared)]
-	public partial class DisplaySettingsPanel : UserControl, IOptionPage
+	[DataTemplate(typeof(DisplaySettingsViewModel))]
+	public partial class DisplaySettingsPanel
 	{
 		public DisplaySettingsPanel()
 		{
@@ -45,132 +47,6 @@ namespace ICSharpCode.ILSpy.Options
 
 			DataObject.AddPastingHandler(tabSizeTextBox, OnPaste);
 			DataObject.AddPastingHandler(indentSizeTextBox, OnPaste);
-
-			Task<FontFamily[]> task = new Task<FontFamily[]>(FontLoader);
-			task.Start();
-			task.ContinueWith(
-				delegate (Task continuation) {
-					App.Current.Dispatcher.Invoke(
-						DispatcherPriority.Normal,
-						(Action)(
-							() => {
-								fontSelector.ItemsSource = task.Result;
-								if (continuation.Exception != null)
-								{
-									foreach (var ex in continuation.Exception.InnerExceptions)
-									{
-										MessageBox.Show(ex.ToString());
-									}
-								}
-							})
-					);
-				}
-			);
-		}
-
-		public void Load(ILSpySettings settings)
-		{
-			this.DataContext = LoadDisplaySettings(settings);
-		}
-
-		static bool IsSymbolFont(FontFamily fontFamily)
-		{
-			foreach (var tf in fontFamily.GetTypefaces())
-			{
-				GlyphTypeface glyph;
-				try
-				{
-					if (tf.TryGetGlyphTypeface(out glyph))
-						return glyph.Symbol;
-				}
-				catch (Exception)
-				{
-					return true;
-				}
-			}
-			return false;
-		}
-
-		static FontFamily[] FontLoader()
-		{
-			return (from ff in Fonts.SystemFontFamilies
-					where !IsSymbolFont(ff)
-					orderby ff.Source
-					select ff).ToArray();
-		}
-
-		public static DisplaySettings LoadDisplaySettings(ILSpySettings settings, SessionSettings? sessionSettings = null)
-		{
-			XElement e = settings["DisplaySettings"];
-			var s = new DisplaySettings();
-			s.SelectedFont = new FontFamily((string?)e.Attribute("Font") ?? "Consolas");
-			s.SelectedFontSize = (double?)e.Attribute("FontSize") ?? 10.0 * 4 / 3;
-			s.ShowLineNumbers = (bool?)e.Attribute("ShowLineNumbers") ?? false;
-			s.ShowMetadataTokens = (bool?)e.Attribute("ShowMetadataTokens") ?? false;
-			s.ShowMetadataTokensInBase10 = (bool?)e.Attribute("ShowMetadataTokensInBase10") ?? false;
-			s.ShowDebugInfo = (bool?)e.Attribute("ShowDebugInfo") ?? false;
-			s.EnableWordWrap = (bool?)e.Attribute("EnableWordWrap") ?? false;
-			s.SortResults = (bool?)e.Attribute("SortResults") ?? true;
-			s.FoldBraces = (bool?)e.Attribute("FoldBraces") ?? false;
-			s.ExpandMemberDefinitions = (bool?)e.Attribute("ExpandMemberDefinitions") ?? false;
-			s.ExpandUsingDeclarations = (bool?)e.Attribute("ExpandUsingDeclarations") ?? false;
-			s.IndentationUseTabs = (bool?)e.Attribute("IndentationUseTabs") ?? true;
-			s.IndentationSize = (int?)e.Attribute("IndentationSize") ?? 4;
-			s.IndentationTabSize = (int?)e.Attribute("IndentationTabSize") ?? 4;
-			s.HighlightMatchingBraces = (bool?)e.Attribute("HighlightMatchingBraces") ?? true;
-			s.HighlightCurrentLine = (bool?)e.Attribute("HighlightCurrentLine") ?? false;
-			s.HideEmptyMetadataTables = (bool?)e.Attribute("HideEmptyMetadataTables") ?? true;
-			s.UseNestedNamespaceNodes = (bool?)e.Attribute("UseNestedNamespaceNodes") ?? false;
-			s.ShowRawOffsetsAndBytesBeforeInstruction = (bool?)e.Attribute("ShowRawOffsetsAndBytesBeforeInstruction") ?? false;
-			s.StyleWindowTitleBar = (bool?)e.Attribute("StyleWindowTitleBar") ?? false;
-
-			s.Theme = (sessionSettings ?? SettingsService.Instance.SessionSettings).Theme;
-
-			return s;
-		}
-
-		public void Save(XElement root)
-		{
-			var s = (DisplaySettings)this.DataContext;
-
-			var section = new XElement("DisplaySettings");
-			section.SetAttributeValue("Font", s.SelectedFont.Source);
-			section.SetAttributeValue("FontSize", s.SelectedFontSize);
-			section.SetAttributeValue("ShowLineNumbers", s.ShowLineNumbers);
-			section.SetAttributeValue("ShowMetadataTokens", s.ShowMetadataTokens);
-			section.SetAttributeValue("ShowMetadataTokensInBase10", s.ShowMetadataTokensInBase10);
-			section.SetAttributeValue("ShowDebugInfo", s.ShowDebugInfo);
-			section.SetAttributeValue("EnableWordWrap", s.EnableWordWrap);
-			section.SetAttributeValue("SortResults", s.SortResults);
-			section.SetAttributeValue("FoldBraces", s.FoldBraces);
-			section.SetAttributeValue("ExpandMemberDefinitions", s.ExpandMemberDefinitions);
-			section.SetAttributeValue("ExpandUsingDeclarations", s.ExpandUsingDeclarations);
-			section.SetAttributeValue("IndentationUseTabs", s.IndentationUseTabs);
-			section.SetAttributeValue("IndentationSize", s.IndentationSize);
-			section.SetAttributeValue("IndentationTabSize", s.IndentationTabSize);
-			section.SetAttributeValue("HighlightMatchingBraces", s.HighlightMatchingBraces);
-			section.SetAttributeValue("HighlightCurrentLine", s.HighlightCurrentLine);
-			section.SetAttributeValue("HideEmptyMetadataTables", s.HideEmptyMetadataTables);
-			section.SetAttributeValue("UseNestedNamespaceNodes", s.UseNestedNamespaceNodes);
-			section.SetAttributeValue("ShowRawOffsetsAndBytesBeforeInstruction", s.ShowRawOffsetsAndBytesBeforeInstruction);
-			section.SetAttributeValue("StyleWindowTitleBar", s.StyleWindowTitleBar);
-
-			SettingsService.Instance.SessionSettings.Theme = s.Theme;
-			var sessionSettings = SettingsService.Instance.SessionSettings.ToXml();
-
-			SettingsService.Instance.DisplaySettings.CopyValues(s);
-
-			Update(section);
-			Update(sessionSettings);
-
-			void Update(XElement element)
-			{
-				var existingElement = root.Element(element.Name);
-				if (existingElement != null)
-					existingElement.ReplaceWith(element);
-				else
-					root.Add(element);
-			}
 		}
 
 		private void TextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
@@ -179,44 +55,39 @@ namespace ICSharpCode.ILSpy.Options
 				e.Handled = true;
 		}
 
-		private void OnPaste(object sender, DataObjectPastingEventArgs e)
+		private static void OnPaste(object sender, DataObjectPastingEventArgs e)
 		{
 			if (!e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true))
 				return;
+
 			var text = (string)e.SourceDataObject.GetData(DataFormats.UnicodeText, true) ?? string.Empty;
+
 			if (!text.All(char.IsDigit))
 				e.CancelCommand();
 		}
-
-		public void LoadDefaults()
-		{
-			SettingsService.Instance.DisplaySettings.CopyValues(new DisplaySettings());
-			this.DataContext = SettingsService.Instance.DisplaySettings;
-		}
 	}
 
-	public class FontSizeConverter : IValueConverter
+	public sealed class FontSizeConverter : ValueConverter
 	{
-		public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+		protected override object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 		{
 			if (value is double d)
 			{
 				return Math.Round(d / 4 * 3);
 			}
 
-			throw new NotImplementedException();
+			return DependencyProperty.UnsetValue;
 		}
 
-		public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+		protected override object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
 		{
-			if (value is string s)
-			{
-				if (double.TryParse(s, out double d))
-					return d * 4 / 3;
-				return 11.0 * 4 / 3;
-			}
+			if (value is not string s)
+				return DependencyProperty.UnsetValue;
+
+			if (double.TryParse(s, out double d))
+				return d * 4 / 3;
 
-			throw new NotImplementedException();
+			return 11.0 * 4 / 3;
 		}
 	}
 }
\ No newline at end of file
diff --git a/ILSpy/Options/DisplaySettingsViewModel.cs b/ILSpy/Options/DisplaySettingsViewModel.cs
new file mode 100644
index 000000000..4d7c53f43
--- /dev/null
+++ b/ILSpy/Options/DisplaySettingsViewModel.cs
@@ -0,0 +1,94 @@
+using ICSharpCode.ILSpyX.Settings;
+using System.Windows.Media;
+using System.Xml.Linq;
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+using TomsToolbox.Wpf;
+using ICSharpCode.ILSpy.Themes;
+
+namespace ICSharpCode.ILSpy.Options
+{
+	[ExportOptionPage(Order = 20)]
+	[PartCreationPolicy(CreationPolicy.NonShared)]
+	public class DisplaySettingsViewModel : ObservableObject, IOptionPage
+	{
+		private DisplaySettings settings = new();
+		private FontFamily[] fontFamilies;
+		private SessionSettings sessionSettings;
+
+		public DisplaySettingsViewModel()
+		{
+			fontFamilies = [settings.SelectedFont];
+
+			Task.Run(FontLoader).ContinueWith(continuation => {
+				FontFamilies = continuation.Result;
+				if (continuation.Exception == null)
+					return;
+				foreach (var ex in continuation.Exception.InnerExceptions)
+				{
+					MessageBox.Show(ex.ToString());
+				}
+			});
+		}
+
+		public string Title => Properties.Resources.Display;
+
+		public DisplaySettings Settings {
+			get => settings;
+			set => SetProperty(ref settings, value);
+		}
+
+		public SessionSettings SessionSettings {
+			get => sessionSettings;
+			set => SetProperty(ref sessionSettings, value);
+		}
+
+		public FontFamily[] FontFamilies {
+			get => fontFamilies;
+			set => SetProperty(ref fontFamilies, value);
+		}
+
+		public int[] FontSizes { get; } = Enumerable.Range(6, 24 - 6 + 1).ToArray();
+
+		public void Load(SettingsSnapshot snapshot)
+		{
+			Settings = snapshot.GetSettings<DisplaySettings>();
+			SessionSettings = snapshot.GetSettings<SessionSettings>();
+		}
+
+		static bool IsSymbolFont(FontFamily fontFamily)
+		{
+			foreach (var tf in fontFamily.GetTypefaces())
+			{
+				try
+				{
+					if (tf.TryGetGlyphTypeface(out GlyphTypeface glyph))
+						return glyph.Symbol;
+				}
+				catch (Exception)
+				{
+					return true;
+				}
+			}
+			return false;
+		}
+
+		static FontFamily[] FontLoader()
+		{
+			return Fonts.SystemFontFamilies
+				.Where(ff => !IsSymbolFont(ff))
+				.OrderBy(ff => ff.Source)
+				.ToArray();
+		}
+
+		public void LoadDefaults()
+		{
+			Settings.LoadFromXml(new XElement("empty"));
+			SessionSettings.Theme = ThemeManager.Current.DefaultTheme;
+		}
+	}
+}
diff --git a/ICSharpCode.ILSpyX/Settings/MiscSettings.cs b/ILSpy/Options/MiscSettings.cs
similarity index 56%
rename from ICSharpCode.ILSpyX/Settings/MiscSettings.cs
rename to ILSpy/Options/MiscSettings.cs
index d349764d4..75f35e81d 100644
--- a/ICSharpCode.ILSpyX/Settings/MiscSettings.cs
+++ b/ILSpy/Options/MiscSettings.cs
@@ -19,25 +19,42 @@
 using System;
 using System.Xml.Linq;
 
+using TomsToolbox.Wpf;
+
 namespace ICSharpCode.ILSpyX.Settings
 {
-	public class MiscSettings : IMiscSettings, ISettingsSection<MiscSettings>
+	public class MiscSettings : ObservableObject, ISettingsSection
 	{
-		private MiscSettings()
-		{
+		private bool allowMultipleInstances;
+		private bool loadPreviousAssemblies = true;
+
+		public bool AllowMultipleInstances {
+			get => allowMultipleInstances;
+			set => SetProperty(ref allowMultipleInstances, value);
+		}
+
+		public bool LoadPreviousAssemblies {
+			get => loadPreviousAssemblies;
+			set => SetProperty(ref loadPreviousAssemblies, value);
 		}
 
-		public bool AllowMultipleInstances { get; set; }
-		public bool LoadPreviousAssemblies { get; set; }
+		public XName SectionName => "MiscSettings";
 
-		public static MiscSettings Load(ISettingsProvider settingsProvider)
+		public void LoadFromXml(XElement e)
 		{
-			XElement e = settingsProvider["MiscSettings"];
-			var s = new MiscSettings();
-			s.AllowMultipleInstances = (bool?)e.Attribute(nameof(s.AllowMultipleInstances)) ?? false;
-			s.LoadPreviousAssemblies = (bool?)e.Attribute(nameof(s.LoadPreviousAssemblies)) ?? true;
+			AllowMultipleInstances = (bool?)e.Attribute(nameof(AllowMultipleInstances)) ?? false;
+			LoadPreviousAssemblies = (bool?)e.Attribute(nameof(LoadPreviousAssemblies)) ?? true;
+		}
+
+		public XElement SaveToXml()
+		{
+			var section = new XElement(SectionName);
 
-			return s;
+			section.SetAttributeValue(nameof(AllowMultipleInstances), AllowMultipleInstances);
+			section.SetAttributeValue(nameof(LoadPreviousAssemblies), LoadPreviousAssemblies);
+
+			return section;
 		}
 	}
 }
+
diff --git a/ILSpy/Options/MiscSettingsPanel.xaml b/ILSpy/Options/MiscSettingsPanel.xaml
index 502f65fcc..c3a984c9b 100644
--- a/ILSpy/Options/MiscSettingsPanel.xaml
+++ b/ILSpy/Options/MiscSettingsPanel.xaml
@@ -2,16 +2,17 @@
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
-             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" 
 			 xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
-			 mc:Ignorable="d" 
+			 xmlns:options="clr-namespace:ICSharpCode.ILSpy.Options"
+			 d:DataContext="{d:DesignInstance options:MiscSettingsViewModel}"
              d:DesignHeight="300" d:DesignWidth="300">
 	<StackPanel Orientation="Vertical">
 		<GroupBox Header="{x:Static properties:Resources.Misc}">
 			<StackPanel Margin="10">
-				<CheckBox IsChecked="{Binding AllowMultipleInstances}" Content="{x:Static properties:Resources.AllowMultipleInstances}" />
-				<CheckBox IsChecked="{Binding LoadPreviousAssemblies}" Content="{x:Static properties:Resources.LoadAssembliesThatWereLoadedInTheLastInstance}"/>
-				<Button Command="{Binding AddRemoveShellIntegrationCommand}" IsEnabled="{Binding EnableShellIntegrationCommand}" Content="{Binding AddRemoveShellIntegrationText}" Margin="3" />
+				<CheckBox IsChecked="{Binding Settings.AllowMultipleInstances}" Content="{x:Static properties:Resources.AllowMultipleInstances}" />
+				<CheckBox IsChecked="{Binding Settings.LoadPreviousAssemblies}" Content="{x:Static properties:Resources.LoadAssembliesThatWereLoadedInTheLastInstance}"/>
+				<Button Command="{Binding AddRemoveShellIntegrationCommand}" Content="{Binding AddRemoveShellIntegrationText, FallbackValue=temp}" Margin="3" />
 			</StackPanel>
 		</GroupBox>
 	</StackPanel>
diff --git a/ILSpy/Options/MiscSettingsPanel.xaml.cs b/ILSpy/Options/MiscSettingsPanel.xaml.cs
index d72778b40..d6c7c672d 100644
--- a/ILSpy/Options/MiscSettingsPanel.xaml.cs
+++ b/ILSpy/Options/MiscSettingsPanel.xaml.cs
@@ -20,53 +20,20 @@ using System.ComponentModel.Composition;
 using System.Windows.Controls;
 using System.Xml.Linq;
 
-using ICSharpCode.ILSpyX.Settings;
+using TomsToolbox.Wpf.Composition.Mef;
 
 namespace ICSharpCode.ILSpy.Options
 {
 	/// <summary>
 	/// Interaction logic for MiscSettingsPanel.xaml
 	/// </summary>
-	[ExportOptionPage(Title = nameof(Properties.Resources.Misc), Order = 30)]
+	[DataTemplate(typeof(MiscSettingsViewModel))]
 	[PartCreationPolicy(CreationPolicy.NonShared)]
-	public partial class MiscSettingsPanel : UserControl, IOptionPage
+	public partial class MiscSettingsPanel
 	{
 		public MiscSettingsPanel()
 		{
 			InitializeComponent();
 		}
-
-		public void Load(ILSpySettings settings)
-		{
-			this.DataContext = LoadMiscSettings(settings);
-		}
-
-		static MiscSettingsViewModel? currentMiscSettings;
-
-		public static MiscSettingsViewModel CurrentMiscSettings {
-			get {
-				return currentMiscSettings ?? (currentMiscSettings = LoadMiscSettings(ILSpySettings.Load()));
-			}
-		}
-
-		public static MiscSettingsViewModel LoadMiscSettings(ILSpySettings settings)
-		{
-			var s = MiscSettings.Load(settings);
-			return new MiscSettingsViewModel(s);
-		}
-
-		public void Save(XElement root)
-		{
-			var s = (MiscSettingsViewModel)this.DataContext;
-			IMiscSettings.Save(root, s);
-
-			currentMiscSettings = null; // invalidate cached settings
-		}
-
-		public void LoadDefaults()
-		{
-			currentMiscSettings = new MiscSettingsViewModel(MiscSettings.Load(ILSpySettings.Load()));
-			this.DataContext = currentMiscSettings;
-		}
 	}
 }
diff --git a/ILSpy/Options/MiscSettingsViewModel.cs b/ILSpy/Options/MiscSettingsViewModel.cs
index 727773a45..2ed4b9868 100644
--- a/ILSpy/Options/MiscSettingsViewModel.cs
+++ b/ILSpy/Options/MiscSettingsViewModel.cs
@@ -16,89 +16,61 @@
 // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 // DEALINGS IN THE SOFTWARE.
 
-using System.ComponentModel;
+using System.ComponentModel.Composition;
 using System.IO;
 using System.Reflection;
-using System.Runtime.CompilerServices;
 using System.Windows;
 using System.Windows.Input;
+using System.Xml.Linq;
 
 using ICSharpCode.ILSpy.AppEnv;
-using ICSharpCode.ILSpy.Commands;
 using ICSharpCode.ILSpyX.Settings;
 
 using Microsoft.Win32;
 
+using TomsToolbox.Wpf;
+
 namespace ICSharpCode.ILSpy.Options
 {
-	public class MiscSettingsViewModel : IMiscSettings, INotifyPropertyChanged
+	[ExportOptionPage(Order = 30)]
+	[PartCreationPolicy(CreationPolicy.NonShared)]
+	public class MiscSettingsViewModel : ObservableObject, IOptionPage
 	{
-		bool allowMultipleInstances;
-		bool loadPreviousAssemblies = true;
-
-		public MiscSettingsViewModel(MiscSettings s)
-		{
-			AllowMultipleInstances = s.AllowMultipleInstances;
-			LoadPreviousAssemblies = s.LoadPreviousAssemblies;
-
-			if (EnableShellIntegrationCommand)
-			{
-				AddRemoveShellIntegrationCommand = new DelegateCommand<object>(AddRemoveShellIntegration);
-			}
-		}
-
-		/// <summary>
-		/// Allow multiple instances.
-		/// </summary>
-		public bool AllowMultipleInstances {
-			get { return allowMultipleInstances; }
-			set {
-				if (allowMultipleInstances != value)
-				{
-					allowMultipleInstances = value;
-					OnPropertyChanged();
-				}
-			}
+		private MiscSettings settings;
+		public MiscSettings Settings {
+			get => settings;
+			set => SetProperty(ref settings, value);
 		}
 
-		/// <summary>
-		/// Load assemblies that were loaded in the previous instance
-		/// </summary>
-		public bool LoadPreviousAssemblies {
-			get { return loadPreviousAssemblies; }
-			set {
-				if (loadPreviousAssemblies != value)
-				{
-					loadPreviousAssemblies = value;
-					OnPropertyChanged();
-				}
-			}
-		}
-
-		public ICommand AddRemoveShellIntegrationCommand { get; }
-		public bool EnableShellIntegrationCommand => AppEnvironment.IsWindows;
+		public ICommand AddRemoveShellIntegrationCommand => new DelegateCommand(() => AppEnvironment.IsWindows, AddRemoveShellIntegration);
 
 		const string rootPath = @"Software\Classes\{0}\shell";
 		const string fullPath = @"Software\Classes\{0}\shell\Open with ILSpy\command";
 
-		private void AddRemoveShellIntegration(object obj)
+		private void AddRemoveShellIntegration()
 		{
-			string commandLine = CommandLineTools.ArgumentArrayToCommandLine(Path.ChangeExtension(Assembly.GetEntryAssembly().Location, ".exe")) + " \"%L\"";
+			string commandLine = CommandLineTools.ArgumentArrayToCommandLine(Path.ChangeExtension(Assembly.GetEntryAssembly()?.Location, ".exe")) + " \"%L\"";
 			if (RegistryEntriesExist())
 			{
 				if (MessageBox.Show(string.Format(Properties.Resources.RemoveShellIntegrationMessage, commandLine), "ILSpy", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
 				{
-					Registry.CurrentUser.CreateSubKey(string.Format(rootPath, "dllfile")).DeleteSubKeyTree("Open with ILSpy");
-					Registry.CurrentUser.CreateSubKey(string.Format(rootPath, "exefile")).DeleteSubKeyTree("Open with ILSpy");
+					Registry.CurrentUser
+						.CreateSubKey(string.Format(rootPath, "dllfile"))?
+						.DeleteSubKeyTree("Open with ILSpy");
+					Registry.CurrentUser
+						.CreateSubKey(string.Format(rootPath, "exefile"))?
+						.DeleteSubKeyTree("Open with ILSpy");
 				}
 			}
 			else
 			{
 				if (MessageBox.Show(string.Format(Properties.Resources.AddShellIntegrationMessage, commandLine), "ILSpy", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
 				{
-					Registry.CurrentUser.CreateSubKey(string.Format(fullPath, "dllfile"))?
+					Registry.CurrentUser
+						.CreateSubKey(string.Format(fullPath, "dllfile"))?
 						.SetValue("", commandLine);
-					Registry.CurrentUser.CreateSubKey(string.Format(fullPath, "exefile"))?
+					Registry.CurrentUser
+						.CreateSubKey(string.Format(fullPath, "exefile"))?
 						.SetValue("", commandLine);
 				}
 			}
@@ -117,20 +89,16 @@ namespace ICSharpCode.ILSpy.Options
 			}
 		}
 
-		#region INotifyPropertyChanged Implementation
+		public string Title => Properties.Resources.Misc;
 
-		public event PropertyChangedEventHandler? PropertyChanged;
-
-		protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+		public void Load(SettingsSnapshot settings)
 		{
-			PropertyChanged?.Invoke(this, e);
+			Settings = settings.GetSettings<MiscSettings>();
 		}
 
-		protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+		public void LoadDefaults()
 		{
-			OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
+			Settings.LoadFromXml(new XElement("dummy"));
 		}
-
-		#endregion
 	}
 }
diff --git a/ILSpy/Options/OptionsDialog.xaml b/ILSpy/Options/OptionsDialog.xaml
index 7dd6d38df..ce6102195 100644
--- a/ILSpy/Options/OptionsDialog.xaml
+++ b/ILSpy/Options/OptionsDialog.xaml
@@ -1,35 +1,43 @@
 <Window x:Class="ICSharpCode.ILSpy.Options.OptionsDialog"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-	    xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
-	    xmlns:options="clr-namespace:ICSharpCode.ILSpy.Options"
-	    Style="{DynamicResource DialogWindow}"
+        xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
+        xmlns:options="clr-namespace:ICSharpCode.ILSpy.Options"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:toms="urn:TomsToolbox"
+        mc:Ignorable="d"
+        d:DataContext="{d:DesignInstance options:OptionsDialogViewModel}"
+        Style="{StaticResource DialogWindow}"
         WindowStartupLocation="CenterOwner"
         ResizeMode="CanResizeWithGrip"
         Title="{x:Static properties:Resources.Options}" Height="500" Width="600">
-	<Grid>
-		<Grid.RowDefinitions>
-			<RowDefinition
-				Height="1*" />
-			<RowDefinition
-				Height="Auto" />
-		</Grid.RowDefinitions>
-		<TabControl Name="tabControl" SelectedValuePath="Content">
+	<DockPanel>
+		<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="12,8">
+			<Button Margin="2,0" 
+			        Command="{Binding ResetDefaultsCommand}"
+			        Content="{x:Static properties:Resources.ResetToDefaults}" />
+			<Button IsDefault="True" Margin="2,0" 
+			        toms:Button.DialogResult="true"
+			        Command="{Binding CommitCommand}" 
+			        Content="{x:Static properties:Resources.OK}" />
+			<Button IsCancel="True" Margin="2,0" 
+			        Content="{x:Static properties:Resources.Cancel}" />
+		</StackPanel>
+		<TabControl ItemsSource="{Binding OptionPages}"
+					SelectedIndex="0"
+		            SelectedValuePath="Content"
+		            SelectedValue="{Binding SelectedPage}">
 			<TabControl.ItemTemplate>
-				<DataTemplate DataType="options:TabItemViewModel">
-					<TextBlock Text="{Binding Header}" />
+				<DataTemplate DataType="options:OptionsItemViewModel">
+					<TextBlock Text="{Binding Title}" />
 				</DataTemplate>
 			</TabControl.ItemTemplate>
 			<TabControl.ContentTemplate>
-				<DataTemplate DataType="options:TabItemViewModel">
+				<DataTemplate DataType="options:OptionsItemViewModel">
 					<ContentPresenter Content="{Binding Content}" />
 				</DataTemplate>
 			</TabControl.ContentTemplate>
 		</TabControl>
-		<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="12,8">
-			<Button Margin="2,0" Name="defaultsButton" Click="DefaultsButton_Click" Content="{x:Static properties:Resources.ResetToDefaults}" />
-			<Button IsDefault="True" Margin="2,0" Name="okButton" Click="OKButton_Click" Content="{x:Static properties:Resources.OK}" />
-			<Button IsCancel="True" Margin="2,0" Content="{x:Static properties:Resources.Cancel}" />
-		</StackPanel>
-	</Grid>
+	</DockPanel>
 </Window>
\ No newline at end of file
diff --git a/ILSpy/Options/OptionsDialog.xaml.cs b/ILSpy/Options/OptionsDialog.xaml.cs
index 184dc9b1c..783488175 100644
--- a/ILSpy/Options/OptionsDialog.xaml.cs
+++ b/ILSpy/Options/OptionsDialog.xaml.cs
@@ -18,105 +18,42 @@
 
 using System;
 using System.ComponentModel.Composition;
-using System.Linq;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Documents;
-using System.Windows.Media;
-using System.Xml.Linq;
 
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Properties;
-using ICSharpCode.ILSpyX.Settings;
-
-using TomsToolbox.Composition;
 
 namespace ICSharpCode.ILSpy.Options
 {
-	public class TabItemViewModel
-	{
-		public TabItemViewModel(string header, UIElement content)
-		{
-			Header = header;
-			Content = content;
-		}
-
-		public string Header { get; }
-		public UIElement Content { get; }
-	}
-
 	/// <summary>
 	/// Interaction logic for OptionsDialog.xaml
 	/// </summary>
-	public partial class OptionsDialog : Window
+	public sealed partial class OptionsDialog
 	{
-		readonly IExport<UIElement, IOptionsMetadata>[] optionPages;
-
 		public OptionsDialog()
 		{
+			DataContext = new OptionsDialogViewModel();
 			InitializeComponent();
-			var ep = App.ExportProvider;
-			this.optionPages = ep.GetExports<UIElement, IOptionsMetadata>("OptionPages").ToArray();
-			ILSpySettings settings = ILSpySettings.Load();
-			foreach (var optionPage in optionPages.OrderBy(p => p.Metadata.Order))
-			{
-				var tabItem = new TabItemViewModel(MainWindow.GetResourceString(optionPage.Metadata.Title), optionPage.Value);
-
-				tabControl.Items.Add(tabItem);
-
-				IOptionPage? page = optionPage.Value as IOptionPage;
-				if (page != null)
-					page.Load(settings);
-			}
-		}
-
-		void OKButton_Click(object sender, RoutedEventArgs e)
-		{
-			ILSpySettings.Update(
-				delegate (XElement root) {
-					foreach (var optionPage in optionPages)
-					{
-						IOptionPage? page = optionPage.Value as IOptionPage;
-						if (page != null)
-							page.Save(root);
-					}
-				});
-			this.DialogResult = true;
-			Close();
-		}
-
-		private void DefaultsButton_Click(object sender, RoutedEventArgs e)
-		{
-			if (MessageBox.Show(Properties.Resources.ResetToDefaultsConfirmationMessage, "ILSpy", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
-			{
-				var page = tabControl.SelectedValue as IOptionPage;
-				if (page != null)
-					page.LoadDefaults();
-			}
 		}
 	}
 
 	public interface IOptionsMetadata
 	{
-		string Title { get; }
 		int Order { get; }
 	}
 
 	public interface IOptionPage
 	{
-		void Load(ILSpySettings settings);
-		void Save(XElement root);
+		string Title { get; }
+
+		void Load(SettingsSnapshot settings);
+
 		void LoadDefaults();
 	}
 
 	[MetadataAttribute]
-	[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
-	public class ExportOptionPageAttribute : ExportAttribute
+	[AttributeUsage(AttributeTargets.Class)]
+	public sealed class ExportOptionPageAttribute() : ExportAttribute("OptionPages", typeof(IOptionPage)), IOptionsMetadata
 	{
-		public ExportOptionPageAttribute() : base("OptionPages", typeof(UIElement))
-		{ }
-
-		public string Title { get; set; }
-
 		public int Order { get; set; }
 	}
 
@@ -124,13 +61,22 @@ namespace ICSharpCode.ILSpy.Options
 	[PartCreationPolicy(CreationPolicy.Shared)]
 	sealed class ShowOptionsCommand : SimpleCommand
 	{
-		public override void Execute(object? parameter)
+		private readonly AssemblyTreeModel assemblyTreeModel;
+
+		[ImportingConstructor]
+		public ShowOptionsCommand(AssemblyTreeModel assemblyTreeModel)
+		{
+			this.assemblyTreeModel = assemblyTreeModel;
+		}
+
+		public override void Execute(object parameter)
 		{
-			OptionsDialog dlg = new OptionsDialog();
-			dlg.Owner = MainWindow.Instance;
+			OptionsDialog dlg = new() {
+				Owner = MainWindow.Instance,
+			};
 			if (dlg.ShowDialog() == true)
 			{
-				new RefreshCommand().Execute(parameter);
+				assemblyTreeModel.Refresh();
 			}
 		}
 	}
diff --git a/ILSpy/Options/OptionsDialogViewModel.cs b/ILSpy/Options/OptionsDialogViewModel.cs
new file mode 100644
index 000000000..38aad4e4b
--- /dev/null
+++ b/ILSpy/Options/OptionsDialogViewModel.cs
@@ -0,0 +1,68 @@
+using System.Linq;
+using System.Windows;
+using System.Windows.Input;
+
+using TomsToolbox.Essentials;
+using TomsToolbox.Wpf;
+
+#nullable enable
+
+namespace ICSharpCode.ILSpy.Options
+{
+	public class OptionsItemViewModel(IOptionPage content) : ObservableObject
+	{
+		public string Title { get; } = content.Title;
+
+		public IOptionPage Content { get; } = content;
+	}
+
+	public class OptionsDialogViewModel : ObservableObject
+	{
+		private IOptionPage? selectedPage;
+
+		private SettingsSnapshot snapshot;
+
+		private readonly IOptionPage[] optionPages = App.ExportProvider.GetExports<IOptionPage, IOptionsMetadata>("OptionPages")
+			.OrderBy(page => page.Metadata?.Order)
+			.Select(item => item.Value)
+			.ExceptNullItems()
+			.ToArray();
+
+		public OptionsDialogViewModel()
+		{
+			this.snapshot = SettingsService.Instance.CreateSnapshot();
+
+			foreach (var optionPage in optionPages)
+			{
+				optionPage.Load(this.snapshot);
+			}
+
+			OptionPages = optionPages.Select(page => new OptionsItemViewModel(page)).ToArray();
+			SelectedPage = optionPages.FirstOrDefault();
+		}
+
+		public OptionsItemViewModel[] OptionPages { get; }
+
+		public IOptionPage? SelectedPage {
+			get => selectedPage;
+			set => SetProperty(ref selectedPage, value);
+		}
+
+		public ICommand CommitCommand => new DelegateCommand(Commit);
+
+		public ICommand ResetDefaultsCommand => new DelegateCommand(ResetDefaults);
+
+		private void ResetDefaults()
+		{
+			if (MessageBox.Show(Properties.Resources.ResetToDefaultsConfirmationMessage, "ILSpy", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+			{
+				SelectedPage?.LoadDefaults();
+			}
+		}
+
+		private void Commit()
+		{
+			snapshot.Save();
+		}
+	}
+}
diff --git a/ILSpy/Properties/AssemblyInfo.cs b/ILSpy/Properties/AssemblyInfo.cs
index 8e5ce2530..cf41117da 100644
--- a/ILSpy/Properties/AssemblyInfo.cs
+++ b/ILSpy/Properties/AssemblyInfo.cs
@@ -1,6 +1,5 @@
 #region Using directives
 
-using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using System.Resources;
@@ -26,7 +25,7 @@ using System.Runtime.Versioning;
 [assembly: ComVisible(false)]
 
 [assembly: AssemblyVersion(DecompilerVersionInfo.Major + "." + DecompilerVersionInfo.Minor + "." + DecompilerVersionInfo.Build + "." + DecompilerVersionInfo.Revision)]
-[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithShortCommitHash)]
+[assembly: AssemblyInformationalVersion(DecompilerVersionInfo.FullVersionWithCommitHash)]
 [assembly: NeutralResourcesLanguage("en-US")]
 
 [assembly: SupportedOSPlatform("Windows7.0")]
diff --git a/ILSpy/Search/SearchPane.xaml.cs b/ILSpy/Search/SearchPane.xaml.cs
index c9ae39930..e1484c806 100644
--- a/ILSpy/Search/SearchPane.xaml.cs
+++ b/ILSpy/Search/SearchPane.xaml.cs
@@ -20,6 +20,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.ComponentModel;
 using System.ComponentModel.Composition;
 using System.Diagnostics;
 using System.Text.RegularExpressions;
@@ -32,7 +33,7 @@ using System.Windows.Media;
 using System.Windows.Threading;
 
 using ICSharpCode.ILSpy.AppEnv;
-using ICSharpCode.ILSpy.Util;
+using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.ViewModels;
 using ICSharpCode.ILSpyX;
 using ICSharpCode.ILSpyX.Extensions;
@@ -67,7 +68,7 @@ namespace ICSharpCode.ILSpy.Search
 
 			ContextMenuProvider.Add(listBox);
 			MessageBus<CurrentAssemblyListChangedEventArgs>.Subscribers += (sender, e) => CurrentAssemblyList_Changed();
-			MessageBus<LanguageSettingsChangedEventArgs>.Subscribers += (sender, e) => LanguageSettings_PropertyChanged();
+			MessageBus<SettingsChangedEventArgs>.Subscribers += (sender, e) => Settings_PropertyChanged(sender, e);
 
 			CompositionTarget.Rendering += UpdateResults;
 		}
@@ -85,8 +86,11 @@ namespace ICSharpCode.ILSpy.Search
 			}
 		}
 
-		void LanguageSettings_PropertyChanged()
+		void Settings_PropertyChanged(object sender, PropertyChangedEventArgs e)
 		{
+			if (sender is not LanguageSettings)
+				return;
+
 			UpdateFilter();
 		}
 
@@ -250,8 +254,8 @@ namespace ICSharpCode.ILSpy.Search
 			{
 
 				searchProgressBar.IsIndeterminate = true;
-				startedSearch = new(await mainWindow.CurrentAssemblyList.GetAllAssemblies(), searchTerm,
-					(SearchMode)searchModeComboBox.SelectedIndex, mainWindow.CurrentLanguage,
+				startedSearch = new(await mainWindow.AssemblyTreeModel.AssemblyList.GetAllAssemblies(), searchTerm,
+					(SearchMode)searchModeComboBox.SelectedIndex, mainWindow.AssemblyTreeModel.CurrentLanguage,
 					SettingsService.Instance.SessionSettings.LanguageSettings.ShowApiLevel);
 				currentSearch = startedSearch;
 
@@ -269,7 +273,7 @@ namespace ICSharpCode.ILSpy.Search
 		{
 			if (listBox.SelectedItem is SearchResult result)
 			{
-				MainWindow.Instance.JumpToReference(result.Reference);
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(result.Reference));
 			}
 		}
 
@@ -543,5 +547,10 @@ namespace ICSharpCode.ILSpy.Search
 			gestures.Add(new KeyGesture(Key.F, ModifierKeys.Control | ModifierKeys.Shift));
 			gestures.Add(new KeyGesture(Key.E, ModifierKeys.Control));
 		}
+
+		protected override void OnExecute(object sender, ExecutedRoutedEventArgs e)
+		{
+			DockWorkspace.Instance.ShowToolPane(SearchPaneModel.PaneContentId);
+		}
 	}
 }
\ No newline at end of file
diff --git a/ILSpy/Search/SearchPaneModel.cs b/ILSpy/Search/SearchPaneModel.cs
index 6ddcd9f04..b3bc30c40 100644
--- a/ILSpy/Search/SearchPaneModel.cs
+++ b/ILSpy/Search/SearchPaneModel.cs
@@ -20,7 +20,6 @@ using System.ComponentModel.Composition;
 using System.Windows.Input;
 using System.Windows.Media;
 
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpy.ViewModels;
 using ICSharpCode.ILSpyX.Search;
 
diff --git a/ILSpy/SessionSettings.cs b/ILSpy/SessionSettings.cs
index 74aaaa177..ddd9f7826 100644
--- a/ILSpy/SessionSettings.cs
+++ b/ILSpy/SessionSettings.cs
@@ -17,6 +17,7 @@
 // DEALINGS IN THE SOFTWARE.
 
 using System;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.Globalization;
 using System.Linq;
@@ -28,9 +29,7 @@ using System.Xml.Linq;
 
 using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.Themes;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX.Search;
-using ICSharpCode.ILSpyX.Settings;
 
 namespace ICSharpCode.ILSpy
 {
@@ -38,58 +37,37 @@ namespace ICSharpCode.ILSpy
 	/// Per-session setting:
 	/// Loaded at startup; saved at exit.
 	/// </summary>
-	public sealed class SessionSettings : INotifyPropertyChanged
+	public sealed class SessionSettings : ISettingsSection
 	{
-		public SessionSettings(ILSpySettings spySettings)
-		{
-			XElement doc = spySettings["SessionSettings"];
-
-			XElement? filterSettings = doc.Element("FilterSettings");
-			if (filterSettings == null)
-				filterSettings = new XElement("FilterSettings");
-
-			this.LanguageSettings = new LanguageSettings(filterSettings);
-
-			this.ActiveAssemblyList = (string)doc.Element("ActiveAssemblyList");
-
-			XElement? activeTreeViewPath = doc.Element("ActiveTreeViewPath");
-			if (activeTreeViewPath != null)
-			{
-				this.ActiveTreeViewPath = activeTreeViewPath.Elements().Select(e => Unescape((string)e)).ToArray();
-			}
-			this.ActiveAutoLoadedAssembly = (string)doc.Element("ActiveAutoLoadedAssembly");
+		public XName SectionName => "SessionSettings";
 
-			this.WindowState = FromString((string)doc.Element("WindowState"), WindowState.Normal);
-			this.WindowBounds = FromString((string)doc.Element("WindowBounds"), DefaultWindowBounds);
-			this.SelectedSearchMode = FromString((string)doc.Element("SelectedSearchMode"), SearchMode.TypeAndMember);
-			this.Theme = FromString((string)doc.Element(nameof(Theme)), ThemeManager.Current.DefaultTheme);
-			string? currentCulture = (string)doc.Element(nameof(CurrentCulture));
-			this.CurrentCulture = string.IsNullOrEmpty(currentCulture) ? null : currentCulture;
-
-			this.DockLayout = new DockLayoutSettings(doc.Element("DockLayout"));
-		}
-
-		public event PropertyChangedEventHandler? PropertyChanged;
-
-		void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+		public void LoadFromXml(XElement section)
 		{
-			var args = new PropertyChangedEventArgs(propertyName);
-
-			PropertyChanged?.Invoke(this, args);
-
-			MessageBus.Send(this, new SessionSettingsChangedEventArgs(args));
+			XElement filterSettings = section.Element("FilterSettings") ?? new XElement("FilterSettings");
+
+			LanguageSettings = new(filterSettings, this);
+			LanguageSettings.PropertyChanged += (sender, e) => PropertyChanged?.Invoke(sender, e);
+
+			ActiveAssemblyList = (string)section.Element("ActiveAssemblyList");
+			ActiveTreeViewPath = section.Element("ActiveTreeViewPath")?.Elements().Select(e => Unescape((string)e)).ToArray();
+			ActiveAutoLoadedAssembly = (string)section.Element("ActiveAutoLoadedAssembly");
+			WindowState = FromString((string)section.Element("WindowState"), WindowState.Normal);
+			WindowBounds = FromString((string)section.Element("WindowBounds"), DefaultWindowBounds);
+			SelectedSearchMode = FromString((string)section.Element("SelectedSearchMode"), SearchMode.TypeAndMember);
+			Theme = FromString((string)section.Element(nameof(Theme)), ThemeManager.Current.DefaultTheme);
+			var culture = (string)section.Element(nameof(CurrentCulture));
+			CurrentCulture = string.IsNullOrEmpty(culture) ? null : culture;
+			DockLayout = new(section.Element("DockLayout"));
 		}
 
-		public LanguageSettings LanguageSettings { get; }
+		public LanguageSettings LanguageSettings { get; set; }
 
 		public SearchMode SelectedSearchMode { get; set; }
 
+		private string theme;
 		public string Theme {
-			get => ThemeManager.Current.Theme;
-			set {
-				ThemeManager.Current.Theme = value;
-				OnPropertyChanged();
-			}
+			get => theme;
+			set => SetProperty(ref theme, value);
 		}
 
 		public string[] ActiveTreeViewPath;
@@ -121,56 +99,51 @@ namespace ICSharpCode.ILSpy
 
 		public WindowState WindowState;
 		public Rect WindowBounds;
-		internal static Rect DefaultWindowBounds = new Rect(10, 10, 750, 550);
+		internal static Rect DefaultWindowBounds = new(10, 10, 750, 550);
 
-		public DockLayoutSettings DockLayout { get; }
+		public DockLayoutSettings DockLayout { get; set; }
 
-		public XElement ToXml()
+		public XElement SaveToXml()
 		{
-			XElement doc = new XElement("SessionSettings");
-			doc.Add(this.LanguageSettings.SaveAsXml());
+			var section = new XElement(SectionName);
+
+			section.Add(this.LanguageSettings.SaveAsXml());
 			if (this.ActiveAssemblyList != null)
 			{
-				doc.Add(new XElement("ActiveAssemblyList", this.ActiveAssemblyList));
+				section.Add(new XElement("ActiveAssemblyList", this.ActiveAssemblyList));
 			}
 			if (this.ActiveTreeViewPath != null)
 			{
-				doc.Add(new XElement("ActiveTreeViewPath", ActiveTreeViewPath.Select(p => new XElement("Node", Escape(p)))));
+				section.Add(new XElement("ActiveTreeViewPath", ActiveTreeViewPath.Select(p => new XElement("Node", Escape(p)))));
 			}
 			if (this.ActiveAutoLoadedAssembly != null)
 			{
-				doc.Add(new XElement("ActiveAutoLoadedAssembly", this.ActiveAutoLoadedAssembly));
+				section.Add(new XElement("ActiveAutoLoadedAssembly", this.ActiveAutoLoadedAssembly));
 			}
-			doc.Add(new XElement("WindowState", ToString(this.WindowState)));
-			doc.Add(new XElement("WindowBounds", ToString(this.WindowBounds)));
-			doc.Add(new XElement("SelectedSearchMode", ToString(this.SelectedSearchMode)));
-			doc.Add(new XElement(nameof(Theme), ToString(this.Theme)));
+			section.Add(new XElement("WindowState", ToString(this.WindowState)));
+			section.Add(new XElement("WindowBounds", ToString(this.WindowBounds)));
+			section.Add(new XElement("SelectedSearchMode", ToString(this.SelectedSearchMode)));
+			section.Add(new XElement(nameof(Theme), ToString(this.Theme)));
 			if (this.CurrentCulture != null)
 			{
-				doc.Add(new XElement(nameof(CurrentCulture), this.CurrentCulture));
+				section.Add(new XElement(nameof(CurrentCulture), this.CurrentCulture));
 			}
-
 			var dockLayoutElement = new XElement("DockLayout");
 			if (DockLayout.Valid)
 			{
 				dockLayoutElement.Add(DockLayout.SaveAsXml());
 			}
-			doc.Add(dockLayoutElement);
-			return doc;
-		}
+			section.Add(dockLayoutElement);
 
-		public void Save()
-		{
-			var doc = ToXml();
-			ILSpySettings.SaveSettings(doc);
+			return section;
 		}
 
-		static Regex regex = new Regex("\\\\x(?<num>[0-9A-f]{4})");
+		static Regex regex = new("\\\\x(?<num>[0-9A-f]{4})");
 		private string? activeAssemblyList;
 
 		static string Escape(string p)
 		{
-			StringBuilder sb = new StringBuilder();
+			StringBuilder sb = new();
 			foreach (char ch in p)
 			{
 				if (char.IsLetterOrDigit(ch))
@@ -206,5 +179,21 @@ namespace ICSharpCode.ILSpy
 			TypeConverter c = TypeDescriptor.GetConverter(typeof(T));
 			return c.ConvertToInvariantString(obj);
 		}
+
+		public event PropertyChangedEventHandler PropertyChanged;
+
+		private void OnPropertyChanged([CallerMemberName] string propertyName = null)
+		{
+			PropertyChanged?.Invoke(this, new(propertyName));
+		}
+
+		private bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
+		{
+			if (EqualityComparer<T>.Default.Equals(field, value))
+				return false;
+			field = value;
+			OnPropertyChanged(propertyName);
+			return true;
+		}
 	}
 }
diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs
index a3f412d09..dd2361ddc 100644
--- a/ILSpy/SolutionWriter.cs
+++ b/ILSpy/SolutionWriter.cs
@@ -28,6 +28,7 @@ using System.Threading.Tasks;
 using ICSharpCode.Decompiler;
 using ICSharpCode.Decompiler.Solution;
 using ICSharpCode.Decompiler.Util;
+using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.TextView;
 using ICSharpCode.ILSpyX;
 
@@ -212,7 +213,7 @@ namespace ICSharpCode.ILSpy
 				using (var projectFileWriter = new StreamWriter(projectFileName))
 				{
 					var projectFileOutput = new PlainTextOutput(projectFileWriter);
-					var options = MainWindow.Instance.CreateDecompilationOptions();
+					var options = SettingsService.Instance.CreateDecompilationOptions(DockWorkspace.Instance.ActiveTabPage);
 					options.FullDecompilation = true;
 					options.CancellationToken = ct;
 					options.SaveAsProjectDirectory = targetDirectory;
diff --git a/ILSpy/TaskHelper.cs b/ILSpy/TaskHelper.cs
index 630d1d7d2..462143edd 100644
--- a/ILSpy/TaskHelper.cs
+++ b/ILSpy/TaskHelper.cs
@@ -198,7 +198,7 @@ namespace ICSharpCode.ILSpy
 		public static void HandleExceptions(this Task task)
 		{
 			task.Catch<Exception>(exception => MainWindow.Instance.Dispatcher.BeginInvoke(new Action(delegate {
-				AvalonEditTextOutput output = new AvalonEditTextOutput();
+				AvalonEditTextOutput output = new();
 				output.Write(exception.ToString());
 				Docking.DockWorkspace.Instance.ShowText(output);
 			}))).IgnoreExceptions();
diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs
index f500fd374..927215a25 100644
--- a/ILSpy/TextView/DecompilerTextView.cs
+++ b/ILSpy/TextView/DecompilerTextView.cs
@@ -52,11 +52,11 @@ using ICSharpCode.Decompiler.CSharp.OutputVisitor;
 using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
 using ICSharpCode.Decompiler.Documentation;
 using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.AvalonEdit;
 using ICSharpCode.ILSpy.Options;
 using ICSharpCode.ILSpy.Themes;
 using ICSharpCode.ILSpy.TreeNodes;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpy.ViewModels;
 using ICSharpCode.ILSpyX;
 
@@ -414,7 +414,7 @@ namespace ICSharpCode.ILSpy.TextView
 			}
 			else if (segment.Reference is EntityReference unresolvedEntity)
 			{
-				var module = unresolvedEntity.ResolveAssembly(MainWindow.Instance.CurrentAssemblyList);
+				var module = unresolvedEntity.ResolveAssembly(MainWindow.Instance.AssemblyTreeModel.AssemblyList);
 				if (module == null)
 					return null;
 				var typeSystem = new DecompilerTypeSystem(module,
@@ -474,7 +474,7 @@ namespace ICSharpCode.ILSpy.TextView
 
 			IEntity? ResolveReference(string idString)
 			{
-				return MainWindow.FindEntityInRelevantAssemblies(idString, MainWindow.Instance.CurrentAssemblyList.GetAssemblies());
+				return AssemblyTreeModel.FindEntityInRelevantAssemblies(idString, MainWindow.Instance.AssemblyTreeModel.AssemblyList.GetAssemblies());
 			}
 		}
 
@@ -1001,7 +1001,7 @@ namespace ICSharpCode.ILSpy.TextView
 					return;
 				}
 			}
-			MainWindow.Instance.JumpToReference(reference, openInNewTab);
+			MessageBus.Send(this, new NavigateToReferenceEventArgs(reference, openInNewTab));
 		}
 
 		Point? mouseDownPos;
diff --git a/ILSpy/TextView/DocumentationUIBuilder.cs b/ILSpy/TextView/DocumentationUIBuilder.cs
index 1e0cd1e8d..75b3d552f 100644
--- a/ILSpy/TextView/DocumentationUIBuilder.cs
+++ b/ILSpy/TextView/DocumentationUIBuilder.cs
@@ -37,7 +37,6 @@ using ICSharpCode.Decompiler.Documentation;
 using ICSharpCode.Decompiler.Output;
 using ICSharpCode.Decompiler.TypeSystem;
 using ICSharpCode.ILSpy.Options;
-using ICSharpCode.ILSpy.Util;
 
 namespace ICSharpCode.ILSpy.TextView
 {
@@ -339,7 +338,7 @@ namespace ICSharpCode.ILSpy.TextView
 		{
 			var h = new Hyperlink(new Run(ambience.ConvertSymbol(referencedEntity)));
 			h.Click += (sender, e) => {
-				MainWindow.Instance.JumpToReference(referencedEntity);
+				MessageBus.Send(this, new NavigateToReferenceEventArgs(referencedEntity));
 			};
 			return h;
 		}
@@ -382,7 +381,7 @@ namespace ICSharpCode.ILSpy.TextView
 				{
 					Hyperlink link = new Hyperlink();
 					link.Click += (sender, e) => {
-						MainWindow.Instance.JumpToReference(referencedEntity);
+						MessageBus.Send(this, new NavigateToReferenceEventArgs(referencedEntity));
 					};
 					AddSpan(link, element.Children);
 				}
diff --git a/ILSpy/Themes/ThemeManager.cs b/ILSpy/Themes/ThemeManager.cs
index 419e4026c..dae84fc93 100644
--- a/ILSpy/Themes/ThemeManager.cs
+++ b/ILSpy/Themes/ThemeManager.cs
@@ -20,6 +20,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Linq;
 using System.Windows;
 using System.Windows.Controls;
@@ -42,6 +43,7 @@ namespace ICSharpCode.ILSpy.Themes
 		private ThemeManager()
 		{
 			Application.Current.Resources.MergedDictionaries.Add(_themeDictionaryContainer);
+			MessageBus<SettingsChangedEventArgs>.Subscribers += (sender, e) => Settings_Changed(sender, e);
 		}
 
 		public string DefaultTheme => "Light";
@@ -199,5 +201,13 @@ namespace ICSharpCode.ILSpy.Themes
 			var b = (byte)((b1 + m) * 255f);
 			return (r, g, b);
 		}
+
+		private void Settings_Changed(object? sender, PropertyChangedEventArgs e)
+		{
+			if (sender is not SessionSettings settings || e.PropertyName != nameof(SessionSettings.Theme))
+				return;
+
+			Theme = settings.Theme;
+		}
 	}
 }
diff --git a/ILSpy/Themes/WindowStyleManagerBehavior.cs b/ILSpy/Themes/WindowStyleManagerBehavior.cs
index 8388be987..4b8aa62a2 100644
--- a/ILSpy/Themes/WindowStyleManagerBehavior.cs
+++ b/ILSpy/Themes/WindowStyleManagerBehavior.cs
@@ -24,7 +24,6 @@ using System.Windows.Interop;
 using System.Windows.Media;
 
 using ICSharpCode.ILSpy.Options;
-using ICSharpCode.ILSpy.Util;
 
 using TomsToolbox.Essentials;
 using TomsToolbox.Wpf;
diff --git a/ILSpy/TreeNodes/AssemblyListTreeNode.cs b/ILSpy/TreeNodes/AssemblyListTreeNode.cs
index 218771aa7..ee8bae8ef 100644
--- a/ILSpy/TreeNodes/AssemblyListTreeNode.cs
+++ b/ILSpy/TreeNodes/AssemblyListTreeNode.cs
@@ -87,13 +87,10 @@ namespace ICSharpCode.ILSpy.TreeNodes
 			e.Effects = XPlatDragDropEffects.Move | XPlatDragDropEffects.Copy | XPlatDragDropEffects.Link;
 			if (e.Data.GetDataPresent(AssemblyTreeNode.DataFormat))
 				return true;
-			else if (e.Data.GetDataPresent(DataFormats.FileDrop))
+			if (e.Data.GetDataPresent(DataFormats.FileDrop))
 				return true;
-			else
-			{
-				e.Effects = XPlatDragDropEffects.None;
-				return false;
-			}
+			e.Effects = XPlatDragDropEffects.None;
+			return false;
 		}
 
 		public override void Drop(IPlatformDragEventArgs e, int index)
@@ -110,8 +107,8 @@ namespace ICSharpCode.ILSpy.TreeNodes
 					.Distinct()
 					.ToArray();
 				assemblyList.Move(assemblies, index);
-				var nodes = assemblies.SelectArray(MainWindow.Instance.FindTreeNode);
-				MainWindow.Instance.SelectNodes(nodes);
+				var nodes = assemblies.SelectArray(MainWindow.Instance.AssemblyTreeModel.FindTreeNode);
+				MainWindow.Instance.AssemblyTreeModel.SelectNodes(nodes);
 			}
 		}
 
@@ -187,7 +184,7 @@ namespace ICSharpCode.ILSpy.TreeNodes
 				if (bundle == null)
 					return null;
 				bundle.EnsureLazyChildren();
-				foreach (var node in ILSpyX.TreeView.TreeTraversal.PreOrder(bundle.Children, ExpandAndGetChildren).OfType<AssemblyTreeNode>())
+				foreach (var node in TreeTraversal.PreOrder(bundle.Children, ExpandAndGetChildren).OfType<AssemblyTreeNode>())
 				{
 					if (node.LoadedAssembly == asm)
 						return node;
diff --git a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs
index 25602d875..57a326353 100644
--- a/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs
+++ b/ILSpy/TreeNodes/AssemblyReferenceTreeNode.cs
@@ -26,7 +26,6 @@ using ICSharpCode.Decompiler;
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.Decompiler.TypeSystem;
 using ICSharpCode.ILSpy.Themes;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
 
 namespace ICSharpCode.ILSpy.TreeNodes
diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs
index c0540530e..724688d59 100644
--- a/ILSpy/TreeNodes/AssemblyTreeNode.cs
+++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs
@@ -30,10 +30,11 @@ using ICSharpCode.Decompiler;
 using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.Decompiler.TypeSystem;
+using ICSharpCode.ILSpy.AssemblyTree;
 using ICSharpCode.ILSpy.Controls.TreeView;
+using ICSharpCode.ILSpy.Docking;
 using ICSharpCode.ILSpy.Metadata;
 using ICSharpCode.ILSpy.Properties;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpy.ViewModels;
 using ICSharpCode.ILSpyX;
 using ICSharpCode.ILSpyX.FileLoaders;
@@ -546,7 +547,7 @@ namespace ICSharpCode.ILSpy.TreeNodes
 			dlg.Filter = language.Name + " project|*" + language.ProjectFileExtension + "|" + language.Name + " single file|*" + language.FileExtension + "|All files|*.*";
 			if (dlg.ShowDialog() == true)
 			{
-				DecompilationOptions options = MainWindow.Instance.CreateDecompilationOptions();
+				var options = SettingsService.Instance.CreateDecompilationOptions(DockWorkspace.Instance.ActiveTabPage);
 				options.FullDecompilation = true;
 				if (dlg.FilterIndex == 1)
 				{
@@ -630,13 +631,13 @@ namespace ICSharpCode.ILSpy.TreeNodes
 			{
 				foreach (var node in context.SelectedTreeNodes)
 				{
-					paths.Add(MainWindow.GetPathForNode(node));
+					paths.Add(AssemblyTreeModel.GetPathForNode(node));
 					var la = ((AssemblyTreeNode)node).LoadedAssembly;
 					la.AssemblyList.ReloadAssembly(la.FileName);
 				}
 			}
-			MainWindow.Instance.SelectNodes(paths.Select(p => MainWindow.Instance.FindNodeByPath(p, true)).ToArray());
-			MainWindow.Instance.RefreshDecompiledView();
+			MainWindow.Instance.AssemblyTreeModel.SelectNodes(paths.Select(p => MainWindow.Instance.AssemblyTreeModel.FindNodeByPath(p, true)).ToArray());
+			MainWindow.Instance.AssemblyTreeModel.RefreshDecompiledView();
 		}
 	}
 
@@ -676,7 +677,7 @@ namespace ICSharpCode.ILSpy.TreeNodes
 				}
 			}
 			await Task.WhenAll(tasks);
-			MainWindow.Instance.RefreshDecompiledView();
+			MainWindow.Instance.AssemblyTreeModel.RefreshDecompiledView();
 		}
 	}
 
@@ -711,7 +712,7 @@ namespace ICSharpCode.ILSpy.TreeNodes
 					node.RaisePropertyChanged(nameof(ILSpyTreeNode.IsAutoLoaded));
 				}
 			}
-			MainWindow.Instance.CurrentAssemblyList.RefreshSave();
+			MainWindow.Instance.AssemblyTreeModel.AssemblyList.RefreshSave();
 		}
 	}
 
diff --git a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs
index 36fdcd729..760685dc9 100644
--- a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs
+++ b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs
@@ -73,7 +73,6 @@ namespace ICSharpCode.ILSpy.TreeNodes
 					continue;
 				var metadata = module.Metadata;
 				var assembly = module.GetTypeSystemOrNull()?.MainModule as MetadataModule;
-
 				if (assembly == null)
 					continue;
 				foreach (var h in metadata.TypeDefinitions)
@@ -108,4 +107,4 @@ namespace ICSharpCode.ILSpy.TreeNodes
 			threading.Decompile(language, output, options, EnsureLazyChildren);
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/ILSpy/TreeNodes/ILSpyTreeNode.cs b/ILSpy/TreeNodes/ILSpyTreeNode.cs
index 9be0705a0..edbb9c213 100644
--- a/ILSpy/TreeNodes/ILSpyTreeNode.cs
+++ b/ILSpy/TreeNodes/ILSpyTreeNode.cs
@@ -27,7 +27,6 @@ using System.Windows.Threading;
 
 using ICSharpCode.Decompiler;
 using ICSharpCode.Decompiler.TypeSystem;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX.Abstractions;
 using ICSharpCode.ILSpyX.TreeView;
 using ICSharpCode.ILSpyX.TreeView.PlatformAbstractions;
@@ -41,9 +40,9 @@ namespace ICSharpCode.ILSpy.TreeNodes
 	{
 		bool childrenNeedFiltering;
 
-		public ILSpyTreeNode()
+		protected ILSpyTreeNode()
 		{
-			MessageBus<LanguageSettingsChangedEventArgs>.Subscribers += LanguageSettings_Changed;
+			MessageBus<SettingsChangedEventArgs>.Subscribers += (sender, e) => Settings_Changed(sender, e);
 		}
 
 		LanguageSettings LanguageSettings => SettingsService.Instance.SessionSettings.LanguageSettings;
@@ -72,8 +71,8 @@ namespace ICSharpCode.ILSpy.TreeNodes
 
 		public override void ActivateItemSecondary(IPlatformRoutedEventArgs e)
 		{
-			MainWindow.Instance.SelectNode(this, inNewTabPage: true);
-			MainWindow.Instance.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)MainWindow.Instance.RefreshDecompiledView);
+			MainWindow.Instance.AssemblyTreeModel.SelectNode(this, inNewTabPage: true);
+			MainWindow.Instance.Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)MainWindow.Instance.AssemblyTreeModel.RefreshDecompiledView);
 		}
 
 		/// <summary>
@@ -86,7 +85,7 @@ namespace ICSharpCode.ILSpy.TreeNodes
 			return false;
 		}
 
-		protected internal override void OnChildrenChanged(NotifyCollectionChangedEventArgs e)
+		public override void OnChildrenChanged(NotifyCollectionChangedEventArgs e)
 		{
 			if (e.NewItems != null)
 			{
@@ -128,8 +127,13 @@ namespace ICSharpCode.ILSpy.TreeNodes
 			}
 		}
 
-		protected virtual void LanguageSettings_Changed(object? sender, EventArgs e)
+		protected virtual void Settings_Changed(object? sender, PropertyChangedEventArgs e)
 		{
+			if (sender is not ILSpy.LanguageSettings)
+				return;
+			if (e.PropertyName is not (nameof(LanguageSettings.Language) or nameof(LanguageSettings.LanguageVersion)))
+				return;
+
 			RaisePropertyChanged(nameof(Text));
 			if (IsVisible)
 			{
diff --git a/ILSpy/Updates/NotifyOfUpdatesStrategy.cs b/ILSpy/Updates/NotifyOfUpdatesStrategy.cs
index d02565784..166d87d42 100644
--- a/ILSpy/Updates/NotifyOfUpdatesStrategy.cs
+++ b/ILSpy/Updates/NotifyOfUpdatesStrategy.cs
@@ -61,7 +61,7 @@ namespace ICSharpCode.ILSpy.Updates
 		/// Returns the download URL if an update is available.
 		/// Returns null if no update is available, or if no check was performed.
 		/// </summary>
-		public static async Task<string?> CheckForUpdatesIfEnabledAsync(ILSpySettings spySettings)
+		public static async Task<string?> CheckForUpdatesIfEnabledAsync(ISettingsProvider spySettings)
 		{
 			UpdateSettings s = new UpdateSettings(spySettings);
 
@@ -87,7 +87,7 @@ namespace ICSharpCode.ILSpy.Updates
 			}
 		}
 
-		public static Task<string?> CheckForUpdatesAsync(ILSpySettings spySettings)
+		public static Task<string?> CheckForUpdatesAsync(ISettingsProvider spySettings)
 		{
 			UpdateSettings s = new UpdateSettings(spySettings);
 			return CheckForUpdateInternal(s);
diff --git a/ILSpy/Updates/UpdateSettings.cs b/ILSpy/Updates/UpdateSettings.cs
index 14022c323..dbf18fbc4 100644
--- a/ILSpy/Updates/UpdateSettings.cs
+++ b/ILSpy/Updates/UpdateSettings.cs
@@ -26,7 +26,7 @@ namespace ICSharpCode.ILSpy.Updates
 {
 	sealed class UpdateSettings : INotifyPropertyChanged
 	{
-		public UpdateSettings(ILSpySettings spySettings)
+		public UpdateSettings(ISettingsProvider spySettings)
 		{
 			XElement s = spySettings["UpdateSettings"];
 			this.automaticUpdateCheckEnabled = (bool?)s.Element("AutomaticUpdateCheckEnabled") ?? true;
@@ -75,7 +75,7 @@ namespace ICSharpCode.ILSpy.Updates
 			updateSettings.Add(new XElement("AutomaticUpdateCheckEnabled", automaticUpdateCheckEnabled));
 			if (lastSuccessfulUpdateCheck != null)
 				updateSettings.Add(new XElement("LastSuccessfulUpdateCheck", lastSuccessfulUpdateCheck));
-			ILSpySettings.SaveSettings(updateSettings);
+			SettingsService.Instance.SpySettings.SaveSettings(updateSettings);
 		}
 
 		public event PropertyChangedEventHandler? PropertyChanged;
diff --git a/ILSpy/Util/MenuService.cs b/ILSpy/Util/MenuService.cs
new file mode 100644
index 000000000..7766ab77a
--- /dev/null
+++ b/ILSpy/Util/MenuService.cs
@@ -0,0 +1,237 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+
+using ICSharpCode.ILSpy.Commands;
+using ICSharpCode.ILSpy.Docking;
+using ICSharpCode.ILSpy.Themes;
+using ICSharpCode.ILSpy.ViewModels;
+
+using TomsToolbox.Composition;
+using TomsToolbox.ObservableCollections;
+using TomsToolbox.Wpf.Converters;
+
+namespace ICSharpCode.ILSpy.Util
+{
+	internal class MenuService
+	{
+		public static readonly MenuService Instance = new();
+
+		private readonly DockWorkspace dockWorkspace = DockWorkspace.Instance;
+
+		public void Init(Menu mainMenu, ToolBar toolBar, InputBindingCollection inputBindings)
+		{
+			InitMainMenu(mainMenu);
+			InitWindowMenu(mainMenu, inputBindings);
+			InitToolbar(toolBar);
+		}
+
+		static void InitMainMenu(Menu mainMenu)
+		{
+			var mainMenuCommands = App.ExportProvider.GetExports<ICommand, IMainMenuCommandMetadata>("MainMenuCommand");
+			// Start by constructing the individual flat menus
+			var parentMenuItems = new Dictionary<string, MenuItem>();
+			var menuGroups = mainMenuCommands.OrderBy(c => c.Metadata?.MenuOrder).GroupBy(c => c.Metadata?.ParentMenuID).ToArray();
+			foreach (var menu in menuGroups)
+			{
+				// Get or add the target menu item and add all items grouped by menu category
+				var parentMenuItem = GetOrAddParentMenuItem(menu.Key, menu.Key);
+				foreach (var category in menu.GroupBy(c => c.Metadata?.MenuCategory))
+				{
+					if (parentMenuItem.Items.Count > 0)
+					{
+						parentMenuItem.Items.Add(new Separator { Tag = category.Key });
+					}
+					foreach (var entry in category)
+					{
+						if (menuGroups.Any(g => g.Key == entry.Metadata?.MenuID))
+						{
+							var menuItem = GetOrAddParentMenuItem(entry.Metadata?.MenuID, entry.Metadata?.Header);
+							// replace potential dummy text with real name
+							menuItem.Header = ResourceHelper.GetString(entry.Metadata?.Header);
+							parentMenuItem.Items.Add(menuItem);
+						}
+						else
+						{
+							var menuItem = new MenuItem {
+								Command = CommandWrapper.Unwrap(entry.Value),
+								Tag = entry.Metadata?.MenuID,
+								Header = ResourceHelper.GetString(entry.Metadata?.Header)
+							};
+							if (!string.IsNullOrEmpty(entry.Metadata?.MenuIcon))
+							{
+								menuItem.Icon = new Image {
+									Width = 16,
+									Height = 16,
+									Source = Images.Load(entry.Value, entry.Metadata.MenuIcon)
+								};
+							}
+
+							menuItem.IsEnabled = entry.Metadata?.IsEnabled ?? false;
+							if (entry.Value is ToggleableCommand)
+							{
+								menuItem.IsCheckable = true;
+								menuItem.SetBinding(MenuItem.IsCheckedProperty, new Binding("IsChecked") { Source = entry.Value, Mode = BindingMode.OneWay });
+							}
+
+							menuItem.InputGestureText = entry.Metadata?.InputGestureText;
+							parentMenuItem.Items.Add(menuItem);
+						}
+					}
+				}
+			}
+
+			foreach (var item in parentMenuItems.Values)
+			{
+				if (item.Parent == null)
+				{
+					mainMenu.Items.Add(item);
+				}
+			}
+
+			MenuItem GetOrAddParentMenuItem(string menuId, string resourceKey)
+			{
+				if (!parentMenuItems.TryGetValue(menuId, out var parentMenuItem))
+				{
+					var topLevelMenuItem = mainMenu.Items.OfType<MenuItem>().FirstOrDefault(m => (string)m.Tag == menuId);
+					if (topLevelMenuItem == null)
+					{
+						parentMenuItem = new() {
+							Header = ResourceHelper.GetString(resourceKey),
+							Tag = menuId
+						};
+						parentMenuItems.Add(menuId, parentMenuItem);
+					}
+					else
+					{
+						parentMenuItems.Add(menuId, topLevelMenuItem);
+						parentMenuItem = topLevelMenuItem;
+					}
+				}
+				return parentMenuItem;
+			}
+		}
+
+		void InitWindowMenu(Menu mainMenu, InputBindingCollection inputBindings)
+		{
+			var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == nameof(Properties.Resources._Window));
+
+			var defaultItems = windowMenuItem.Items.Cast<Control>().ToArray();
+
+			windowMenuItem.Items.Clear();
+
+			var toolItems = dockWorkspace.ToolPanes.ObservableSelect(toolPane => CreateMenuItem(toolPane, inputBindings));
+			var tabItems = dockWorkspace.TabPages.ObservableSelect(tabPage => CreateMenuItem(tabPage, dockWorkspace));
+
+			var allItems = new ObservableCompositeCollection<Control>(defaultItems, [new Separator()], toolItems, [new Separator()], tabItems);
+
+			windowMenuItem.ItemsSource = allItems;
+		}
+
+		static void InitToolbar(ToolBar toolBar)
+		{
+			int navigationPos = 0;
+			int openPos = 1;
+			var toolbarCommandsByTitle = App.ExportProvider.GetExports<ICommand, IToolbarCommandMetadata>("ToolbarCommand")
+				.OrderBy(c => c.Metadata?.ToolbarOrder)
+				.GroupBy(c => c.Metadata?.ToolbarCategory);
+
+			foreach (var commandGroup in toolbarCommandsByTitle)
+			{
+				if (commandGroup.Key == nameof(Properties.Resources.Navigation))
+				{
+					foreach (var command in commandGroup)
+					{
+						toolBar.Items.Insert(navigationPos++, CreateToolbarItem(command));
+						openPos++;
+					}
+				}
+				else if (commandGroup.Key == nameof(Properties.Resources.Open))
+				{
+					foreach (var command in commandGroup)
+					{
+						toolBar.Items.Insert(openPos++, CreateToolbarItem(command));
+					}
+				}
+				else
+				{
+					toolBar.Items.Add(new Separator());
+					foreach (var command in commandGroup)
+					{
+						toolBar.Items.Add(CreateToolbarItem(command));
+					}
+				}
+			}
+
+		}
+
+		static Control CreateMenuItem(TabPageModel pane, DockWorkspace dock)
+		{
+			var header = new TextBlock {
+				MaxWidth = 200,
+				TextTrimming = TextTrimming.CharacterEllipsis
+			};
+
+			header.SetBinding(TextBlock.TextProperty, new Binding(nameof(pane.Title)) {
+				Source = pane
+			});
+
+			MenuItem menuItem = new() {
+				Command = new TabPageCommand(pane),
+				Header = header,
+				IsCheckable = true
+			};
+
+			menuItem.SetBinding(MenuItem.IsCheckedProperty, new Binding(nameof(dock.ActiveTabPage)) {
+				Source = dock,
+				ConverterParameter = pane,
+				Converter = BinaryOperationConverter.Equality
+			});
+
+			return menuItem;
+		}
+
+		static Control CreateMenuItem(ToolPaneModel pane, InputBindingCollection inputBindings)
+		{
+			MenuItem menuItem = new() {
+				Command = pane.AssociatedCommand ?? new ToolPaneCommand(pane.ContentId),
+				Header = pane.Title
+			};
+			var shortcutKey = pane.ShortcutKey;
+			if (shortcutKey != null)
+			{
+				inputBindings.Add(new(menuItem.Command, shortcutKey));
+				menuItem.InputGestureText = shortcutKey.GetDisplayStringForCulture(CultureInfo.CurrentUICulture);
+			}
+			if (!string.IsNullOrEmpty(pane.Icon))
+			{
+				menuItem.Icon = new Image {
+					Width = 16,
+					Height = 16,
+					Source = Images.Load(pane, pane.Icon)
+				};
+			}
+
+			return menuItem;
+		}
+
+		static Button CreateToolbarItem(IExport<ICommand, IToolbarCommandMetadata> command)
+		{
+			return new() {
+				Style = ThemeManager.Current.CreateToolBarButtonStyle(),
+				Command = CommandWrapper.Unwrap(command.Value),
+				ToolTip = Properties.Resources.ResourceManager.GetString(command.Metadata?.ToolTip),
+				Tag = command.Metadata?.Tag,
+				Content = new Image {
+					Width = 16,
+					Height = 16,
+					Source = Images.Load(command.Value, command.Metadata?.ToolbarIcon)
+				}
+			};
+		}
+	}
+}
diff --git a/ILSpy/Util/MessageBus.cs b/ILSpy/Util/MessageBus.cs
index 7573cb33c..b08b29dc6 100644
--- a/ILSpy/Util/MessageBus.cs
+++ b/ILSpy/Util/MessageBus.cs
@@ -22,16 +22,16 @@ namespace ICSharpCode.ILSpy.Util
 	public static class MessageBus<T>
 		where T : EventArgs
 	{
-		private static readonly WeakEventSource<T> Subscriptions = new();
+		private static readonly WeakEventSource<T> subscriptions = new();
 
 		public static event EventHandler<T> Subscribers {
-			add => Subscriptions.Subscribe(value);
-			remove => Subscriptions.Unsubscribe(value);
+			add => subscriptions.Subscribe(value);
+			remove => subscriptions.Unsubscribe(value);
 		}
 
 		public static void Send(object sender, T e)
 		{
-			Subscriptions.Raise(sender, e);
+			subscriptions.Raise(sender, e);
 		}
 	}
 
@@ -52,9 +52,14 @@ namespace ICSharpCode.ILSpy.Util
 
 	public class CurrentAssemblyListChangedEventArgs(NotifyCollectionChangedEventArgs e) : WrappedEventArgs<NotifyCollectionChangedEventArgs>(e);
 
-	public class LanguageSettingsChangedEventArgs(PropertyChangedEventArgs e) : WrappedEventArgs<PropertyChangedEventArgs>(e);
+	public class SettingsChangedEventArgs(PropertyChangedEventArgs e) : WrappedEventArgs<PropertyChangedEventArgs>(e);
 
-	public class SessionSettingsChangedEventArgs(PropertyChangedEventArgs e) : WrappedEventArgs<PropertyChangedEventArgs>(e);
+	public class NavigateToReferenceEventArgs(object reference, bool inNewTabPage = false) : EventArgs
+	{
+		public object Reference { get; } = reference;
+
+		public bool InNewTabPage { get; } = inNewTabPage;
+	}
 
-	public class DockWorkspaceActiveTabPageChangedEventArgs : EventArgs;
+	public class AssemblyTreeSelectionChangedEventArgs() : EventArgs;
 }
\ No newline at end of file
diff --git a/ILSpy/Util/ResourceHelper.cs b/ILSpy/Util/ResourceHelper.cs
new file mode 100644
index 000000000..b37bb2cbb
--- /dev/null
+++ b/ILSpy/Util/ResourceHelper.cs
@@ -0,0 +1,15 @@
+namespace ICSharpCode.ILSpy.Util
+{
+	internal static class ResourceHelper
+	{
+		internal static string GetString(string key)
+		{
+			if (string.IsNullOrEmpty(key))
+				return null;
+
+			string value = Properties.Resources.ResourceManager.GetString(key);
+
+			return !string.IsNullOrEmpty(value) ? value : key;
+		}
+	}
+}
diff --git a/ILSpy/Util/SettingsService.cs b/ILSpy/Util/SettingsService.cs
index 9068733f3..092339b48 100644
--- a/ILSpy/Util/SettingsService.cs
+++ b/ILSpy/Util/SettingsService.cs
@@ -1,34 +1,196 @@
-using ICSharpCode.Decompiler;
+using System;
+using System.Collections.Concurrent;
+using System.ComponentModel;
+using System.Xml.Linq;
+
+using ICSharpCode.Decompiler;
 using ICSharpCode.ILSpy.Options;
+using ICSharpCode.ILSpy.ViewModels;
 using ICSharpCode.ILSpyX;
 using ICSharpCode.ILSpyX.Settings;
 
+using DecompilerSettings = ICSharpCode.ILSpy.Options.DecompilerSettings;
+
+#nullable enable
+
 namespace ICSharpCode.ILSpy.Util
 {
-	public class SettingsService
+	public interface IChildSettings
+	{
+		ISettingsSection Parent { get; }
+	}
+
+	public interface ISettingsSection : INotifyPropertyChanged
+	{
+		XName SectionName { get; }
+
+		void LoadFromXml(XElement section);
+
+		XElement SaveToXml();
+	}
+
+	public abstract class SettingsServiceBase
+	{
+		protected readonly ConcurrentDictionary<Type, ISettingsSection> sections = new();
+
+		public ISettingsProvider SpySettings;
+
+		protected SettingsServiceBase(ISettingsProvider spySettings)
+		{
+			SpySettings = spySettings;
+		}
+
+		public T GetSettings<T>() where T : ISettingsSection, new()
+		{
+			return (T)sections.GetOrAdd(typeof(T), _ => {
+				T section = new T();
+
+				var sectionElement = SpySettings[section.SectionName];
+
+				section.LoadFromXml(sectionElement);
+				section.PropertyChanged += Section_PropertyChanged;
+
+				return section;
+			});
+		}
+
+		protected void SaveSection(ISettingsSection section, XElement root)
+		{
+			var element = section.SaveToXml();
+
+			var existingElement = root.Element(section.SectionName);
+			if (existingElement != null)
+				existingElement.ReplaceWith(element);
+			else
+				root.Add(element);
+		}
+
+		protected virtual void Section_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+		{
+		}
+	}
+
+	public class SettingsSnapshot : SettingsServiceBase
+	{
+		private readonly SettingsService parent;
+
+		public SettingsSnapshot(SettingsService parent) : base(parent.SpySettings)
+		{
+			this.parent = parent;
+		}
+
+		public void Save()
+		{
+			SpySettings.Update(root => {
+				foreach (var section in sections.Values)
+				{
+					SaveSection(section, root);
+				}
+			});
+
+			parent.Reload();
+		}
+	}
+
+	public class SettingsService : SettingsServiceBase
 	{
 		public static readonly SettingsService Instance = new();
 
-		private SettingsService()
+		private SettingsService() : base(LoadSettings())
+		{
+		}
+
+		public SessionSettings SessionSettings => GetSettings<SessionSettings>();
+
+		public DecompilerSettings DecompilerSettings => GetSettings<DecompilerSettings>();
+
+		public DisplaySettings DisplaySettings => GetSettings<DisplaySettings>();
+
+		public MiscSettings MiscSettings => GetSettings<MiscSettings>();
+
+		private AssemblyListManager? assemblyListManager;
+		public AssemblyListManager AssemblyListManager => assemblyListManager ??= new(SpySettings) {
+			ApplyWinRTProjections = DecompilerSettings.ApplyWindowsRuntimeProjections,
+			UseDebugSymbols = DecompilerSettings.UseDebugSymbols
+		};
+
+		public DecompilationOptions CreateDecompilationOptions(TabPageModel tabPage)
+		{
+			return new(SessionSettings.LanguageSettings.LanguageVersion, DecompilerSettings, DisplaySettings) { Progress = tabPage.Content as IProgress<DecompilationProgress> };
+		}
+
+		public AssemblyList LoadInitialAssemblyList()
 		{
-			SpySettings = ILSpySettings.Load();
-			SessionSettings = new(SpySettings);
-			DecompilerSettings = DecompilerSettingsPanel.LoadDecompilerSettings(SpySettings);
-			DisplaySettings = DisplaySettingsPanel.LoadDisplaySettings(SpySettings, SessionSettings);
-			AssemblyListManager = new(SpySettings) {
-				ApplyWinRTProjections = DecompilerSettings.ApplyWindowsRuntimeProjections,
-				UseDebugSymbols = DecompilerSettings.UseDebugSymbols
-			};
+			var loadPreviousAssemblies = MiscSettings.LoadPreviousAssemblies;
+
+			if (loadPreviousAssemblies)
+			{
+				return AssemblyListManager.LoadList(SessionSettings.ActiveAssemblyList);
+			}
+			else
+			{
+				AssemblyListManager.ClearAll();
+				return AssemblyListManager.CreateList(AssemblyListManager.DefaultListName);
+			}
 		}
 
-		public ILSpySettings SpySettings { get; }
+		private bool reloading;
 
-		public SessionSettings SessionSettings { get; }
+		public void Reload()
+		{
+			reloading = true;
 
-		public DecompilerSettings DecompilerSettings { get; set; }
+			try
+			{
+				SpySettings = ILSpySettings.Load();
 
-		public DisplaySettings DisplaySettings { get; }
+				foreach (var section in sections.Values)
+				{
+					var element = SpySettings[section.SectionName];
+
+					section.LoadFromXml(element);
+				}
+			}
+			finally
+			{
+				reloading = false;
+			}
+		}
 
-		public AssemblyListManager AssemblyListManager { get; }
+		public SettingsSnapshot CreateSnapshot()
+		{
+			return new(this);
+		}
+
+		protected override void Section_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+		{
+			base.Section_PropertyChanged(sender, e);
+
+			if (!reloading)
+			{
+				var section = (sender as IChildSettings)?.Parent ?? sender as ISettingsSection;
+
+				if (section != null)
+				{
+					SpySettings.Update(root => {
+						SaveSection(section, root);
+					});
+				};
+			}
+
+			if (sender is DecompilerSettings decompilerSettings && assemblyListManager != null)
+			{
+				assemblyListManager.ApplyWinRTProjections = decompilerSettings.ApplyWindowsRuntimeProjections;
+				assemblyListManager.UseDebugSymbols = decompilerSettings.UseDebugSymbols;
+			}
+
+			MessageBus.Send(sender, new SettingsChangedEventArgs(e));
+		}
+
+		private static ILSpySettings LoadSettings()
+		{
+			ILSpySettings.SettingsFilePathProvider = new ILSpySettingsFilePathProvider();
+			return ILSpySettings.Load();
+		}
 	}
 }
diff --git a/ILSpy/ViewModels/AssemblyListPaneModel.cs b/ILSpy/ViewModels/AssemblyListPaneModel.cs
deleted file mode 100644
index d6156cf8f..000000000
--- a/ILSpy/ViewModels/AssemblyListPaneModel.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2019 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.ComponentModel.Composition;
-using System.Windows;
-using System.Windows.Input;
-
-using ICSharpCode.ILSpy.Properties;
-
-namespace ICSharpCode.ILSpy.ViewModels
-{
-	[ExportToolPane]
-	[PartCreationPolicy(CreationPolicy.Shared)]
-	public class AssemblyListPaneModel : ToolPaneModel
-	{
-		public const string PaneContentId = "assemblyListPane";
-
-		public AssemblyListPaneModel()
-		{
-			Title = Resources.Assemblies;
-			ContentId = PaneContentId;
-			IsCloseable = false;
-			ShortcutKey = new KeyGesture(Key.F6);
-		}
-	}
-}
diff --git a/ILSpy/ViewModels/ManageAssemblyListsViewModel.cs b/ILSpy/ViewModels/ManageAssemblyListsViewModel.cs
index 4b88b4dbe..a9e5ae7af 100644
--- a/ILSpy/ViewModels/ManageAssemblyListsViewModel.cs
+++ b/ILSpy/ViewModels/ManageAssemblyListsViewModel.cs
@@ -27,7 +27,6 @@ using System.Windows.Input;
 using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.ILSpy.Commands;
 using ICSharpCode.ILSpy.Properties;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpyX;
 
 namespace ICSharpCode.ILSpy.ViewModels
diff --git a/ILSpy/ViewModels/PaneModel.cs b/ILSpy/ViewModels/PaneModel.cs
index c8856c7dc..0f4ce987d 100644
--- a/ILSpy/ViewModels/PaneModel.cs
+++ b/ILSpy/ViewModels/PaneModel.cs
@@ -21,7 +21,7 @@ using System.ComponentModel;
 using System.Windows;
 using System.Windows.Input;
 
-using ICSharpCode.ILSpy.Docking;
+using ICSharpCode.Decompiler.IL;
 
 using TomsToolbox.Wpf;
 
@@ -29,6 +29,13 @@ namespace ICSharpCode.ILSpy.ViewModels
 {
 	public abstract class PaneModel : ObservableObject
 	{
+		private Throttle titleChangeThrottle;
+
+		protected PaneModel()
+		{
+			titleChangeThrottle = new Throttle(() => OnPropertyChanged(nameof(Title)));
+		}
+
 		class CloseCommandImpl : ICommand
 		{
 			readonly PaneModel model;
@@ -107,7 +114,10 @@ namespace ICSharpCode.ILSpy.ViewModels
 
 		public string Title {
 			get => title;
-			set => SetProperty(ref title, value);
+			set {
+				title = value;
+				titleChangeThrottle.Tick();
+			}
 		}
 	}
 
diff --git a/ILSpy/ViewModels/TabPageModel.cs b/ILSpy/ViewModels/TabPageModel.cs
index 7972b84b6..90ff9ce57 100644
--- a/ILSpy/ViewModels/TabPageModel.cs
+++ b/ILSpy/ViewModels/TabPageModel.cs
@@ -20,8 +20,6 @@ using System;
 using System.Threading.Tasks;
 
 using ICSharpCode.ILSpy.TextView;
-using ICSharpCode.ILSpy.Util;
-using ICSharpCode.ILSpyX;
 
 namespace ICSharpCode.ILSpy.ViewModels
 {
diff --git a/ILSpy/Views/DebugSteps.xaml.cs b/ILSpy/Views/DebugSteps.xaml.cs
index e69199aff..b7336a43b 100644
--- a/ILSpy/Views/DebugSteps.xaml.cs
+++ b/ILSpy/Views/DebugSteps.xaml.cs
@@ -8,7 +8,6 @@ using System.Windows.Input;
 using ICSharpCode.Decompiler.IL;
 using ICSharpCode.Decompiler.IL.Transforms;
 using ICSharpCode.ILSpy.Docking;
-using ICSharpCode.ILSpy.Util;
 using ICSharpCode.ILSpy.ViewModels;
 
 using TomsToolbox.Wpf.Composition.Mef;
@@ -34,9 +33,9 @@ namespace ICSharpCode.ILSpy
 			InitializeComponent();
 
 #if DEBUG
-			MessageBus<LanguageSettingsChangedEventArgs>.Subscribers += (sender, e) => LanguageSettings_PropertyChanged(sender, e);
+			MessageBus<SettingsChangedEventArgs>.Subscribers += (sender, e) => Settings_PropertyChanged(sender, e);
+			MessageBus<AssemblyTreeSelectionChangedEventArgs>.Subscribers += SelectionChanged;
 
-			MainWindow.Instance.SelectionChanged += SelectionChanged;
 			writingOptions.PropertyChanged += WritingOptions_PropertyChanged;
 
 			if (SettingsService.Instance.SessionSettings.LanguageSettings.Language is ILAstLanguage l)
@@ -53,7 +52,7 @@ namespace ICSharpCode.ILSpy
 			DecompileAsync(lastSelectedStep);
 		}
 
-		private void SelectionChanged(object sender, SelectionChangedEventArgs e)
+		private void SelectionChanged(object sender, EventArgs e)
 		{
 			Dispatcher.Invoke(() => {
 				tree.ItemsSource = null;
@@ -61,10 +60,13 @@ namespace ICSharpCode.ILSpy
 			});
 		}
 
-		private void LanguageSettings_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+		private void Settings_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
 		{
 #if DEBUG
-			if (e.PropertyName == "Language")
+			if (sender is not LanguageSettings)
+				return;
+
+			if (e.PropertyName == nameof(LanguageSettings.Language))
 			{
 				if (language != null)
 				{
@@ -123,8 +125,8 @@ namespace ICSharpCode.ILSpy
 			lastSelectedStep = step;
 			var window = MainWindow.Instance;
 			var state = DockWorkspace.Instance.ActiveTabPage.GetState();
-			DockWorkspace.Instance.ActiveTabPage.ShowTextViewAsync(textView => textView.DecompileAsync(window.CurrentLanguage, window.SelectedNodes,
-				new DecompilationOptions(window.CurrentLanguageVersion, SettingsService.Instance.DecompilerSettings, SettingsService.Instance.DisplaySettings) {
+			DockWorkspace.Instance.ActiveTabPage.ShowTextViewAsync(textView => textView.DecompileAsync(window.AssemblyTreeModel.CurrentLanguage, window.AssemblyTreeModel.SelectedNodes,
+				new DecompilationOptions(window.AssemblyTreeModel.CurrentLanguageVersion, SettingsService.Instance.DecompilerSettings, SettingsService.Instance.DisplaySettings) {
 					StepLimit = step,
 					IsDebug = isDebug,
 					TextViewState = state as TextView.DecompilerTextViewState
diff --git a/ILSpy/Views/OpenFromGacDialog.xaml.cs b/ILSpy/Views/OpenFromGacDialog.xaml.cs
index 5189be034..24b9f7b2b 100644
--- a/ILSpy/Views/OpenFromGacDialog.xaml.cs
+++ b/ILSpy/Views/OpenFromGacDialog.xaml.cs
@@ -31,6 +31,8 @@ using ICSharpCode.Decompiler.Metadata;
 using ICSharpCode.ILSpy.Controls;
 using ICSharpCode.ILSpyX.Extensions;
 
+using TomsToolbox.Essentials;
+
 namespace ICSharpCode.ILSpy
 {
 	/// <summary>
diff --git a/TestPlugin/CustomOptionPage.xaml b/TestPlugin/CustomOptionPage.xaml
index 2aa86fcc1..b5533e7e5 100644
--- a/TestPlugin/CustomOptionPage.xaml
+++ b/TestPlugin/CustomOptionPage.xaml
@@ -1,8 +1,13 @@
 <UserControl x:Class="TestPlugin.CustomOptionPage"
 	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+	xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d:DesignHeight="500" d:DesignWidth="500"
+	xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
+	xmlns:testPlugin="clr-namespace:TestPlugin"
+	d:DataContext="{d:DesignInstance testPlugin:CustomOptionsViewModel}"
+	>
 	<StackPanel>
-		<CheckBox IsChecked="{Binding UselessOption1}">Useless option 1</CheckBox>
-		<Slider Minimum="0" Maximum="100" Value="{Binding UselessOption2}"/>
+		<CheckBox IsChecked="{Binding  Options.UselessOption1}">Useless option 1</CheckBox>
+		<Slider Minimum="0" Maximum="100" Value="{Binding Options.UselessOption2}"/>
 	</StackPanel>
 </UserControl>
\ No newline at end of file
diff --git a/TestPlugin/CustomOptionPage.xaml.cs b/TestPlugin/CustomOptionPage.xaml.cs
index b4d607917..c550e02c0 100644
--- a/TestPlugin/CustomOptionPage.xaml.cs
+++ b/TestPlugin/CustomOptionPage.xaml.cs
@@ -1,99 +1,85 @@
 // Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
 // This code is distributed under MIT X11 license (for details please see \doc\license.txt)
 
-using System.ComponentModel;
 using System.ComponentModel.Composition;
-using System.Windows.Controls;
 using System.Xml.Linq;
 
-using ICSharpCode.ILSpy;
 using ICSharpCode.ILSpy.Options;
-using ICSharpCode.ILSpyX.Settings;
+using ICSharpCode.ILSpy.Util;
+
+using TomsToolbox.Wpf;
+using TomsToolbox.Wpf.Composition.Mef;
 
 namespace TestPlugin
 {
-	[ExportOptionPage(Title = "TestPlugin", Order = 0)]
+	[DataTemplate(typeof(CustomOptionsViewModel))]
 	[PartCreationPolicy(CreationPolicy.NonShared)]
-	partial class CustomOptionPage : UserControl, IOptionPage
+	partial class CustomOptionPage
 	{
-		static readonly XNamespace ns = "http://www.ilspy.net/testplugin";
-
 		public CustomOptionPage()
 		{
 			InitializeComponent();
 		}
+	}
 
-		public void Load(ILSpySettings settings)
-		{
-			// For loading options, use ILSpySetting's indexer.
-			// If the specified section does exist, the indexer will return a new empty element.
-			XElement e = settings[ns + "CustomOptions"];
-			// Now load the options from the XML document:
-			Options s = new Options();
-			s.UselessOption1 = (bool?)e.Attribute("useless1") ?? s.UselessOption1;
-			s.UselessOption2 = (double?)e.Attribute("useless2") ?? s.UselessOption2;
-			this.DataContext = s;
+	[ExportOptionPage(Order = 0)]
+	[PartCreationPolicy(CreationPolicy.NonShared)]
+	class CustomOptionsViewModel : ObservableObject, IOptionPage
+	{
+		private Options options;
+
+		public string Title => "TestPlugin";
+
+		public Options Options {
+			get => options;
+			set => SetProperty(ref options, value);
 		}
 
-		public void LoadDefaults()
+		public void Load(SettingsSnapshot snapshot)
 		{
-			this.DataContext = new Options();
+			this.Options = snapshot.GetSettings<Options>();
 		}
 
-		public void Save(XElement root)
+		public void LoadDefaults()
 		{
-			Options s = (Options)this.DataContext;
-			// Save the options back into XML:
-			XElement section = new XElement(ns + "CustomOptions");
-			section.SetAttributeValue("useless1", s.UselessOption1);
-			section.SetAttributeValue("useless2", s.UselessOption2);
-
-			// Replace the existing section in the settings file, or add a new section,
-			// if required.
-			XElement? existingElement = root.Element(ns + "CustomOptions");
-			if (existingElement != null)
-				existingElement.ReplaceWith(section);
-			else
-				root.Add(section);
+			Options.LoadFromXml(new XElement("dummy"));
 		}
 	}
 
-	class Options : INotifyPropertyChanged
+	class Options : ObservableObject, ISettingsSection
 	{
+		static readonly XNamespace ns = "http://www.ilspy.net/testplugin";
+
 		bool uselessOption1;
 
 		public bool UselessOption1 {
-			get { return uselessOption1; }
-			set {
-				if (uselessOption1 != value)
-				{
-					uselessOption1 = value;
-					OnPropertyChanged("UselessOption1");
-				}
-			}
+			get => uselessOption1;
+			set => SetProperty(ref uselessOption1, value);
 		}
 
 		double uselessOption2;
 
 		public double UselessOption2 {
-			get { return uselessOption2; }
-			set {
-				if (uselessOption2 != value)
-				{
-					uselessOption2 = value;
-					OnPropertyChanged("UselessOption2");
-				}
-			}
+			get => uselessOption2;
+			set => SetProperty(ref uselessOption2, value);
 		}
 
-		public event PropertyChangedEventHandler? PropertyChanged;
+		public XName SectionName { get; } = ns + "CustomOptions";
 
-		protected virtual void OnPropertyChanged(string propertyName)
+		public void LoadFromXml(XElement e)
 		{
-			if (PropertyChanged != null)
-			{
-				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
-			}
+			UselessOption1 = (bool?)e.Attribute("useless1") ?? false;
+			UselessOption2 = (double?)e.Attribute("useless2") ?? 50.0;
+		}
+
+		public XElement SaveToXml()
+		{
+			var section = new XElement(SectionName);
+
+			section.SetAttributeValue("useless1", UselessOption1);
+			section.SetAttributeValue("useless2", UselessOption2);
+
+			return section;
 		}
 	}
 }
\ No newline at end of file
diff --git a/TestPlugin/MainMenuCommand.cs b/TestPlugin/MainMenuCommand.cs
index 3832716c2..99c1c0958 100644
--- a/TestPlugin/MainMenuCommand.cs
+++ b/TestPlugin/MainMenuCommand.cs
@@ -23,7 +23,7 @@ namespace TestPlugin
 	{
 		public override void Execute(object? parameter)
 		{
-			foreach (var loadedAssembly in MainWindow.Instance.CurrentAssemblyList.GetAssemblies())
+			foreach (var loadedAssembly in MainWindow.Instance.AssemblyTreeModel.AssemblyList.GetAssemblies())
 			{
 				loadedAssembly.AssemblyList.Unload(loadedAssembly);
 			}