From 3df99f31befb6955465f5fe9cb6b3c36d8db820b Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 13 May 2026 19:13:44 +0200 Subject: [PATCH] Stream search results through ILSpyX strategies Wire the search pane to the shared ICSharpCode.ILSpyX.Search engine. Setting SearchTerm or SelectedSearchMode now cancels any in-flight run and starts a fresh one; cleared term tears the run down. Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../MemberImplementsInterfaceAnalyzerTests.cs | 9 +- .../Search/SearchPaneStreamingTests.cs | 126 ++++++++++ ILSpy/Search/AvaloniaSearchResultFactory.cs | 136 +++++++++++ ILSpy/Search/RunningSearch.cs | 222 ++++++++++++++++++ ILSpy/Search/SearchPaneModel.cs | 81 +++++++ 5 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 ILSpy.Tests/Search/SearchPaneStreamingTests.cs create mode 100644 ILSpy/Search/AvaloniaSearchResultFactory.cs create mode 100644 ILSpy/Search/RunningSearch.cs diff --git a/ILSpy.Tests/Analyzers/Library/MemberImplementsInterfaceAnalyzerTests.cs b/ILSpy.Tests/Analyzers/Library/MemberImplementsInterfaceAnalyzerTests.cs index 1deba698b..16442d935 100644 --- a/ILSpy.Tests/Analyzers/Library/MemberImplementsInterfaceAnalyzerTests.cs +++ b/ILSpy.Tests/Analyzers/Library/MemberImplementsInterfaceAnalyzerTests.cs @@ -62,7 +62,10 @@ public class MemberImplementsInterfaceAnalyzerTests public void VerifyDoesNotShowForNoSymbol() { var analyzer = new MemberImplementsInterfaceAnalyzer(); - var shouldShow = analyzer.Show(symbol: null); + // IAnalyzer.Show declares its parameter non-nullable; the contract this test + // pins is "the implementation guards against null callers even though the + // signature says it can't happen". null-forgiving silences the CS8625. + var shouldShow = analyzer.Show(symbol: null!); Assert.That(!shouldShow, "The analyzer will be unexpectedly shown for no symbol"); } @@ -141,6 +144,10 @@ public class MemberImplementsInterfaceAnalyzerTests { var memberMock = Substitute.For(); memberMock.SymbolKind.Returns(symbolKind); + // NSubstitute's fluent path setup: Returns reads memberMock.DeclaringTypeDefinition + // as if it were a real reference, building the expectation in place. The compiler + // flags it as a possibly-null deref; the suppression is required for the mock to + // configure the chained property. memberMock.DeclaringTypeDefinition!.Kind.Returns(typeKind); memberMock.IsStatic.Returns(isStatic); return memberMock; diff --git a/ILSpy.Tests/Search/SearchPaneStreamingTests.cs b/ILSpy.Tests/Search/SearchPaneStreamingTests.cs new file mode 100644 index 000000000..0e9196479 --- /dev/null +++ b/ILSpy.Tests/Search/SearchPaneStreamingTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ICSharpCode.ILSpyX.Search; + +using ILSpy.AppEnv; +using ILSpy.Search; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Search; + +[TestFixture] +public class SearchPaneStreamingTests +{ + [AvaloniaTest] + public async Task Diagnostic_Direct_Strategy_Invocation_Surfaces_Type_Results() + { + // Diagnostic: drive a MemberSearchStrategy directly to confirm the shared search + // logic works for the fixture. If this fails, the bug is in the strategy / fixture; + // if it passes but the orchestrator test below fails, the bug is in our wiring. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + var module = await assemblyNode.LoadedAssembly.GetMetadataFileAsync(); + + var queue = new System.Collections.Concurrent.ConcurrentQueue(); + var language = AppComposition.Current.GetExport().CurrentLanguage; + var request = new ICSharpCode.ILSpyX.Search.SearchRequest { + Mode = SearchMode.Type, + Keywords = new[] { "Enumerable" }, + SearchResultFactory = new global::ILSpy.Search.AvaloniaSearchResultFactory(language), + DecompilerSettings = new ICSharpCode.Decompiler.DecompilerSettings(), + FullNameSearch = false, + OmitGenerics = false, + InNamespace = null!, + InAssembly = null!, + }; + var strategy = new ICSharpCode.ILSpyX.Search.MemberSearchStrategy( + language, ICSharpCode.ILSpyX.ApiVisibility.All, request, queue, + ICSharpCode.ILSpyX.Search.MemberSearchKind.Type); + + strategy.Search(module, default); + queue.Count.Should().BeGreaterThan(0, + "if the strategy can't find Enumerable in the fixture, the orchestrator certainly won't either"); + } + + [AvaloniaTest] + public async Task Typing_A_Term_Surfaces_Matching_Type_Results_From_The_Loaded_AssemblyList() + { + // End-to-end: set SearchTerm to "Enumerable", let the orchestrator run a TypeAndMember + // search across the loaded fixture assemblies, and wait for at least one matching + // result to land in the Results collection. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var search = AppComposition.Current.GetExport(); + search.Results.Clear(); + search.SelectedSearchMode = search.SearchModes.First(m => m.Mode == SearchMode.Type); + search.SearchTerm = "Enumerable"; + + await Waiters.WaitForAsync( + () => search.Results.Any(r => r.Name.Contains("Enumerable", StringComparison.OrdinalIgnoreCase)), + timeout: TimeSpan.FromSeconds(30)); + + search.Results.Should().NotBeEmpty( + "the Type search strategy must have surfaced at least one match within the timeout"); + search.Results.Should().Contain(r => r.Name.Contains("Enumerable", StringComparison.OrdinalIgnoreCase)); + } + + [AvaloniaTest] + public async Task Clearing_The_Search_Term_Empties_The_Result_List() + { + // Switching to an empty term tears down the running search and clears the visible + // results. The user shouldn't see stale rows after they've explicitly cleared the box. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var search = AppComposition.Current.GetExport(); + search.Results.Clear(); + search.SelectedSearchMode = search.SearchModes.First(m => m.Mode == SearchMode.Type); + search.SearchTerm = "Enumerable"; + + await Waiters.WaitForAsync(() => search.Results.Count > 0, timeout: TimeSpan.FromSeconds(30)); + + search.SearchTerm = string.Empty; + await Waiters.WaitForAsync(() => search.Results.Count == 0, timeout: TimeSpan.FromSeconds(5)); + search.Results.Should().BeEmpty("an empty search term must clear stale results"); + } +} diff --git a/ILSpy/Search/AvaloniaSearchResultFactory.cs b/ILSpy/Search/AvaloniaSearchResultFactory.cs new file mode 100644 index 000000000..b2913f069 --- /dev/null +++ b/ILSpy/Search/AvaloniaSearchResultFactory.cs @@ -0,0 +1,136 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Output; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX.Abstractions; +using ICSharpCode.ILSpyX.Search; + +using ILSpy.Languages; + +namespace ILSpy.Search +{ + /// + /// Builds the objects the search strategies stream into the + /// pane's result queue. Mirrors WPF's SearchResultFactory: fitness ranking + /// privileges short names (shorter == higher fitness == higher rank); compiler-generated + /// names (those starting with <) get fitness 0 so they sink to the bottom. + /// + internal sealed class AvaloniaSearchResultFactory : ISearchResultFactory + { + readonly Language language; + + public AvaloniaSearchResultFactory(Language language) + { + this.language = language; + } + + public MemberSearchResult Create(IEntity entity) + { + var declaringType = entity.DeclaringTypeDefinition; + return new MemberSearchResult { + Member = entity, + Fitness = CalculateFitness(entity), + Name = GetLanguageSpecificName(entity), + Location = declaringType != null + ? language.TypeToString(declaringType, ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.UseFullyQualifiedTypeNames) + : entity.Namespace ?? string.Empty, + Assembly = entity.ParentModule?.FullAssemblyName ?? string.Empty, + ToolTip = entity.ParentModule?.MetadataFile?.FileName, + Image = GetIcon(entity), + LocationImage = declaringType != null ? Images.Images.Class : Images.Images.Namespace, + AssemblyImage = Images.Images.Assembly, + }; + } + + public ResourceSearchResult Create(MetadataFile module, Resource resource, ITreeNode node, ITreeNode parent) + { + return new ResourceSearchResult { + Resource = resource, + Fitness = 1.0f / Math.Max(1, resource.Name.Length), + Image = Images.Images.Library, + Name = resource.Name, + LocationImage = Images.Images.Library, + Location = (parent.Text as string) ?? string.Empty, + Assembly = module.FullName, + ToolTip = module.FileName, + AssemblyImage = Images.Images.Assembly, + }; + } + + public AssemblySearchResult Create(MetadataFile module) + { + return new AssemblySearchResult { + Module = module, + Fitness = 1.0f / Math.Max(1, module.Name.Length), + Name = module.Name, + Location = module.FileName, + Assembly = module.FullName, + ToolTip = module.FileName, + Image = Images.Images.Assembly, + LocationImage = Images.Images.Library, + AssemblyImage = Images.Images.Assembly, + }; + } + + public NamespaceSearchResult Create(MetadataFile module, INamespace ns) + { + var name = ns.FullName.Length == 0 ? "-" : ns.FullName; + return new NamespaceSearchResult { + Namespace = ns, + Name = name, + Fitness = 1.0f / Math.Max(1, name.Length), + Location = module.Name, + Assembly = module.FullName, + Image = Images.Images.Namespace, + LocationImage = Images.Images.Assembly, + AssemblyImage = Images.Images.Assembly, + }; + } + + static float CalculateFitness(IEntity member) + { + var text = member.Name; + if (text.StartsWith('<')) + return 0; + if (member.SymbolKind is SymbolKind.Constructor or SymbolKind.Destructor) + text = member.DeclaringType?.Name ?? text; + text = ReflectionHelper.SplitTypeParameterCountFromReflectionName(text); + return 1.0f / Math.Max(1, text.Length); + } + + string GetLanguageSpecificName(IEntity member) => member switch { + ITypeDefinition t => language.TypeToString(t, ConversionFlags.None), + IField or IProperty or IMethod or IEvent => language.EntityToString(member, ConversionFlags.ShowDeclaringType), + _ => member.Name, + }; + + static object GetIcon(IEntity member) => member switch { + ITypeDefinition => Images.Images.Class, + IField => Images.Images.Field, + IProperty => Images.Images.Property, + IMethod => Images.Images.Method, + IEvent => Images.Images.Event, + _ => Images.Images.Library, + }; + } +} diff --git a/ILSpy/Search/RunningSearch.cs b/ILSpy/Search/RunningSearch.cs new file mode 100644 index 000000000..c2a1a7bd0 --- /dev/null +++ b/ILSpy/Search/RunningSearch.cs @@ -0,0 +1,222 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Avalonia.Threading; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.Search; + +using ILSpy.Languages; + +namespace ILSpy.Search +{ + /// + /// Orchestrates one search across the loaded . Builds a + /// , picks the matching , + /// runs it on a background task, and streams each emitted + /// into the supplied UI-thread via + /// . + /// + internal sealed class RunningSearch + { + const int MaxResults = 1000; + + readonly IReadOnlyList assemblies; + readonly SearchMode mode; + readonly string searchTerm; + readonly Language language; + readonly ApiVisibility apiVisibility; + readonly ISearchResultFactory resultFactory; + readonly ObservableCollection sink; + readonly ConcurrentQueue queue = new(); + readonly CancellationTokenSource cts = new(); + Task? runTask; + + public RunningSearch( + IReadOnlyList assemblies, + string searchTerm, + SearchMode mode, + Language language, + ApiVisibility apiVisibility, + ISearchResultFactory resultFactory, + ObservableCollection sink) + { + this.assemblies = assemblies; + this.searchTerm = searchTerm; + this.mode = mode; + this.language = language; + this.apiVisibility = apiVisibility; + this.resultFactory = resultFactory; + this.sink = sink; + } + + public bool IsCompleted => runTask is { IsCompleted: true }; + + public void Start() + { + var token = cts.Token; + // Task.Run with an async lambda wraps the inner Task so runTask only completes + // after RunSearchAsync actually finishes — without the wrapping, runTask would + // complete the moment Task.Run returned the inner Task to the caller, and the + // drain loop would exit before any results landed. + runTask = Task.Run(async () => await RunSearchAsync(token).ConfigureAwait(false), token); + _ = DrainQueueAsync(token); + } + + public void Cancel() + { + cts.Cancel(); + } + + async Task RunSearchAsync(CancellationToken ct) + { + try + { + var request = BuildRequest(); + var strategy = GetStrategy(request); + if (strategy == null) + return; + foreach (var assembly in assemblies) + { + ct.ThrowIfCancellationRequested(); + // Force the load so search hits the metadata even for assemblies the user + // hasn't expanded in the tree yet. Failed loads are already surfaced in + // the assembly tree with the AssemblyWarning icon — don't double-report + // here, just skip so the search keeps streaming results from the + // healthy assemblies. + MetadataFile? module; + try + { + module = await assembly.GetMetadataFileAsync().ConfigureAwait(false); + } + catch (Exception ex) when (IsExpectedLoadFailure(ex)) + { + continue; + } + if (module == null) + continue; + strategy.Search(module, ct); + } + } + catch (OperationCanceledException) + { + // Expected on user-driven term/mode change. + } + } + + static bool IsExpectedLoadFailure(Exception ex) => ex is + BadImageFormatException + or IOException + or InvalidOperationException + or UnauthorizedAccessException + or NotSupportedException + or ReflectionTypeLoadException; + + SearchRequest BuildRequest() + { + // Minimal keyword parser: split on whitespace. The WPF pane supports a richer + // prefix DSL (inassembly:, t:, /regex/, =exact, ~fuzzy, …) — that lands as a + // follow-up. Plain-keyword search covers the common case end-to-end. + var keywords = (searchTerm ?? string.Empty) + .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return new SearchRequest { + Mode = mode, + Keywords = keywords, + SearchResultFactory = resultFactory, + // MemberSearchStrategy.Search resolves a type system via + // module.GetTypeSystemWithDecompilerSettingsOrNull(request.DecompilerSettings); + // passing null short-circuits to zero results. Default settings are fine for + // search — we only need the type system to materialise, not specific + // decompiler behaviour. + DecompilerSettings = new DecompilerSettings(), + FullNameSearch = false, + OmitGenerics = false, + // IsInNamespaceOrAssembly treats null as "no filter, accept everything"; + // the EMPTY STRING would restrict to the global namespace (Namespace.Length + // == 0), which matches almost nothing. The DSL prefixes that flip these on + // (innamespace:, inassembly:) are out of scope for the minimal parser. + InNamespace = null!, + InAssembly = null!, + }; + } + + AbstractSearchStrategy? GetStrategy(SearchRequest request) + { + if (request.Keywords.Length == 0 && request.RegEx is null) + return null; + return mode switch { + SearchMode.TypeAndMember => new MemberSearchStrategy(language, apiVisibility, request, queue), + SearchMode.Type => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Type), + SearchMode.Member => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Member), + SearchMode.Method => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Method), + SearchMode.Field => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Field), + SearchMode.Property => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Property), + SearchMode.Event => new MemberSearchStrategy(language, apiVisibility, request, queue, MemberSearchKind.Event), + SearchMode.Literal => new LiteralSearchStrategy(language, apiVisibility, request, queue), + SearchMode.Token => new MetadataTokenSearchStrategy(language, apiVisibility, request, queue), + SearchMode.Assembly => new AssemblySearchStrategy(request, queue, AssemblySearchKind.NameOrFileName), + SearchMode.Namespace => new NamespaceSearchStrategy(request, queue), + // Resource search needs ITreeNodeFactory infrastructure — deferred to a follow-up. + _ => null, + }; + } + + async Task DrainQueueAsync(CancellationToken ct) + { + try + { + while (!ct.IsCancellationRequested) + { + var batch = new List(); + while (queue.TryDequeue(out var result) && batch.Count < 100) + batch.Add(result); + if (batch.Count > 0) + { + Dispatcher.UIThread.Post(() => { + foreach (var r in batch) + { + if (sink.Count >= MaxResults) + break; + sink.Add(r); + } + }); + } + if (runTask is { IsCompleted: true } && queue.IsEmpty) + return; + await Task.Delay(50, ct).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // Expected when the run is replaced or cleared. + } + } + } +} diff --git a/ILSpy/Search/SearchPaneModel.cs b/ILSpy/Search/SearchPaneModel.cs index fea64c38d..acd78cbb8 100644 --- a/ILSpy/Search/SearchPaneModel.cs +++ b/ILSpy/Search/SearchPaneModel.cs @@ -21,9 +21,13 @@ using System.Composition; using CommunityToolkit.Mvvm.ComponentModel; +using ICSharpCode.ILSpyX; using ICSharpCode.ILSpyX.Search; +using ILSpy.AppEnv; +using ILSpy.AssemblyTree; using ILSpy.Commands; +using ILSpy.Languages; using ILSpy.ViewModels; namespace ILSpy.Search @@ -51,6 +55,13 @@ namespace ILSpy.Search Id = PaneContentId; Title = "Search"; SelectedSearchMode = SearchModes[0]; + PropertyChanged += OnPropertyChangedDispatch; + } + + void OnPropertyChangedDispatch(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(SearchTerm) or nameof(SelectedSearchMode)) + RestartSearch(); } /// @@ -94,5 +105,75 @@ namespace ILSpy.Search /// Dispatcher.UIThread.Post as the strategies emit them. /// public ObservableCollection Results { get; } = new(); + + RunningSearch? currentSearch; + + void RestartSearch() + { + currentSearch?.Cancel(); + currentSearch = null; + Results.Clear(); + + var term = SearchTerm; + if (string.IsNullOrWhiteSpace(term)) + return; + + var assemblyTreeModel = TryGetAssemblyTreeModel(); + var assemblyList = assemblyTreeModel?.AssemblyList; + if (assemblyList == null) + return; + var language = TryGetLanguage(); + if (language == null) + return; + var apiVisibility = TryGetSettings()?.SessionSettings?.LanguageSettings?.ShowApiLevel + ?? ApiVisibility.PublicOnly; + + var factory = new AvaloniaSearchResultFactory(language); + currentSearch = new RunningSearch( + assemblyList.GetAssemblies(), + term, + SelectedSearchMode.Mode, + language, + apiVisibility, + factory, + Results); + currentSearch.Start(); + } + + static AssemblyTreeModel? TryGetAssemblyTreeModel() + { + try + { + return AppComposition.Current.GetExport(); + } + catch + { + return null; + } + } + + static Language? TryGetLanguage() + { + try + { + return AppComposition.Current.GetExport().CurrentLanguage; + } + catch + { + return null; + } + } + + static SettingsService? TryGetSettings() + { + try + { + return AppComposition.Current.GetExport(); + } + catch + { + return null; + } + } } }