Browse Source

Port the sample plugin to Avalonia

TestPlugin was still WPF (net10.0-windows, UseWpf, System.Windows,
TomsToolbox, the old ICSharpCode.ILSpy namespace), so it could not load
into the cross-platform app. Re-target it to net10.0 and map each
extension point onto the port's contracts: the custom Language, About-page
addition, context-menu entry, main-menu/toolbar command, and an Avalonia
options page whose view the ViewLocator resolves by naming convention. The
command needs [ImportingConstructor] for System.Composition to satisfy its
service dependency. Reference it build-only from the tests (no DLL copied
into the test output) so a composition test can load it without the
headless app auto-loading the plugin into every other test.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
461faeda58
  1. 4
      ILSpy.Tests/ILSpy.Tests.csproj
  2. 106
      ILSpy.Tests/Plugins/TestPluginCompositionTests.cs
  3. 14
      ILSpy.sln
  4. 19
      TestPlugin/AboutPageAddition.cs
  5. 29
      TestPlugin/ContextMenuCommand.cs
  6. 30
      TestPlugin/CustomLanguage.cs
  7. 13
      TestPlugin/CustomOptionPage.xaml
  8. 15
      TestPlugin/CustomOptionsView.axaml
  9. 15
      TestPlugin/CustomOptionsView.axaml.cs
  10. 57
      TestPlugin/CustomOptionsViewModel.cs
  11. 19
      TestPlugin/MainMenuCommand.cs
  12. 18
      TestPlugin/Properties/AssemblyInfo.cs
  13. 24
      TestPlugin/TestPlugin.csproj

4
ILSpy.Tests/ILSpy.Tests.csproj

@ -29,6 +29,10 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ILSpy\ILSpy.csproj" /> <ProjectReference Include="..\ILSpy\ILSpy.csproj" />
<!-- Build the sample plugin so the composition test has something to load, but DON'T copy
Test.Plugin.dll into the test output: AppComposition scans *.Plugin.dll next to the test
assembly, and an auto-loaded plugin would add a language / menu items to every headless test. -->
<ProjectReference Include="..\TestPlugin\TestPlugin.csproj" ReferenceOutputAssembly="false" Private="false" />
</ItemGroup> </ItemGroup>
</Project> </Project>

106
ILSpy.Tests/Plugins/TestPluginCompositionTests.cs

@ -0,0 +1,106 @@
// 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.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Reflection;
using AwesomeAssertions;
using ILSpy.Commands;
using ILSpy.Languages;
using ILSpy.Options;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Plugins;
/// <summary>
/// The ported sample plugin (Test.Plugin.dll) exposes its extension points through the same MEF
/// contracts the app composes at startup. This loads the built plugin assembly (not referenced into
/// this test's output, to avoid polluting the headless app's *.Plugin.dll scan) and asserts its
/// exports resolve against the Avalonia app's extension-point types.
/// </summary>
[TestFixture]
public class TestPluginCompositionTests
{
static Assembly LoadPlugin()
{
// AppContext.BaseDirectory is .../ILSpy.Tests/bin/<Config>/net11.0/. The plugin targets net10.0
// (the host loads it as a library, so its TFM need not track the test project's), and builds to
// the sibling TestPlugin/bin/<Config>/net10.0/ (the ProjectReference guarantees it's built first).
var baseDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);
var config = Path.GetFileName(Path.GetDirectoryName(baseDir)!);
var repo = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", ".."));
var dll = Path.Combine(repo, "TestPlugin", "bin", config, "net10.0", "Test.Plugin.dll");
File.Exists(dll).Should().BeTrue($"the sample plugin must be built at {dll}");
return Assembly.LoadFrom(dll);
}
[Test]
public void Plugin_Contributes_A_Custom_Language()
{
using var container = new ContainerConfiguration().WithAssembly(LoadPlugin()).CreateContainer();
container.GetExports<Language>().Should().Contain(l => l.Name == "Custom",
"the plugin exports a Language named 'Custom'");
}
[Test]
public void Plugin_Contributes_An_Options_Page()
{
using var container = new ContainerConfiguration().WithAssembly(LoadPlugin()).CreateContainer();
// [ExportOptionPage] exports under the named "OptionPages" contract, not the default one.
container.GetExports<IOptionPage>("OptionPages").Should().Contain(p => p.Title == "TestPlugin",
"the plugin exports an [ExportOptionPage] titled 'TestPlugin'");
}
[Test]
public void Plugin_Contributes_An_About_Page_Addition()
{
using var container = new ContainerConfiguration().WithAssembly(LoadPlugin()).CreateContainer();
container.GetExports<IAboutPageAddition>().Should().NotBeEmpty(
"the plugin exports an IAboutPageAddition");
}
[Test]
public void Commands_With_A_DI_Constructor_Mark_It_Importing()
{
// System.Composition can only instantiate a parameterized command constructor when it is
// marked [ImportingConstructor]. A command export whose ctor takes services but lacks the
// attribute throws when the menu/toolbar builder materialises it -- which previously took the
// whole menu bar down. Guard every exported command in the plugin.
var commands = LoadPlugin().GetTypes()
.Where(t => typeof(System.Windows.Input.ICommand).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in commands)
{
var ctors = type.GetConstructors();
if (ctors.Any(c => c.GetParameters().Length == 0))
continue; // a parameterless ctor is always fine
ctors.Should().Contain(
c => c.GetCustomAttribute<System.Composition.ImportingConstructorAttribute>() != null,
$"{type.Name} has only parameterized constructors, so one must be [ImportingConstructor]");
}
}
}

14
ILSpy.sln

@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ILSpy.Tests.Windows", "ILSp
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ILSpy.ReadyToRun", "ILSpy.ReadyToRun\ILSpy.ReadyToRun.csproj", "{AF43A431-AAF7-4758-A180-42C378AE0FB2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ILSpy.ReadyToRun", "ILSpy.ReadyToRun\ILSpy.ReadyToRun.csproj", "{AF43A431-AAF7-4758-A180-42C378AE0FB2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "TestPlugin\TestPlugin.csproj", "{62B21FB3-2A55-4F51-8892-9D6B9864BC22}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -183,6 +185,18 @@ Global
{AF43A431-AAF7-4758-A180-42C378AE0FB2}.Release|x64.Build.0 = Release|Any CPU {AF43A431-AAF7-4758-A180-42C378AE0FB2}.Release|x64.Build.0 = Release|Any CPU
{AF43A431-AAF7-4758-A180-42C378AE0FB2}.Release|x86.ActiveCfg = Release|Any CPU {AF43A431-AAF7-4758-A180-42C378AE0FB2}.Release|x86.ActiveCfg = Release|Any CPU
{AF43A431-AAF7-4758-A180-42C378AE0FB2}.Release|x86.Build.0 = Release|Any CPU {AF43A431-AAF7-4758-A180-42C378AE0FB2}.Release|x86.Build.0 = Release|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Debug|x64.ActiveCfg = Debug|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Debug|x64.Build.0 = Debug|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Debug|x86.ActiveCfg = Debug|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Debug|x86.Build.0 = Debug|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Release|Any CPU.Build.0 = Release|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Release|x64.ActiveCfg = Release|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Release|x64.Build.0 = Release|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Release|x86.ActiveCfg = Release|Any CPU
{62B21FB3-2A55-4F51-8892-9D6B9864BC22}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

19
TestPlugin/AboutPageAddition.cs

@ -2,11 +2,15 @@
// This code is distributed under MIT X11 license (for details please see \doc\license.txt) // This code is distributed under MIT X11 license (for details please see \doc\license.txt)
using System.Composition; using System.Composition;
using System.Windows;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Highlighting; using Avalonia.Controls;
using ICSharpCode.ILSpy; using Avalonia.Media;
using AvaloniaEdit.Highlighting;
using ILSpy;
using ILSpy.Commands;
using ILSpy.TextView;
namespace TestPlugin namespace TestPlugin
{ {
@ -22,11 +26,14 @@ namespace TestPlugin
textOutput.WriteLine(); textOutput.WriteLine();
textOutput.BeginSpan(new HighlightingColor { textOutput.BeginSpan(new HighlightingColor {
Background = new SimpleHighlightingBrush(Colors.Black), Background = new SimpleHighlightingBrush(Colors.Black),
FontStyle = FontStyles.Italic, FontStyle = FontStyle.Italic,
Foreground = new SimpleHighlightingBrush(Colors.Aquamarine) Foreground = new SimpleHighlightingBrush(Colors.Aquamarine)
}); });
textOutput.Write("DO NOT PRESS THIS BUTTON --> "); textOutput.Write("DO NOT PRESS THIS BUTTON --> ");
textOutput.AddButton(null, "Test!", (sender, args) => MessageBox.Show("Naughty Naughty!", "Naughty!", MessageBoxButton.OK, MessageBoxImage.Exclamation)); textOutput.AddButton(null, "Test!", (sender, args) => {
if (sender is Button button)
button.Content = "Naughty Naughty!";
});
textOutput.Write(" <--"); textOutput.Write(" <--");
textOutput.WriteLine(); textOutput.WriteLine();
textOutput.EndSpan(); textOutput.EndSpan();

29
TestPlugin/ContextMenuCommand.cs

@ -2,42 +2,31 @@
// This code is distributed under MIT X11 license (for details please see \doc\license.txt) // This code is distributed under MIT X11 license (for details please see \doc\license.txt)
using System.Composition; using System.Composition;
using System.Linq;
using ICSharpCode.ILSpy; using ILSpy;
using ICSharpCode.ILSpy.TreeNodes;
namespace TestPlugin namespace TestPlugin
{ {
[ExportContextMenuEntryAttribute(Header = "_Save Assembly")] // Demonstrates contributing an entry to the tree's context menu. (The real "Save Assembly"
// command ships with the app; the concrete assembly-node types are internal to ILSpy, so this
// sample only shows the extension point and operates on the selected nodes generically.)
[ExportContextMenuEntry(Header = "_Test Plugin Command")]
[Shared] [Shared]
public class SaveAssembly : IContextMenuEntry public class TestPluginContextCommand : IContextMenuEntry
{ {
public bool IsVisible(TextViewContext context) public bool IsVisible(TextViewContext context)
{ {
return context.SelectedTreeNodes != null && context.SelectedTreeNodes.All(n => n is AssemblyTreeNode); return context.SelectedTreeNodes is { Length: > 0 };
} }
public bool IsEnabled(TextViewContext context) public bool IsEnabled(TextViewContext context)
{ {
return context.SelectedTreeNodes != null && context.SelectedTreeNodes.Length == 1; return context.SelectedTreeNodes is { Length: 1 };
} }
public void Execute(TextViewContext context) public void Execute(TextViewContext context)
{ {
if (context.SelectedTreeNodes == null) // A real plugin would act on context.SelectedTreeNodes here.
return;
AssemblyTreeNode node = (AssemblyTreeNode)context.SelectedTreeNodes[0];
var asm = node.LoadedAssembly.GetMetadataFileOrNull();
if (asm != null)
{
/*SaveFileDialog dlg = new SaveFileDialog();
dlg.FileName = node.LoadedAssembly.FileName;
dlg.Filter = "Assembly|*.dll;*.exe";
if (dlg.ShowDialog(MainWindow.Instance) == true) {
asm.MainModule.Write(dlg.FileName);
}*/
}
} }
} }
} }

30
TestPlugin/CustomLanguage.cs

@ -3,11 +3,15 @@
using System.Composition; using System.Composition;
using System.Reflection.Metadata; using System.Reflection.Metadata;
using System.Windows.Controls;
using Avalonia.Controls;
using ICSharpCode.Decompiler; using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpy;
using ILSpy;
using ILSpy.Languages;
using ILSpy.TextView;
namespace TestPlugin namespace TestPlugin
{ {
@ -34,32 +38,22 @@ namespace TestPlugin
// There are several methods available to override; in this sample, we deal with methods only // There are several methods available to override; in this sample, we deal with methods only
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options) public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options)
{ {
var module = ((MetadataModule)method.ParentModule).MetadataFile; var module = ((MetadataModule)method.ParentModule!).MetadataFile;
var methodDef = module.Metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken); var methodDef = module.Metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken);
if (methodDef.HasBody()) if (methodDef.HasBody())
{ {
var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress); var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress);
output.WriteLine("Size of method: {0} bytes", methodBody.GetCodeSize()); output.WriteLine("Size of method: {0} bytes", methodBody.GetCodeSize());
ISmartTextOutput smartOutput = output as ISmartTextOutput; if (output is ISmartTextOutput smartOutput)
if (smartOutput != null)
{ {
// when writing to the text view (but not when writing to a file), we can even add UI elements such as buttons: // when writing to the text view (but not when writing to a file), we can even add UI elements such as buttons:
smartOutput.AddButton(null, "Click me!", (sender, e) => (sender as Button).Content = "I was clicked!"); smartOutput.AddButton(null, "Click me!", (sender, e) => {
if (sender is Button button)
button.Content = "I was clicked!";
});
smartOutput.WriteLine(); smartOutput.WriteLine();
} }
// ICSharpCode.Decompiler.CSharp.CSharpDecompiler can be used to decompile to C#.
/*
ModuleDefinition module = LoadModule(assemblyFileName);
var typeSystem = new DecompilerTypeSystem(module);
CSharpDecompiler decompiler = new CSharpDecompiler(typeSystem, new DecompilerSettings());
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
SyntaxTree syntaxTree = decompiler.DecompileWholeModuleAsSingleFile();
var visitor = new CSharpOutputVisitor(output, FormattingOptionsFactory.CreateSharpDevelop());
syntaxTree.AcceptVisitor(visitor);
*/
} }
} }
} }

13
TestPlugin/CustomOptionPage.xaml

@ -1,13 +0,0 @@
<UserControl x:Class="TestPlugin.CustomOptionPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d:DesignHeight="500" d:DesignWidth="500"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
xmlns:testPlugin="clr-namespace:TestPlugin"
d:DataContext="{d:DesignInstance testPlugin:CustomOptionsViewModel}"
>
<StackPanel>
<CheckBox IsChecked="{Binding Options.UselessOption1}">Useless option 1</CheckBox>
<Slider Minimum="0" Maximum="100" Value="{Binding Options.UselessOption2}"/>
</StackPanel>
</UserControl>

15
TestPlugin/CustomOptionsView.axaml

@ -0,0 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:TestPlugin"
x:Class="TestPlugin.CustomOptionsView"
x:DataType="vm:CustomOptionsViewModel">
<StackPanel Spacing="6" Margin="6">
<CheckBox IsChecked="{Binding Options.UselessOption1, Mode=TwoWay}"
Content="A completely useless option" />
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="Another useless option:" VerticalAlignment="Center" />
<Slider Minimum="0" Maximum="100" Width="200"
Value="{Binding Options.UselessOption2, Mode=TwoWay}" />
</StackPanel>
</StackPanel>
</UserControl>

15
TestPlugin/CustomOptionsView.axaml.cs

@ -0,0 +1,15 @@
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under MIT X11 license (for details please see \doc\license.txt)
using Avalonia.Controls;
namespace TestPlugin
{
public partial class CustomOptionsView : UserControl
{
public CustomOptionsView()
{
InitializeComponent();
}
}
}

57
TestPlugin/CustomOptionPage.xaml.cs → TestPlugin/CustomOptionsViewModel.cs

@ -4,41 +4,30 @@
using System.Composition; using System.Composition;
using System.Xml.Linq; using System.Xml.Linq;
using ICSharpCode.ILSpy.Options; using CommunityToolkit.Mvvm.ComponentModel;
using ICSharpCode.ILSpy.Util;
using ICSharpCode.ILSpyX.Settings; using ICSharpCode.ILSpyX.Settings;
using TomsToolbox.Wpf; using ILSpy;
using TomsToolbox.Wpf.Composition.AttributedModel; using ILSpy.Options;
namespace TestPlugin namespace TestPlugin
{ {
[DataTemplate(typeof(CustomOptionsViewModel))] // The Options host renders an [ExportOptionPage] viewmodel by resolving its view through the
[NonShared] // app-wide ViewLocator (SomeViewModel -> SomeView, searched across every loaded assembly,
partial class CustomOptionPage // including plugins). So this viewmodel pairs with CustomOptionsView.axaml by name.
{
public CustomOptionPage()
{
InitializeComponent();
}
}
[ExportOptionPage(Order = 0)] [ExportOptionPage(Order = 0)]
[NonShared] [Shared]
class CustomOptionsViewModel : ObservableObjectBase, IOptionPage public sealed partial class CustomOptionsViewModel : ObservableObject, IOptionPage
{ {
private Options options;
public string Title => "TestPlugin"; public string Title => "TestPlugin";
public Options Options { [ObservableProperty]
get => options; private Options options = null!;
set => SetProperty(ref options, value);
}
public void Load(SettingsSnapshot snapshot) public void Load(SettingsService settings)
{ {
this.Options = snapshot.GetSettings<Options>(); Options = settings.GetSettings<Options>();
} }
public void LoadDefaults() public void LoadDefaults()
@ -47,23 +36,15 @@ namespace TestPlugin
} }
} }
class Options : ObservableObjectBase, ISettingsSection public sealed partial class Options : ObservableObject, ISettingsSection
{ {
static readonly XNamespace ns = "http://www.ilspy.net/testplugin"; static readonly XNamespace ns = "http://www.ilspy.net/testplugin";
bool uselessOption1; [ObservableProperty]
private bool uselessOption1;
public bool UselessOption1 {
get => uselessOption1;
set => SetProperty(ref uselessOption1, value);
}
double uselessOption2; [ObservableProperty]
private double uselessOption2;
public double UselessOption2 {
get => uselessOption2;
set => SetProperty(ref uselessOption2, value);
}
public XName SectionName { get; } = ns + "CustomOptions"; public XName SectionName { get; } = ns + "CustomOptions";
@ -76,10 +57,8 @@ namespace TestPlugin
public XElement SaveToXml() public XElement SaveToXml()
{ {
var section = new XElement(SectionName); var section = new XElement(SectionName);
section.SetAttributeValue("useless1", UselessOption1); section.SetAttributeValue("useless1", UselessOption1);
section.SetAttributeValue("useless2", UselessOption2); section.SetAttributeValue("useless2", UselessOption2);
return section; return section;
} }
} }

19
TestPlugin/MainMenuCommand.cs

@ -3,28 +3,33 @@
using System.Composition; using System.Composition;
using ICSharpCode.ILSpy; using ILSpy.AssemblyTree;
using ICSharpCode.ILSpy.AssemblyTree; using ILSpy.Commands;
namespace TestPlugin namespace TestPlugin
{ {
// Menu: menu into which the item is added // ParentMenuID: menu into which the item is added
// MenuIcon: optional, icon to use for the menu item. Must be embedded as "Resource" (WPF-style resource) in the same assembly as the command type. // MenuIcon: optional, icon to use for the menu item. Embedded as an AvaloniaResource in this assembly.
// Header: text on the menu item // Header: text on the menu item
// MenuCategory: optional, used for grouping related menu items together. A separator is added between different groups. // MenuCategory: optional, used for grouping related menu items together. A separator is added between different groups.
// MenuOrder: controls the order in which the items appear (items are sorted by this value) // MenuOrder: controls the order in which the items appear (items are sorted by this value)
[ExportMainMenuCommand(ParentMenuID = "_File", MenuIcon = "Clear.png", Header = "_Clear List", MenuCategory = "Open", MenuOrder = 1.5)] [ExportMainMenuCommand(ParentMenuID = "_File", MenuIcon = "Clear.png", Header = "_Clear List", MenuCategory = "Open", MenuOrder = 1.5)]
// ToolTip: the tool tip // ToolTip: the tool tip
// ToolbarIcon: The icon. Must be embedded as "Resource" (WPF-style resource) in the same assembly as the command type. // ToolbarIcon: the icon, embedded as an AvaloniaResource in this assembly.
// ToolbarCategory: optional, used for grouping related toolbar items together. A separator is added between different groups. // ToolbarCategory: optional, used for grouping related toolbar items together. A separator is added between different groups.
// ToolbarOrder: controls the order in which the items appear (items are sorted by this value) // ToolbarOrder: controls the order in which the items appear (items are sorted by this value)
[ExportToolbarCommand(ToolTip = "Clears the current assembly list", ToolbarIcon = "Clear.png", ToolbarCategory = "Open", ToolbarOrder = 1.5)] [ExportToolbarCommand(ToolTip = "Clears the current assembly list", ToolbarIcon = "Clear.png", ToolbarCategory = "Open", ToolbarOrder = 1.5)]
[Shared] [Shared]
// System.Composition needs the importing constructor marked explicitly; without this the menu/
// toolbar builder can't instantiate the command and the whole menu build fails.
[method: ImportingConstructor]
public class UnloadAllAssembliesCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand public class UnloadAllAssembliesCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand
{ {
public override void Execute(object parameter) public override void Execute(object? parameter)
{ {
foreach (var loadedAssembly in assemblyTreeModel.AssemblyList.GetAssemblies()) if (assemblyTreeModel.AssemblyList is not { } assemblyList)
return;
foreach (var loadedAssembly in assemblyList.GetAssemblies())
{ {
loadedAssembly.AssemblyList.Unload(loadedAssembly); loadedAssembly.AssemblyList.Unload(loadedAssembly);
} }

18
TestPlugin/Properties/AssemblyInfo.cs

@ -1,18 +0,0 @@
#region Using directives
using System.Reflection;
using System.Runtime.InteropServices;
#endregion
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyDescription("")]
[assembly: AssemblyCopyright("Copyright 2011")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// This sets the default COM visibility of types in the assembly to invisible.
// If you need to expose a type to COM, use [ComVisible(true)] on that type.
[assembly: ComVisible(false)]

24
TestPlugin/TestPlugin.csproj

@ -1,21 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<AssemblyName>Test.Plugin</AssemblyName> <AssemblyName>Test.Plugin</AssemblyName>
<UseWpf>true</UseWpf> <Nullable>enable</Nullable>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
<CheckForOverflowUnderflow>True</CheckForOverflowUnderflow>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -24,7 +12,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Resource Include="Clear.png" /> <PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="CommunityToolkit.Mvvm" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Clear.png" />
<None Include="Readme.txt" /> <None Include="Readme.txt" />
</ItemGroup> </ItemGroup>

Loading…
Cancel
Save