Skip to content

Commit

Permalink
Migrate DI from Microsoft.VisualStudio.Composition to Microsoft.Exten…
Browse files Browse the repository at this point in the history
…sions.DependencyInjection
  • Loading branch information
tom-englert committed Oct 20, 2024
1 parent 21e5d0f commit 66544e6
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 200 deletions.
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand All @@ -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" />
Expand Down
106 changes: 50 additions & 56 deletions ILSpy/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
Expand All @@ -36,8 +35,6 @@

using Medo.Application;

using Microsoft.VisualStudio.Composition;

using TomsToolbox.Wpf.Styles;
using ICSharpCode.ILSpyX.TreeView;

Expand All @@ -47,6 +44,11 @@
using System.Globalization;
using System.Threading;

using Microsoft.Extensions.DependencyInjection;

using TomsToolbox.Composition.MicrosoftExtensions;
using TomsToolbox.Essentials;

namespace ICSharpCode.ILSpy
{
/// <summary>
Expand All @@ -59,10 +61,9 @@ public partial class App : Application

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()
Expand All @@ -80,6 +81,14 @@ public App()

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;
Expand All @@ -92,8 +101,6 @@ public App()

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.
Expand All @@ -103,7 +110,7 @@ public App()
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),
Expand All @@ -125,6 +132,13 @@ public App()
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)
Expand All @@ -136,22 +150,17 @@ static Assembly ResolvePluginDependencies(AssemblyLoadContext context, AssemblyN
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)
{
Expand All @@ -160,62 +169,47 @@ private static async Task InitializeMef()
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;
}
}

protected override void OnStartup(StartupEventArgs e)
{
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();
}

Expand Down
44 changes: 25 additions & 19 deletions ILSpy/AssemblyTree/AssemblyTreeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,17 @@
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;
Expand Down Expand Up @@ -160,13 +159,15 @@ private bool HandleCommandLineArguments(CommandLineArguments args)
/// 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>();
Expand All @@ -180,7 +181,7 @@ public async Task HandleSingleInstanceCommandLineArguments(string[] args)
{
var cmdArgs = CommandLineArguments.Create(args);

await Dispatcher.InvokeAsync(() => {
await Dispatcher.InvokeAsync(async () => {
if (!HandleCommandLineArguments(cmdArgs))
return;
Expand All @@ -192,11 +193,11 @@ await Dispatcher.InvokeAsync(() => {
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)
Expand Down Expand Up @@ -386,26 +387,31 @@ public void Initialize()
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)
Expand Down
Loading

0 comments on commit 66544e6

Please sign in to comment.