From ea2fc92883e63161f2aa21ac9d3aa121d7ffbec9 Mon Sep 17 00:00:00 2001 From: tom-englert Date: Sun, 11 Aug 2024 17:05:02 +0200 Subject: [PATCH] Add abstraction layer for MEF --- Directory.Packages.props | 6 +- .../ICSharpCode.Decompiler.ruleset | 4 + ILSpy.sln | 6 ++ ILSpy/App.xaml.cs | 18 +++- ILSpy/ContextMenuEntry.cs | 12 +-- ILSpy/ExportProviderAdapter.cs | 88 +++++++++++++++++++ ILSpy/ILSpy.csproj | 2 + ILSpy/Languages/Languages.cs | 4 +- ILSpy/MainWindow.xaml.cs | 4 +- ILSpy/Options/OptionsDialog.xaml.cs | 10 +-- 10 files changed, 135 insertions(+), 19 deletions(-) create mode 100644 ILSpy/ExportProviderAdapter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 41b329b4af..1b73cb5e7c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,7 +45,9 @@ - - + + + + \ No newline at end of file diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.ruleset b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.ruleset index 66b1288eb4..61afdc893e 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.ruleset +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.ruleset @@ -79,4 +79,8 @@ + + + + \ No newline at end of file diff --git a/ILSpy.sln b/ILSpy.sln index 5d4e13a931..ce506f4d2c 100644 --- a/ILSpy.sln +++ b/ILSpy.sln @@ -36,6 +36,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ICSharpCode.ILSpyX", "ICSha EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ICSharpCode.BamlDecompiler", "ICSharpCode.BamlDecompiler\ICSharpCode.BamlDecompiler.csproj", "{81A30182-3378-4952-8880-F44822390040}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D0858E90-DCD5-4FD9-AA53-7262FAB8BEDB}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Packages.props = Directory.Packages.props + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/ILSpy/App.xaml.cs b/ILSpy/App.xaml.cs index 77722c3ae8..fc74c786d3 100644 --- a/ILSpy/App.xaml.cs +++ b/ILSpy/App.xaml.cs @@ -42,6 +42,10 @@ using TomsToolbox.Wpf.Styles; using ICSharpCode.ILSpyX.TreeView; +using TomsToolbox.Composition; +using TomsToolbox.Wpf.Composition; +using System.ComponentModel.Composition.Hosting; + namespace ICSharpCode.ILSpy { /// @@ -52,8 +56,7 @@ public partial class App : Application internal static CommandLineArguments CommandLineArguments; internal static readonly IList StartupExceptions = new List(); - public static ExportProvider ExportProvider { get; private set; } - public static IExportProviderFactory ExportProviderFactory { get; private set; } + public static IExportProvider ExportProvider { get; private set; } internal class ExceptionData { @@ -89,6 +92,12 @@ public App() } TaskScheduler.UnobservedTaskException += DotNet40_UnobservedTaskException; InitializeMef().GetAwaiter().GetResult(); + + // Register the export provider so that it can be accessed from WPF/XAML components. + ExportProviderLocator.Register(ExportProvider); + // Add data templates registered via MEF. + Resources.MergedDictionaries.Add(DataTemplateManager.CreateDynamicDataTemplates(ExportProvider)); + Languages.Initialize(ExportProvider); EventManager.RegisterClassHandler(typeof(Window), Hyperlink.RequestNavigateEvent, @@ -170,8 +179,9 @@ private static async Task InitializeMef() // If/When any part needs to import ICompositionService, this will be needed: // catalog.WithCompositionService(); var config = CompositionConfiguration.Create(catalog); - ExportProviderFactory = config.CreateExportProviderFactory(); - ExportProvider = ExportProviderFactory.CreateExportProvider(); + var exportProviderFactory = config.CreateExportProviderFactory(); + ExportProvider = new ExportProviderAdapter(exportProviderFactory.CreateExportProvider()); + // This throws exceptions for composition failures. Alternatively, the configuration's CompositionErrors property // could be used to log the errors directly. Used at the end so that it does not prevent the export provider setup. config.ThrowOnErrors(); diff --git a/ILSpy/ContextMenuEntry.cs b/ILSpy/ContextMenuEntry.cs index 9ce4416a2a..fcbdb1bae9 100644 --- a/ILSpy/ContextMenuEntry.cs +++ b/ILSpy/ContextMenuEntry.cs @@ -31,6 +31,8 @@ using ICSharpCode.ILSpy.Controls.TreeView; using ICSharpCode.ILSpyX.TreeView; +using TomsToolbox.Composition; + namespace ICSharpCode.ILSpy { public interface IContextMenuEntry @@ -205,7 +207,7 @@ public static void Add(DataGrid dataGrid) readonly DecompilerTextView textView; readonly ListBox listBox; readonly DataGrid dataGrid; - readonly Lazy[] entries; + readonly IExport[] entries; private ContextMenuProvider() { @@ -288,8 +290,8 @@ void dataGrid_ContextMenuOpening(object sender, ContextMenuEventArgs e) bool ShowContextMenu(TextViewContext context, out ContextMenu menu) { menu = new ContextMenu(); - var menuGroups = new Dictionary[]>(); - Lazy[] topLevelGroup = null; + var menuGroups = new Dictionary[]>(); + IExport[] topLevelGroup = null; foreach (var group in entries.OrderBy(c => c.Metadata.Order).GroupBy(c => c.Metadata.ParentMenuID)) { if (group.Key == null) @@ -301,10 +303,10 @@ bool ShowContextMenu(TextViewContext context, out ContextMenu menu) menuGroups.Add(group.Key, group.ToArray()); } } - BuildMenu(topLevelGroup ?? Array.Empty>(), menu.Items); + BuildMenu(topLevelGroup ?? Array.Empty>(), menu.Items); return menu.Items.Count > 0; - void BuildMenu(Lazy[] menuGroup, ItemCollection parent) + void BuildMenu(IExport[] menuGroup, ItemCollection parent) { foreach (var category in menuGroup.GroupBy(c => c.Metadata.Category)) { diff --git a/ILSpy/ExportProviderAdapter.cs b/ILSpy/ExportProviderAdapter.cs new file mode 100644 index 0000000000..555352fc46 --- /dev/null +++ b/ILSpy/ExportProviderAdapter.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using Microsoft.VisualStudio.Composition; + +using TomsToolbox.Composition; +using TomsToolbox.Essentials; + +namespace ICSharpCode.ILSpy; + +#nullable enable + +/// +/// Adapter for Microsoft.VisualStudio.Composition. to . +/// +public class ExportProviderAdapter : IExportProvider +{ + private static readonly Type DefaultMetadataType = typeof(Dictionary); + + private readonly ExportProvider _exportProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The export provider. + public ExportProviderAdapter(ExportProvider exportProvider) + { + _exportProvider = exportProvider; + } + + event EventHandler? IExportProvider.ExportsChanged { add { } remove { } } + + T IExportProvider.GetExportedValue(string? contractName) where T : class + { + return _exportProvider.GetExportedValue(contractName) ?? throw new InvalidOperationException($"No export found for type {typeof(T).FullName} with contract '{contractName}'"); + } + + T? IExportProvider.GetExportedValueOrDefault(string? contractName) where T : class + { + return _exportProvider.GetExportedValues(contractName).SingleOrDefault(); + } + +#pragma warning disable CS8769 // Nullability of reference types in type of parameter doesn't match implemented member (possibly because of nullability attributes). + // can't apply NotNullWhen here, because ICSharpCode.Decompiler defines a duplicate attribute, and uses InternalsVisibleTo("ILSpy"), so this attribute is now ambiguous! + bool IExportProvider.TryGetExportedValue(string? contractName, /*[NotNullWhen(true)]*/ out T? value) where T : class +#pragma warning restore CS8769 // Nullability of reference types in type of parameter doesn't match implemented member (possibly because of nullability attributes). + { + value = _exportProvider.GetExportedValues(contractName).SingleOrDefault(); + + return !Equals(value, default(T)); + } + + IEnumerable IExportProvider.GetExportedValues(string? contractName) where T : class + { + return _exportProvider.GetExportedValues(contractName); + } + + IEnumerable IExportProvider.GetExportedValues(Type contractType, string? contractName) + { + return _exportProvider + .GetExports(contractType, DefaultMetadataType, contractName) + .Select(item => item.Value) + .ExceptNullItems(); + } + + IEnumerable> IExportProvider.GetExports(Type contractType, string? contractName) + { + return _exportProvider + .GetExports(contractType, DefaultMetadataType, contractName) + .Select(item => new ExportAdapter(() => item.Value, new MetadataAdapter((IDictionary)item.Metadata))); + } + + IEnumerable> IExportProvider.GetExports(string? contractName) where T : class + { + return _exportProvider + .GetExports(typeof(T), DefaultMetadataType, contractName) + .Select(item => new ExportAdapter(() => (T?)item.Value, new MetadataAdapter((IDictionary)item.Metadata))); + } + + IEnumerable> IExportProvider.GetExports(string? contractName) where T : class where TMetadataView : class + { + return _exportProvider + .GetExports(contractName) + .Select(item => new ExportAdapter(() => item.Value, item.Metadata)); + } +} \ No newline at end of file diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 7b83970f6f..5bbadbe1bf 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -50,6 +50,8 @@ + + diff --git a/ILSpy/Languages/Languages.cs b/ILSpy/Languages/Languages.cs index 5721f0e728..cbce690b8b 100644 --- a/ILSpy/Languages/Languages.cs +++ b/ILSpy/Languages/Languages.cs @@ -22,6 +22,8 @@ using Microsoft.VisualStudio.Composition; +using TomsToolbox.Composition; + namespace ICSharpCode.ILSpy { public static class Languages @@ -39,7 +41,7 @@ public static ReadOnlyCollection AllLanguages { get { return allLanguages; } } - internal static void Initialize(ExportProvider ep) + internal static void Initialize(IExportProvider ep) { List languages = new List(); languages.AddRange(ep.GetExportedValues()); diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index e154a67d39..2f8ec46f7f 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -62,6 +62,8 @@ using Microsoft.Win32; using ICSharpCode.ILSpyX.TreeView; +using TomsToolbox.Composition; + namespace ICSharpCode.ILSpy { /// @@ -245,7 +247,7 @@ void InitToolbar() } - Button MakeToolbarItem(Lazy command) + Button MakeToolbarItem(IExport command) { return new Button { Style = ThemeManager.Current.CreateToolBarButtonStyle(), diff --git a/ILSpy/Options/OptionsDialog.xaml.cs b/ILSpy/Options/OptionsDialog.xaml.cs index e93f08b287..ab15a93d57 100644 --- a/ILSpy/Options/OptionsDialog.xaml.cs +++ b/ILSpy/Options/OptionsDialog.xaml.cs @@ -28,6 +28,8 @@ using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpyX.Settings; +using TomsToolbox.Composition; + namespace ICSharpCode.ILSpy.Options { public class TabItemViewModel @@ -47,16 +49,12 @@ public TabItemViewModel(string header, UIElement content) /// public partial class OptionsDialog : Window { - - readonly Lazy[] optionPages; + readonly IExport[] optionPages; public OptionsDialog() { InitializeComponent(); - // These used to have [ImportMany(..., RequiredCreationPolicy = CreationPolicy.NonShared)], so they use their own - // ExportProvider instance. - // FIXME: Ideally, the export provider should be disposed when it's no longer needed. - var ep = App.ExportProviderFactory.CreateExportProvider(); + var ep = App.ExportProvider; this.optionPages = ep.GetExports("OptionPages").ToArray(); ILSpySettings settings = ILSpySettings.Load(); foreach (var optionPage in optionPages.OrderBy(p => p.Metadata.Order))