Browse Source

Migrate DI from Microsoft.VisualStudio.Composition to Microsoft.Extensions.DependencyInjection

pull/3308/head
tom-englert 9 months ago
parent
commit
66544e6208
  1. 3
      Directory.Packages.props
  2. 106
      ILSpy/App.xaml.cs
  3. 44
      ILSpy/AssemblyTree/AssemblyTreeModel.cs
  4. 103
      ILSpy/ExportProviderAdapter.cs
  5. 44
      ILSpy/ExtensionMethods.cs
  6. 3
      ILSpy/ILSpy.csproj

3
Directory.Packages.props

@ -23,12 +23,12 @@
<PackageVersion Include="Microsoft.DiaSymReader.Native" Version="17.0.0-beta1.21524.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageVersion Include="Microsoft.NETCore.ILAsm" Version="8.0.0" />
<PackageVersion Include="Microsoft.NETCore.ILDAsm" Version="8.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.VisualStudio.Composition" Version="17.11.13" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageVersion Include="Mono.Cecil" Version="0.11.6" />
<PackageVersion Include="NaturalSort.Extension" Version="4.3.0" />
@ -45,6 +45,7 @@
<PackageVersion Include="System.Reflection.Metadata" Version="8.0.1" />
<PackageVersion Include="System.Resources.Extensions" Version="8.0.0" />
<PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageVersion Include="TomsToolbox.Composition.MicrosoftExtensions" Version="2.20.0" />
<PackageVersion Include="TomsToolbox.Wpf.Composition" Version="2.20.0" />
<PackageVersion Include="TomsToolbox.Wpf.Composition.Mef" Version="2.20.0" />
<PackageVersion Include="TomsToolbox.Wpf.Styles" Version="2.20.0" />

106
ILSpy/App.xaml.cs

@ -23,7 +23,6 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
@ -36,8 +35,6 @@ using ICSharpCode.ILSpyX.Analyzers;
using Medo.Application;
using Microsoft.VisualStudio.Composition;
using TomsToolbox.Wpf.Styles;
using ICSharpCode.ILSpyX.TreeView;
@ -47,6 +44,11 @@ using ICSharpCode.ILSpy.Themes;
using System.Globalization;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using TomsToolbox.Composition.MicrosoftExtensions;
using TomsToolbox.Essentials;
namespace ICSharpCode.ILSpy
{
/// <summary>
@ -59,10 +61,9 @@ namespace ICSharpCode.ILSpy
public static IExportProvider ExportProvider { get; private set; }
internal class ExceptionData
internal record ExceptionData(Exception Exception)
{
public Exception Exception;
public string PluginName;
public string PluginName { get; init; }
}
public App()
@ -80,6 +81,14 @@ namespace ICSharpCode.ILSpy
InitializeComponent();
if (!InitializeDependencyInjection(SettingsService.Instance))
{
// There is something completely wrong with DI, probably some service registration is missing => nothing we can do to recover, so stop and shut down.
Exit += (_, _) => MessageBox.Show(StartupExceptions.FormatExceptions(), "Sorry we crashed!");
Shutdown(1);
return;
}
if (!Debugger.IsAttached)
{
AppDomain.CurrentDomain.UnhandledException += ShowErrorBox;
@ -92,8 +101,6 @@ namespace ICSharpCode.ILSpy
Resources.RegisterDefaultStyles();
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.
@ -103,7 +110,7 @@ namespace ICSharpCode.ILSpy
ThemeManager.Current.Theme = sessionSettings.Theme;
if (!string.IsNullOrEmpty(sessionSettings.CurrentCulture))
{
Thread.CurrentThread.CurrentUICulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(sessionSettings.CurrentCulture);
Thread.CurrentThread.CurrentUICulture = CultureInfo.DefaultThreadCurrentUICulture = new(sessionSettings.CurrentCulture);
}
EventManager.RegisterClassHandler(typeof(Window),
@ -125,6 +132,13 @@ namespace ICSharpCode.ILSpy
SettingsService.Instance.AssemblyListManager.CreateDefaultAssemblyLists();
}
public new static App Current => (App)Application.Current;
public new MainWindow MainWindow {
get => (MainWindow)base.MainWindow;
set => base.MainWindow = value;
}
private static void SingleInstance_NewInstanceDetected(object sender, NewInstanceEventArgs e) => ExportProvider.GetExportedValue<AssemblyTreeModel>().HandleSingleInstanceCommandLineArguments(e.Args).HandleExceptions();
static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyName assemblyName)
@ -136,22 +150,17 @@ namespace ICSharpCode.ILSpy
return context.LoadFromAssemblyPath(assemblyFileName);
}
private static async Task InitializeMef()
private static bool InitializeDependencyInjection(SettingsService settingsService)
{
// Add custom logic for resolution of dependencies.
// This necessary because the AssemblyLoadContext.LoadFromAssemblyPath and related methods,
// do not automatically load dependencies.
AssemblyLoadContext.Default.Resolving += ResolvePluginDependencies;
// Cannot show MessageBox here, because WPF would crash with a XamlParseException
// Remember and show exceptions in text output, once MainWindow is properly initialized
try
{
// Set up VS MEF. For now, only do MEF1 part discovery, since that was in use before.
// To support both MEF1 and MEF2 parts, just change this to:
// var discovery = PartDiscovery.Combine(new AttributedPartDiscoveryV1(Resolver.DefaultInstance),
// new AttributedPartDiscovery(Resolver.DefaultInstance));
var discovery = new AttributedPartDiscoveryV1(Resolver.DefaultInstance);
var catalog = ComposableCatalog.Create(Resolver.DefaultInstance);
var services = new ServiceCollection();
var pluginDir = Path.GetDirectoryName(typeof(App).Module.FullyQualifiedName);
if (pluginDir != null)
{
@ -160,46 +169,39 @@ namespace ICSharpCode.ILSpy
var name = Path.GetFileNameWithoutExtension(plugin);
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(plugin);
var parts = await discovery.CreatePartsAsync(asm);
catalog = catalog.AddParts(parts);
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(plugin);
services.BindExports(assembly);
}
catch (Exception ex)
{
StartupExceptions.Add(new ExceptionData { Exception = ex, PluginName = name });
// Cannot show MessageBox here, because WPF would crash with a XamlParseException
// Remember and show exceptions in text output, once MainWindow is properly initialized
StartupExceptions.Add(new(ex) { PluginName = name });
}
}
}
// Add the built-in parts: First, from ILSpyX
var xParts = await discovery.CreatePartsAsync(typeof(IAnalyzer).Assembly);
catalog = catalog.AddParts(xParts);
services.BindExports(typeof(IAnalyzer).Assembly);
// Then from ILSpy itself
var createdParts = await discovery.CreatePartsAsync(Assembly.GetExecutingAssembly());
catalog = catalog.AddParts(createdParts);
// If/When the project switches to .NET Standard/Core, this will be needed to allow metadata interfaces (as opposed
// to metadata classes). When running on .NET Framework, it's automatic.
// catalog.WithDesktopSupport();
// If/When any part needs to import ICompositionService, this will be needed:
// catalog.WithCompositionService();
var config = CompositionConfiguration.Create(catalog);
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();
}
catch (CompositionFailedException ex) when (ex.InnerException is AggregateException agex)
{
foreach (var inner in agex.InnerExceptions)
{
StartupExceptions.Add(new ExceptionData { Exception = inner });
}
services.BindExports(Assembly.GetExecutingAssembly());
// Add the settings service
services.AddSingleton(settingsService);
var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
ExportProvider = new ExportProviderAdapter(serviceProvider);
return true;
}
catch (Exception ex)
{
StartupExceptions.Add(new ExceptionData { Exception = ex });
if (ex is AggregateException aggregate)
StartupExceptions.AddRange(aggregate.InnerExceptions.Select(item => new ExceptionData(ex)));
else
StartupExceptions.Add(new(ex));
return false;
}
}
@ -207,15 +209,7 @@ namespace ICSharpCode.ILSpy
{
base.OnStartup(e);
var output = new StringBuilder();
if (StartupExceptions.FormatExceptions(output))
{
MessageBox.Show(output.ToString(), "Sorry we crashed!");
Environment.Exit(1);
}
MainWindow = new MainWindow();
MainWindow = new();
MainWindow.Show();
}

44
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -21,18 +21,17 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.IO;
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.Input;
using System.Windows.Navigation;
using System.Windows.Threading;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Documentation;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
@ -160,13 +159,15 @@ namespace ICSharpCode.ILSpy.AssemblyTree
/// 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>
private void HandleCommandLineArgumentsAfterShowList(CommandLineArguments args, ISettingsProvider? spySettings = null)
private async Task 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);
await NavigateOnLaunch(args.NavigateTo, sessionSettings.ActiveTreeViewPath, spySettings, relevantAssemblies);
if (args.Search != null)
{
var searchPane = App.ExportProvider.GetExportedValue<SearchPaneModel>();
@ -180,7 +181,7 @@ namespace ICSharpCode.ILSpy.AssemblyTree
{
var cmdArgs = CommandLineArguments.Create(args);
await Dispatcher.InvokeAsync(() => {
await Dispatcher.InvokeAsync(async () => {
if (!HandleCommandLineArguments(cmdArgs))
return;
@ -192,11 +193,11 @@ namespace ICSharpCode.ILSpy.AssemblyTree
window.WindowState = WindowState.Normal;
}
HandleCommandLineArgumentsAfterShowList(cmdArgs);
await HandleCommandLineArgumentsAfterShowList(cmdArgs);
});
}
private async void NavigateOnLaunch(string? navigateTo, string[]? activeTreeViewPath, ISettingsProvider? spySettings, List<LoadedAssembly> relevantAssemblies)
private async Task NavigateOnLaunch(string? navigateTo, string[]? activeTreeViewPath, ISettingsProvider? spySettings, List<LoadedAssembly> relevantAssemblies)
{
var initialSelection = SelectedItem;
if (navigateTo != null)
@ -386,26 +387,31 @@ namespace ICSharpCode.ILSpy.AssemblyTree
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, OpenAssemblies);
}
private void OpenAssemblies()
private async Task OpenAssemblies()
{
HandleCommandLineArgumentsAfterShowList(App.CommandLineArguments, SettingsService.Instance.SpySettings);
await HandleCommandLineArgumentsAfterShowList(App.CommandLineArguments, SettingsService.Instance.SpySettings);
AvalonEditTextOutput output = new();
if (FormatExceptions(App.StartupExceptions.ToArray(), output))
if (FormatExceptions(App.StartupExceptions.ToArray(), out var output))
{
output.Title = "Startup errors";
DockWorkspace.Instance.AddTabPage();
DockWorkspace.Instance.ShowText(output);
}
}
private static bool FormatExceptions(App.ExceptionData[] exceptions, ITextOutput output)
private static bool FormatExceptions(App.ExceptionData[] exceptions, [NotNullWhen(true)] out AvalonEditTextOutput? output)
{
var stringBuilder = new StringBuilder();
var result = exceptions.FormatExceptions(stringBuilder);
if (result)
{
output.Write(stringBuilder.ToString());
}
return result;
output = null;
var result = exceptions.FormatExceptions();
if (result.IsNullOrEmpty())
return false;
output = new();
output.Write(result);
return true;
}
private void ShowAssemblyList(string name)

103
ILSpy/ExportProviderAdapter.cs

@ -1,103 +0,0 @@
// Copyright (c) 2024 Tom Englert 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.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.VisualStudio.Composition;
using TomsToolbox.Composition;
using TomsToolbox.Essentials;
namespace ICSharpCode.ILSpy;
#nullable enable
/// <summary>
/// Adapter for Microsoft.VisualStudio.Composition.<see cref="ExportProvider"/> to <see cref="IExportProvider"/>.
/// </summary>
public sealed class ExportProviderAdapter : IExportProvider
{
private static readonly Type DefaultMetadataType = typeof(Dictionary<string, object>);
private readonly ExportProvider _exportProvider;
/// <summary>
/// Initializes a new instance of the <see cref="ExportProviderAdapter"/> class.
/// </summary>
/// <param name="exportProvider">The export provider.</param>
public ExportProviderAdapter(ExportProvider exportProvider)
{
_exportProvider = exportProvider;
}
event EventHandler<EventArgs>? IExportProvider.ExportsChanged { add { } remove { } }
T IExportProvider.GetExportedValue<T>(string? contractName) where T : class
{
return _exportProvider.GetExportedValue<T>(contractName) ?? throw new InvalidOperationException($"No export found for type {typeof(T).FullName} with contract '{contractName}'");
}
T? IExportProvider.GetExportedValueOrDefault<T>(string? contractName) where T : class
{
return _exportProvider.GetExportedValues<T>(contractName).SingleOrDefault();
}
bool IExportProvider.TryGetExportedValue<T>(string? contractName, [NotNullWhen(true)] out T? value) where T : class
{
value = _exportProvider.GetExportedValues<T>(contractName).SingleOrDefault();
return !Equals(value, default(T));
}
IEnumerable<T> IExportProvider.GetExportedValues<T>(string? contractName) where T : class
{
return _exportProvider.GetExportedValues<T>(contractName);
}
IEnumerable<object> IExportProvider.GetExportedValues(Type contractType, string? contractName)
{
return _exportProvider
.GetExports(contractType, DefaultMetadataType, contractName)
.Select(item => item.Value)
.ExceptNullItems();
}
IEnumerable<IExport<object>> IExportProvider.GetExports(Type contractType, string? contractName)
{
return _exportProvider
.GetExports(contractType, DefaultMetadataType, contractName)
.Select(item => new ExportAdapter<object>(() => item.Value, new MetadataAdapter((IDictionary<string, object?>)item.Metadata)));
}
IEnumerable<IExport<T>> IExportProvider.GetExports<T>(string? contractName) where T : class
{
return _exportProvider
.GetExports(typeof(T), DefaultMetadataType, contractName)
.Select(item => new ExportAdapter<T>(() => (T?)item.Value, new MetadataAdapter((IDictionary<string, object?>)item.Metadata)));
}
IEnumerable<IExport<T, TMetadataView>> IExportProvider.GetExports<T, TMetadataView>(string? contractName) where T : class where TMetadataView : class
{
return _exportProvider
.GetExports<T, TMetadataView>(contractName)
.Select(item => new ExportAdapter<T, TMetadataView>(() => item.Value, item.Metadata));
}
}

44
ILSpy/ExtensionMethods.cs

@ -20,6 +20,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
@ -166,34 +167,37 @@ namespace ICSharpCode.ILSpy
return color?.R * 0.3 + color?.G * 0.6 + color?.B * 0.1 ?? 0.0;
}
internal static bool FormatExceptions(this IList<App.ExceptionData> exceptions, StringBuilder output)
internal static string? FormatExceptions(this IList<App.ExceptionData> exceptions)
{
if (exceptions.Count == 0)
return false;
bool first = true;
return null;
foreach (var item in exceptions)
{
if (first)
first = false;
else
output.AppendLine("-------------------------------------------------");
string delimiter = $"-------------------------------------------------{Environment.NewLine}";
return string.Join(delimiter, exceptions.Select(FormatException));
}
private static string FormatException(App.ExceptionData item)
{
var output = new StringBuilder();
if (!item.PluginName.IsNullOrEmpty())
output.AppendLine("Error(s) loading plugin: " + item.PluginName);
if (item.Exception is System.Reflection.ReflectionTypeLoadException exception)
{
foreach (var ex in exception.LoaderExceptions.ExceptNullItems())
{
output.AppendLine(ex.ToString());
output.AppendLine();
}
}
else
if (item.Exception is System.Reflection.ReflectionTypeLoadException exception)
{
foreach (var ex in exception.LoaderExceptions.ExceptNullItems())
{
output.AppendLine(item.Exception.ToString());
output.AppendLine(ex.ToString());
output.AppendLine();
}
}
else
{
output.AppendLine(item.Exception.ToString());
}
return true;
return output.ToString();
}
public static IDisposable PreserveFocus(this IInputElement? inputElement, bool preserve = true)

3
ILSpy/ILSpy.csproj

@ -45,10 +45,11 @@
<PackageReference Include="AvalonEdit" />
<PackageReference Include="Dirkster.AvalonDock.Themes.VS2013" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" />
<PackageReference Include="Microsoft.VisualStudio.Composition" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="DataGridExtensions" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="NaturalSort.Extension" />
<PackageReference Include="TomsToolbox.Composition.MicrosoftExtensions" />
<PackageReference Include="TomsToolbox.Wpf.Composition" />
<PackageReference Include="TomsToolbox.Wpf.Composition.Mef" />
<PackageReference Include="TomsToolbox.Wpf.Styles" />

Loading…
Cancel
Save