Browse Source

Add "Add preconfigured list" to Manage Assembly Lists

The dialog lost the entry point for building a preconfigured list when
the view-model wasn't ported; the backing AssemblyListManager.
CreateDefaultList was already present and cross-platform. Re-add the
button and a flyout of the generated lists. The three GAC-based
framework lists resolve nothing without a GAC, so they're only offered
when a GAC directory exists (i.e. on Windows); the per-installed-runtime
entries work everywhere.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
8b26f2c0e9
  1. 91
      ILSpy.Tests/AssemblyList/PreconfiguredAssemblyListTests.cs
  2. 6
      ILSpy/Views/ManageAssemblyListsDialog.axaml
  3. 103
      ILSpy/Views/ManageAssemblyListsDialog.axaml.cs

91
ILSpy.Tests/AssemblyList/PreconfiguredAssemblyListTests.cs

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
// 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.Linq;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpyX;
using ILSpy;
using ILSpy.AppEnv;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
[TestFixture]
public class PreconfiguredAssemblyListTests
{
[AvaloniaTest]
public void Preconfigured_Menu_Offers_Installed_Runtimes_And_Creates_A_Populated_List()
{
// The "Add preconfigured list..." menu resolves one entry per installed .NET runtime
// (discovered under the dotnet install's shared folder) on every platform, and
// selecting one builds a populated assembly list via the shared AssemblyListManager.
var settingsService = AppComposition.Current.GetExport<SettingsService>();
var manager = settingsService.AssemblyListManager;
var dialog = new ManageAssemblyListsDialog(settingsService);
var configs = dialog.GetPreconfiguredAssemblyLists().ToList();
// At least one runtime-path entry must be discovered (ILSpy itself runs on one).
var runtimeEntry = configs.FirstOrDefault(c => c.Path != null);
((object?)runtimeEntry).Should().NotBeNull(
"the running machine has at least one installed .NET runtime to offer");
var newName = "Preconfigured Test " + System.Guid.NewGuid().ToString("N");
manager.AssemblyLists.Should().NotContain(newName);
var created = dialog.CreatePreconfiguredList(runtimeEntry!, newName);
((object?)created).Should().NotBeNull("a runtime directory resolves to a non-empty list");
created!.Count.Should().BeGreaterThan(0, "the runtime's framework assemblies populate the list");
manager.AssemblyLists.Should().Contain(newName, "the new preconfigured list is registered");
}
[AvaloniaTest]
public void Preconfigured_Menu_Hides_Gac_Lists_When_No_Gac_Is_Present()
{
// The three GAC-based framework lists (.NET 4.x / 3.5 / ASP.NET MVC) resolve nothing on
// platforms without a GAC, so they are only offered when a GAC directory actually
// exists (i.e. on Windows). On a GAC-less host they must not appear.
var settingsService = AppComposition.Current.GetExport<SettingsService>();
var dialog = new ManageAssemblyListsDialog(settingsService);
var names = dialog.GetPreconfiguredAssemblyLists().Select(c => c.Name).ToList();
bool gacPresent = ICSharpCode.Decompiler.Metadata.UniversalAssemblyResolver
.GetGacPaths().Any(System.IO.Directory.Exists);
if (gacPresent)
{
names.Should().Contain(AssemblyListManager.DotNet4List);
}
else
{
names.Should().NotContain(AssemblyListManager.DotNet4List);
names.Should().NotContain(AssemblyListManager.DotNet35List);
names.Should().NotContain(AssemblyListManager.ASPDotNetMVC3List);
}
}
}

6
ILSpy/Views/ManageAssemblyListsDialog.axaml

@ -13,9 +13,13 @@ @@ -13,9 +13,13 @@
<Button Name="DeleteButton" Content="_Delete" />
<Button Name="ResetButton" Margin="0,12,0,0" Content="_Reset" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6">
<Grid Grid.Row="1" Grid.ColumnSpan="2" ColumnDefinitions="*,Auto">
<Button Grid.Column="0" Name="AddPreconfiguredButton" HorizontalAlignment="Left"
Content="Add preconfigured list..." />
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6">
<Button Name="SelectButton" Content="Select" IsDefault="True" MinWidth="72" />
<Button Name="CloseButton" Content="Close" IsCancel="True" MinWidth="72" />
</StackPanel>
</Grid>
</Grid>
</Window>

103
ILSpy/Views/ManageAssemblyListsDialog.axaml.cs

@ -17,12 +17,16 @@ @@ -17,12 +17,16 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpyX;
namespace ILSpy.Views
@ -65,10 +69,25 @@ namespace ILSpy.Views @@ -65,10 +69,25 @@ namespace ILSpy.Views
this.FindControl<Button>("RenameButton")!.Click += async (_, _) => await RenameListAsync();
this.FindControl<Button>("DeleteButton")!.Click += (_, _) => DeleteList();
this.FindControl<Button>("ResetButton")!.Click += (_, _) => ResetLists();
var addPreconfigured = this.FindControl<Button>("AddPreconfiguredButton")!;
addPreconfigured.Click += (_, _) => ShowPreconfiguredMenu(addPreconfigured);
this.FindControl<Button>("SelectButton")!.Click += (_, _) => SelectAndClose();
this.FindControl<Button>("CloseButton")!.Click += (_, _) => Close();
}
void ShowPreconfiguredMenu(Button anchor)
{
var flyout = new MenuFlyout();
foreach (var config in GetPreconfiguredAssemblyLists())
{
var captured = config;
var item = new MenuItem { Header = captured.Name };
item.Click += async (_, _) => await AddPreconfiguredListAsync(captured);
flyout.Items.Add(item);
}
flyout.ShowAt(anchor);
}
string? SelectedListName => listsBox.SelectedItem as string;
async Task<string?> PromptAsync(string title, string? initialText = null)
@ -136,5 +155,89 @@ namespace ILSpy.Views @@ -136,5 +155,89 @@ namespace ILSpy.Views
settingsService.SessionSettings.ActiveAssemblyList = selected;
Close();
}
/// <summary>
/// The list of preconfigured assembly lists offered by the "Add preconfigured list..."
/// menu: the three GAC-based framework lists (only when a GAC is actually present, i.e.
/// on Windows) plus one entry per installed .NET runtime discovered under the dotnet
/// install's <c>shared</c> folder (works on every platform). Mirrors WPF's
/// ManageAssemblyListsViewModel.ResolvePreconfiguredAssemblyLists.
/// </summary>
internal IEnumerable<PreconfiguredAssemblyList> GetPreconfiguredAssemblyLists()
{
if (IsGacAvailable())
{
yield return new PreconfiguredAssemblyList(AssemblyListManager.DotNet4List);
yield return new PreconfiguredAssemblyList(AssemblyListManager.DotNet35List);
yield return new PreconfiguredAssemblyList(AssemblyListManager.ASPDotNetMVC3List);
}
var basePath = DotNetCorePathFinder.FindDotNetExeDirectory();
if (basePath == null)
yield break;
var sharedRoot = Path.Combine(basePath, "shared");
if (!Directory.Exists(sharedRoot))
yield break;
var foundVersions = new Dictionary<string, string>();
var latestRevision = new Dictionary<string, int>();
foreach (var sdkDir in Directory.GetDirectories(sharedRoot))
{
if (sdkDir.EndsWith(".Ref", StringComparison.OrdinalIgnoreCase))
continue;
foreach (var versionDir in Directory.GetDirectories(sdkDir))
{
var match = Regex.Match(versionDir, @"[/\\](?<name>[A-z0-9.]+)[/\\](?<version>\d+\.\d+)(.(?<revision>\d+))?(?<suffix>-.*)?$");
if (!match.Success)
continue;
string name = match.Groups["name"].Value;
int index = name.LastIndexOfAny(new[] { '/', '\\' });
if (index >= 0)
name = name.Substring(index + 1);
string text = name + " " + match.Groups["version"].Value;
if (!latestRevision.TryGetValue(text, out int revision))
revision = -1;
int newRevision = int.Parse(match.Groups["revision"].Value);
if (newRevision > revision)
{
latestRevision[text] = newRevision;
foundVersions[text] = versionDir;
}
}
}
foreach (var pair in foundVersions)
yield return new PreconfiguredAssemblyList($"{pair.Key}(.{latestRevision[pair.Key]})", pair.Value);
}
static bool IsGacAvailable()
=> UniversalAssemblyResolver.GetGacPaths().Any(Directory.Exists);
async Task AddPreconfiguredListAsync(PreconfiguredAssemblyList config)
{
var name = await PromptAsync("Add preconfigured list...", config.Name);
if (string.IsNullOrWhiteSpace(name) || manager.AssemblyLists.Contains(name))
return;
CreatePreconfiguredList(config, name);
}
/// <summary>
/// Builds the preconfigured list via the shared <see cref="AssemblyListManager"/> and
/// registers it under <paramref name="newName"/>, but only if it actually resolved any
/// assemblies (the GAC-based lists yield nothing off Windows). Returns the created list,
/// or <see langword="null"/> when nothing was added. Separated from the prompt so it can
/// be driven directly by tests.
/// </summary>
internal AssemblyList? CreatePreconfiguredList(PreconfiguredAssemblyList config, string newName)
{
var list = manager.CreateDefaultList(config.Name, config.Path, newName);
if (list.Count == 0)
return null;
manager.AddListIfNotExists(list);
return list;
}
internal sealed record PreconfiguredAssemblyList(string Name, string? Path = null);
}
}

Loading…
Cancel
Save