From 38c9fc6afaf5e86b81580465cd7b0bd9d649f334 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 9 Jun 2026 12:47:33 +0200 Subject: [PATCH] Centralize MEF export lookup in AppComposition.TryGetExport Eleven call sites each wrapped AppComposition.Current.GetExport() in a try/catch that returned null, to survive design-time previews and minimal test hosts where the composition host isn't up. Replace those copies with one AppComposition.TryGetExport()/TryGetExports() and inline it at every site, deleting the per-class wrappers. The shared helper does not blanket-swallow: host absence is checked explicitly (the static field, not the throwing Current), and the built-in TryGetExport(out) reports a missing export as false. So an export that is present but genuinely broken now surfaces its activation error instead of being silently turned into null. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy/AppEnv/AppComposition.cs | 19 ++++++ ILSpy/AssemblyTree/AssemblyTreeModel.cs | 19 ++---- ILSpy/Commands/CompareContextMenuEntry.cs | 9 +-- ILSpy/Docking/DockWorkspace.cs | 25 ++----- ILSpy/Languages/CSharpLanguage.cs | 33 +++------- ILSpy/Languages/ILAstLanguage.cs | 16 ++--- ILSpy/TextView/DecompilerTabPageModel.cs | 65 ++++++------------- .../DocumentSwitcherDropdownBehavior.cs | 14 +--- ILSpy/Themes/MultiRowTabStripBehavior.cs | 14 ++-- ILSpy/Themes/PreviewTabContextMenuBehavior.cs | 9 +-- .../Themes/PreviewTabFreezeButtonBehavior.cs | 9 +-- ILSpy/ViewModels/ContentTabPage.cs | 18 +---- ILSpy/ViewModels/DebugStepsPaneModel.cs | 14 +--- 13 files changed, 79 insertions(+), 185 deletions(-) diff --git a/ILSpy/AppEnv/AppComposition.cs b/ILSpy/AppEnv/AppComposition.cs index 116e073b7..cee63a84c 100644 --- a/ILSpy/AppEnv/AppComposition.cs +++ b/ILSpy/AppEnv/AppComposition.cs @@ -18,8 +18,10 @@ using System; using System.Collections.Generic; +using System.Composition; using System.Composition.Hosting; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.Loader; @@ -45,6 +47,23 @@ namespace ILSpy.AppEnv public static CompositionHost Current => current ?? throw new InvalidOperationException("Composition host is not yet initialized."); + /// + /// Resolves a single MEF export, or returns when the composition host + /// isn't initialized yet (design-time previewer, headless pre-boot) or no such export exists. + /// Unlike a blanket try/catch this does NOT swallow composition/activation errors: host + /// absence is checked explicitly and the built-in TryGetExport reports a missing export + /// as , so a genuinely broken export still surfaces its exception. + /// + public static T? TryGetExport() where T : class + => current is { } host && host.TryGetExport(out T? export) ? export : null; + + /// + /// Resolves all MEF exports of , or an empty sequence when the host + /// isn't initialized yet. for the single-export case. + /// + public static IEnumerable TryGetExports() where T : class + => current?.GetExports() ?? Enumerable.Empty(); + public static CompositionHost Initialize() { RegisterPluginResolver(); diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index 9bf5789de..c516de1a5 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -183,7 +183,7 @@ namespace ILSpy.AssemblyTree { // Open the definition in a fresh carve-out tab instead of replacing the current view // (e.g. "Decompile to new tab" on a symbol in the code). - TryGetExport()?.OpenNodeInNewTab(resolved); + AppEnv.AppComposition.TryGetExport()?.OpenNodeInNewTab(resolved); } else { @@ -194,18 +194,11 @@ namespace ILSpy.AssemblyTree // paints local-reference marks on every match once the new Text lands. if (e.Source is null) return; - var dockWorkspace = TryGetExport(); + var dockWorkspace = AppEnv.AppComposition.TryGetExport(); if (dockWorkspace?.ActiveDecompilerTab is { } decompTab) decompTab.HighlightedReference = e.Source; } - static T? TryGetExport() where T : class - { - try - { return AppEnv.AppComposition.Current.GetExport(); } - catch { return null; } - } - // True while the SelectedItem setter is replacing the collection via Clear()+Add(); // suppresses the selection-changed fan-out until the final state is in place so consumers // never observe the transient empty/multi mid-replace. @@ -455,7 +448,7 @@ namespace ILSpy.AssemblyTree // and its instance IS the AboutCommand, so we can drive its welcome path directly. void ShowAboutWelcomePage() { - var registry = TryGetExport(); + var registry = AppEnv.AppComposition.TryGetExport(); var export = registry?.Commands .FirstOrDefault(c => c.Metadata.Header == nameof(ICSharpCode.ILSpy.Properties.Resources._About)) ?.CreateExport(); @@ -1050,7 +1043,7 @@ namespace ILSpy.AssemblyTree var removed = new HashSet(oldItems.OfType()); if (removed.Count > 0) { - TryGetExport()?.PruneHistory(node => + AppEnv.AppComposition.TryGetExport()?.PruneHistory(node => node.AncestorsAndSelf() .OfType() .Any(a => removed.Contains(a.LoadedAssembly))); @@ -1092,7 +1085,7 @@ namespace ILSpy.AssemblyTree /// (e.g. the ones just resolved). /// public void RefreshDecompiledView() - => TryGetExport()?.ForceRefreshActiveTab(); + => AppEnv.AppComposition.TryGetExport()?.ForceRefreshActiveTab(); /// /// Resolves every assembly reference of each supplied assembly node through that @@ -1154,7 +1147,7 @@ namespace ILSpy.AssemblyTree // SelectedItem setter early-outs, and DockWorkspace.ShowSelectedNode's // dedup short-circuits — leaving stale decompiled text. Force a fresh // render. Mirrors WPF's RefreshDecompiledView() call. - TryGetExport()?.ForceRefreshActiveTab(); + AppEnv.AppComposition.TryGetExport()?.ForceRefreshActiveTab(); } } } diff --git a/ILSpy/Commands/CompareContextMenuEntry.cs b/ILSpy/Commands/CompareContextMenuEntry.cs index 857900e6e..4eedef208 100644 --- a/ILSpy/Commands/CompareContextMenuEntry.cs +++ b/ILSpy/Commands/CompareContextMenuEntry.cs @@ -51,7 +51,7 @@ namespace ILSpy.Commands [AssemblyTreeNode left, AssemblyTreeNode right]) return; - var dockWorkspace = TryGetExport(); + var dockWorkspace = AppComposition.TryGetExport(); if (dockWorkspace == null) return; var pane = new CompareTabPageModel(left.LoadedAssembly, right.LoadedAssembly); @@ -61,12 +61,5 @@ namespace ILSpy.Commands // view is a pair, not a single-node decompile result. dockWorkspace.OpenNewTab(pane); } - - static T? TryGetExport() where T : class - { - try - { return AppComposition.Current.GetExport(); } - catch { return null; } - } } } diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index cf014d817..e96882a1b 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -673,16 +673,7 @@ namespace ILSpy.Docking } static IEnumerable TryGetProtocolHandlers() - { - try - { - return AppEnv.AppComposition.Current.GetExports(); - } - catch - { - return System.Array.Empty(); - } - } + => AppEnv.AppComposition.TryGetExports(); // Long-lived decompiler viewmodel — kept alive across metadata interludes so going // back to text doesn't lose the previous decompile until a fresh one supersedes it. @@ -1133,17 +1124,9 @@ namespace ILSpy.Docking // code-behind subscribes to FocusRequested and posts the focus shift onto the // dispatcher so the freshly-active pane has a frame to surface in the layout // first. Resolving the pane through AppComposition (instead of injecting it) - // keeps the dock-workspace decoupled from the search namespace. - try - { - var search = AppEnv.AppComposition.Current.GetExport(); - search.RequestFocus(); - } - catch - { - // Composition isn't available in design-time previews / minimal tests; the - // activation alone is enough to be useful there. - } + // keeps the dock-workspace decoupled from the search namespace. TryGetExport is + // null in design-time previews / minimal tests, where the activation alone suffices. + AppEnv.AppComposition.TryGetExport()?.RequestFocus(); } public ContentTabPage OpenNewTab(ContentPageModel content, SharpTreeNode? sourceNode = null) diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 4d66b321a..ec67bf510 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -627,19 +627,10 @@ namespace ILSpy.Languages } static IReadOnlyList TryDiscoverHandlers() - { - try - { - return AppEnv.AppComposition.Current.GetExports().ToArray(); - } - catch - { - // Composition isn't available in tests that bypass the host (e.g. invoking - // DecompileAsProject directly with a self-built LoadedAssembly). Fall back - // to the raw-bytes behaviour from the base class. - return System.Array.Empty(); - } - } + // Composition isn't available in tests that bypass the host (e.g. invoking + // DecompileAsProject directly with a self-built LoadedAssembly); TryGetExports then + // yields nothing, falling back to the raw-bytes behaviour from the base class. + => AppEnv.AppComposition.TryGetExports().ToArray(); } static List CollectFieldsAndCtors(ITypeDefinition type, bool isStatic) @@ -820,17 +811,11 @@ namespace ILSpy.Languages static bool HasReferenceErrors(MetadataFile module) { - try - { - var atm = AppEnv.AppComposition.Current.GetExport(); - var loadedAssembly = atm.AssemblyList?.GetAssemblies() - .FirstOrDefault(la => la.GetMetadataFileOrNull() == module); - return loadedAssembly?.LoadedAssemblyReferencesInfo.HasErrors == true; - } - catch - { - return false; - } + // No AssemblyTreeModel without a composition host (tests / minimal hosts) -> no errors. + var atm = AppEnv.AppComposition.TryGetExport(); + var loadedAssembly = atm?.AssemblyList?.GetAssemblies() + .FirstOrDefault(la => la.GetMetadataFileOrNull() == module); + return loadedAssembly?.LoadedAssemblyReferencesInfo.HasErrors == true; } } } diff --git a/ILSpy/Languages/ILAstLanguage.cs b/ILSpy/Languages/ILAstLanguage.cs index c41d18049..478377896 100644 --- a/ILSpy/Languages/ILAstLanguage.cs +++ b/ILSpy/Languages/ILAstLanguage.cs @@ -130,17 +130,6 @@ namespace ILSpy.Languages this.transforms = CSharpDecompiler.GetILTransforms(); } - // DockWorkspace is resolved lazily — taking it as an [ImportingConstructor] argument - // creates a composition cycle (DockWorkspace already imports LanguageService, which - // imports the registered Languages). Lazy lookup at decompile time breaks the cycle - // and Costs only one MEF GetExport call per "Show Steps" button click. - DockWorkspace? TryGetDockWorkspace() - { - try - { return AppComposition.Current.GetExport(); } - catch { return null; } - } - public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options) { base.DecompileMethod(method, output, options); @@ -188,8 +177,11 @@ namespace ILSpy.Languages OnStepperUpdated(); } } + // DockWorkspace is resolved lazily here, not via [ImportingConstructor]: it imports + // LanguageService, which imports the registered Languages, so a constructor import would + // form a composition cycle. The lazy lookup costs one MEF resolve per "Show Steps" click. (output as ISmartTextOutput)?.AddButton(Images.Images.ViewCode, "Show Steps", delegate { - TryGetDockWorkspace()?.ShowToolPane(DebugStepsPaneModel.PaneContentId); + AppComposition.TryGetExport()?.ShowToolPane(DebugStepsPaneModel.PaneContentId); }); output.WriteLine(); il.WriteTo(output, DebugStepsPaneModel.WritingOptions); diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index e8a4073b8..a9587de00 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -278,14 +278,7 @@ namespace ILSpy.TextView // still construct cleanly. TaskbarProgressService? taskbarProgress; TaskbarProgressService? TaskbarProgress - => taskbarProgress ??= TryGetExport(); - - static T? TryGetExport() where T : class - { - try - { return AppEnv.AppComposition.Current.GetExport(); } - catch { return null; } - } + => taskbarProgress ??= AppEnv.AppComposition.TryGetExport(); public DecompilerTabPageModel() { @@ -358,16 +351,12 @@ namespace ILSpy.TextView { if (CurrentNode is not { } node) return; - try - { - var languageService = AppEnv.AppComposition.Current.GetExport(); - var dockWorkspace = AppEnv.AppComposition.Current.GetExport(); - ILSpy.Commands.SaveCodeHelper.SaveNodeAsync(node, languageService, dockWorkspace).HandleExceptions(); - } - catch - { - // Best-effort: no save path available (minimal host). - } + // Best-effort: no save path available without a composition host (minimal/design-time host). + var languageService = AppEnv.AppComposition.TryGetExport(); + var dockWorkspace = AppEnv.AppComposition.TryGetExport(); + if (languageService is null || dockWorkspace is null) + return; + ILSpy.Commands.SaveCodeHelper.SaveNodeAsync(node, languageService, dockWorkspace).HandleExceptions(); } // Fire-and-forget wrapper around DecompileAsync that observes the resulting Task. @@ -653,35 +642,23 @@ namespace ILSpy.TextView // Pulls the live DecompilerSettings via MEF and returns a clone for this run. Also // bakes the active LanguageService.CurrentVersion into the clone — without this the // toolbar's Language-Version dropdown writes-through to LanguageSettings but never - // reaches the decompiler. MEF resolves are wrapped in try/catch so design-time / - // minimal test hosts that bypass composition fall back to default settings rather - // than throwing. + // reaches the decompiler. Resolves go through TryGetExport so design-time / minimal test + // hosts that bypass composition fall back to default settings rather than throwing. static ICSharpCode.Decompiler.DecompilerSettings? TryGetLiveDecompilerSettings() { - try - { - var settingsService = AppEnv.AppComposition.Current.GetExport(); - var settings = settingsService.DecompilerSettings.Clone(); - ApplyDisplaySettings(settings, settingsService.DisplaySettings); - try - { - var version = AppEnv.AppComposition.Current.GetExport().CurrentVersion; - if (Enum.TryParse(version?.Version, out var languageVersion)) - settings.SetLanguageVersion(languageVersion); - else - settings.SetLanguageVersion(ICSharpCode.Decompiler.CSharp.LanguageVersion.Latest); - } - catch - { - // No LanguageService available (non-C# language or minimal host) — leave the - // settings at whatever default the source DecompilerSettings carried. - } - return settings; - } - catch - { + var settingsService = AppEnv.AppComposition.TryGetExport(); + if (settingsService is null) return null; - } + var settings = settingsService.DecompilerSettings.Clone(); + ApplyDisplaySettings(settings, settingsService.DisplaySettings); + // No LanguageService (non-C# language or minimal host) leaves version null, so the + // language version falls back to Latest below. + var version = AppEnv.AppComposition.TryGetExport()?.CurrentVersion; + if (Enum.TryParse(version?.Version, out var languageVersion)) + settings.SetLanguageVersion(languageVersion); + else + settings.SetLanguageVersion(ICSharpCode.Decompiler.CSharp.LanguageVersion.Latest); + return settings; } /// diff --git a/ILSpy/Themes/DocumentSwitcherDropdownBehavior.cs b/ILSpy/Themes/DocumentSwitcherDropdownBehavior.cs index 4b54a457c..237b1be90 100644 --- a/ILSpy/Themes/DocumentSwitcherDropdownBehavior.cs +++ b/ILSpy/Themes/DocumentSwitcherDropdownBehavior.cs @@ -69,7 +69,7 @@ namespace ILSpy.Themes if (e.NewValue is not true) return; - var settings = TryGetSessionSettings(); + var settings = AppComposition.TryGetExport()?.SessionSettings; if (settings is not null) { System.ComponentModel.PropertyChangedEventHandler handler = (_, args) => { @@ -138,7 +138,8 @@ namespace ILSpy.Themes DockPanel.SetDock(button, Avalonia.Controls.Dock.Right); dockPanel.Children.Insert(0, button); - UpdateVisibility(strip, TryGetSessionSettings()?.MultiLineDocumentTabs ?? false); + UpdateVisibility(strip, + AppComposition.TryGetExport()?.SessionSettings?.MultiLineDocumentTabs ?? false); } static void PopulateMenu(DocumentTabStrip strip, ContextMenu menu) @@ -166,14 +167,5 @@ namespace ILSpy.Themes static Button? FindButton(DocumentTabStrip strip) => strip.GetVisualDescendants().OfType