Skip to content

Commit

Permalink
Add abstraction layer for MEF
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-englert committed Aug 11, 2024
1 parent fa0ab07 commit b860ca3
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 19 deletions.
6 changes: 4 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@
<PackageVersion Include="System.Reflection.Metadata" Version="8.0.0" />
<PackageVersion Include="System.Resources.Extensions" Version="8.0.0" />
<PackageVersion Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
<PackageVersion Include="TomsToolbox.Wpf.Styles" Version="2.17.3" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="TomsToolbox.Composition.Analyzer" Version="2.18.1" />
<PackageVersion Include="TomsToolbox.Wpf.Composition" Version="2.18.1" />
<PackageVersion Include="TomsToolbox.Wpf.Styles" Version="2.18.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
</ItemGroup>
</Project>
4 changes: 4 additions & 0 deletions ICSharpCode.Decompiler/ICSharpCode.Decompiler.ruleset
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,8 @@
<Rule Id="ProhibitedModifiersAnalyzer" Action="None" />
<Rule Id="RECS0001" Action="Info" />
</Rules>
<Rules AnalyzerId="TomsToolbox.Composition.Analyzer" RuleNamespace="TomsToolbox.Composition.Analyzer">
<Rule Id="MEF001" Action="Info" />
<Rule Id="MEF006" Action="Hidden" />
</Rules>
</RuleSet>
6 changes: 6 additions & 0 deletions ILSpy.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions ILSpy/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
Expand All @@ -52,8 +56,7 @@ public partial class App : Application
internal static CommandLineArguments CommandLineArguments;
internal static readonly IList<ExceptionData> StartupExceptions = new List<ExceptionData>();

public static ExportProvider ExportProvider { get; private set; }
public static IExportProviderFactory ExportProviderFactory { get; private set; }
public static IExportProvider ExportProvider { get; private set; }

internal class ExceptionData
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
12 changes: 7 additions & 5 deletions ILSpy/ContextMenuEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
using ICSharpCode.ILSpy.Controls.TreeView;
using ICSharpCode.ILSpyX.TreeView;

using TomsToolbox.Composition;

namespace ICSharpCode.ILSpy
{
public interface IContextMenuEntry
Expand Down Expand Up @@ -205,7 +207,7 @@ public static void Add(DataGrid dataGrid)
readonly DecompilerTextView textView;
readonly ListBox listBox;
readonly DataGrid dataGrid;
readonly Lazy<IContextMenuEntry, IContextMenuEntryMetadata>[] entries;
readonly IExport<IContextMenuEntry, IContextMenuEntryMetadata>[] entries;

private ContextMenuProvider()
{
Expand Down Expand Up @@ -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<string, Lazy<IContextMenuEntry, IContextMenuEntryMetadata>[]>();
Lazy<IContextMenuEntry, IContextMenuEntryMetadata>[] topLevelGroup = null;
var menuGroups = new Dictionary<string, IExport<IContextMenuEntry, IContextMenuEntryMetadata>[]>();
IExport<IContextMenuEntry, IContextMenuEntryMetadata>[] topLevelGroup = null;
foreach (var group in entries.OrderBy(c => c.Metadata.Order).GroupBy(c => c.Metadata.ParentMenuID))
{
if (group.Key == null)
Expand All @@ -301,10 +303,10 @@ bool ShowContextMenu(TextViewContext context, out ContextMenu menu)
menuGroups.Add(group.Key, group.ToArray());
}
}
BuildMenu(topLevelGroup ?? Array.Empty<Lazy<IContextMenuEntry, IContextMenuEntryMetadata>>(), menu.Items);
BuildMenu(topLevelGroup ?? Array.Empty<IExport<IContextMenuEntry, IContextMenuEntryMetadata>>(), menu.Items);
return menu.Items.Count > 0;

void BuildMenu(Lazy<IContextMenuEntry, IContextMenuEntryMetadata>[] menuGroup, ItemCollection parent)
void BuildMenu(IExport<IContextMenuEntry, IContextMenuEntryMetadata>[] menuGroup, ItemCollection parent)
{
foreach (var category in menuGroup.GroupBy(c => c.Metadata.Category))
{
Expand Down
88 changes: 88 additions & 0 deletions ILSpy/ExportProviderAdapter.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Adapter for Microsoft.VisualStudio.Composition.<see cref="ExportProvider"/> to <see cref="IExportProvider"/>.
/// </summary>
public 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

Check warning on line 35 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.GetExportedValue' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
{
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

Check warning on line 40 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.GetExportedValueOrDefault' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 40 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.GetExportedValueOrDefault' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
{
return _exportProvider.GetExportedValues<T>(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<T>(string? contractName, /*[NotNullWhen(true)]*/ out T? value) where T : class

Check warning on line 47 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.TryGetExportedValue' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 47 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.TryGetExportedValue' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
#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<T>(contractName).SingleOrDefault();

return !Equals(value, default(T));
}

IEnumerable<T> IExportProvider.GetExportedValues<T>(string? contractName) where T : class

Check warning on line 55 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.GetExportedValues' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)

Check warning on line 55 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Release)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.GetExportedValues' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
{
return _exportProvider.GetExportedValues<T>(contractName);
}

IEnumerable<object> IExportProvider.GetExportedValues(Type contractType, string? contractName)

Check warning on line 60 in ILSpy/ExportProviderAdapter.cs

View workflow job for this annotation

GitHub Actions / Build (Debug)

Make 'ExportProviderAdapter' sealed (a breaking change if this class has previously shipped), implement the method non-explicitly, or implement a new method that exposes the functionality of 'TomsToolbox.Composition.IExportProvider.GetExportedValues' and is visible to derived classes (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1033)
{
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));
}
}
2 changes: 2 additions & 0 deletions ILSpy/ILSpy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
<PackageReference Include="DataGridExtensions" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="NaturalSort.Extension" />
<PackageReference Include="TomsToolbox.Composition.Analyzer" />
<PackageReference Include="TomsToolbox.Wpf.Composition" />
<PackageReference Include="TomsToolbox.Wpf.Styles" />
</ItemGroup>

Expand Down
4 changes: 3 additions & 1 deletion ILSpy/Languages/Languages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

using Microsoft.VisualStudio.Composition;

using TomsToolbox.Composition;

namespace ICSharpCode.ILSpy
{
public static class Languages
Expand All @@ -39,7 +41,7 @@ public static ReadOnlyCollection<Language> AllLanguages {
get { return allLanguages; }
}

internal static void Initialize(ExportProvider ep)
internal static void Initialize(IExportProvider ep)
{
List<Language> languages = new List<Language>();
languages.AddRange(ep.GetExportedValues<Language>());
Expand Down
4 changes: 3 additions & 1 deletion ILSpy/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
using Microsoft.Win32;
using ICSharpCode.ILSpyX.TreeView;

using TomsToolbox.Composition;

namespace ICSharpCode.ILSpy
{
/// <summary>
Expand Down Expand Up @@ -245,7 +247,7 @@ void InitToolbar()

}

Button MakeToolbarItem(Lazy<ICommand, IToolbarCommandMetadata> command)
Button MakeToolbarItem(IExport<ICommand, IToolbarCommandMetadata> command)
{
return new Button {
Style = ThemeManager.Current.CreateToolBarButtonStyle(),
Expand Down
10 changes: 4 additions & 6 deletions ILSpy/Options/OptionsDialog.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpyX.Settings;

using TomsToolbox.Composition;

namespace ICSharpCode.ILSpy.Options
{
public class TabItemViewModel
Expand All @@ -47,16 +49,12 @@ public TabItemViewModel(string header, UIElement content)
/// </summary>
public partial class OptionsDialog : Window
{

readonly Lazy<UIElement, IOptionsMetadata>[] optionPages;
readonly IExport<UIElement, IOptionsMetadata>[] 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<UIElement, IOptionsMetadata>("OptionPages").ToArray();
ILSpySettings settings = ILSpySettings.Load();
foreach (var optionPage in optionPages.OrderBy(p => p.Metadata.Order))
Expand Down

0 comments on commit b860ca3

Please sign in to comment.