diff --git a/ILSpy.Tests/Views/OpenFromGacDialogStructureTests.cs b/ILSpy.Tests/Views/OpenFromGacDialogStructureTests.cs new file mode 100644 index 000000000..b640b034c --- /dev/null +++ b/ILSpy.Tests/Views/OpenFromGacDialogStructureTests.cs @@ -0,0 +1,82 @@ +// 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.Controls; +using Avalonia.Headless.NUnit; +using Avalonia.VisualTree; + +using AwesomeAssertions; + +using ICSharpCode.ILSpy.Properties; + +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Views; + +/// +/// Pins the GAC browser's grid shape against the WPF dialog. WPF uses a ListView with a +/// GridView of five SortableGridViewColumns — Reference Name, Version, Culture, +/// Public Key Token, Location — each click-to-sort. The Avalonia port started with a +/// single-column ListBox that mashed every field into one TextBlock via +/// ; this fixture asserts the rebuilt dialog matches the +/// WPF column set so a future "let's go back to a simple list" doesn't quietly remove +/// the per-field sort and column widths users came to rely on. +/// +[TestFixture] +public class OpenFromGacDialogStructureTests +{ + [AvaloniaTest] + public void Dialog_Window_Title_Comes_From_Localised_Resources() + { + var dialog = new OpenFromGacDialog(); + dialog.Title.Should().Be(Resources.OpenFrom, + "the dialog title must match WPF's localised Resources.OpenFrom (currently 'Open From GAC')"); + } + + [AvaloniaTest] + public void Dialog_Uses_A_DataGrid_With_Five_Sortable_Columns_Matching_WPF() + { + var dialog = new OpenFromGacDialog(); + // Force layout so the DataGrid's internal column templates are realised. + dialog.Measure(global::Avalonia.Size.Infinity); + dialog.Arrange(new global::Avalonia.Rect(dialog.DesiredSize)); + + var grid = dialog.GetVisualDescendants().OfType().FirstOrDefault(); + grid.Should().NotBeNull( + "the GAC browser must use a DataGrid (not a ListBox) so each field gets its own " + + "sortable column — that's the whole reason WPF picked ListView+GridView"); + + var headers = grid!.Columns.Select(c => c.Header as string).ToList(); + headers.Should().Contain(new[] { + Resources.ReferenceName, + Resources.Version, + Resources.CultureLabel, + Resources.PublicToken, + Resources.Location, + }, "the five WPF column headers must all be present and localised"); + + grid.Columns.Should().HaveCount(5, + "exactly five columns — no extras, no missing fields"); + grid.Columns.Should().AllSatisfy(c => c.CanUserSort.Should().BeTrue( + "every column must be sortable to match WPF's SortableGridViewColumn.SortMode=Automatic")); + } +} diff --git a/ILSpy/Views/GacEntry.cs b/ILSpy/Views/GacEntry.cs new file mode 100644 index 000000000..9b49fdbc8 --- /dev/null +++ b/ILSpy/Views/GacEntry.cs @@ -0,0 +1,71 @@ +// 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.Text; + +using ICSharpCode.Decompiler.Metadata; + +namespace ILSpy.Views +{ + /// + /// One row in the GAC browser. Wraps an + the + /// resolved on-disk path, exposing each WPF column's bound property (ShortName, + /// Version, Culture, PublicKeyToken, FileName) so the DataGrid can sort by any of + /// them independently. + back + /// the filter, mirroring the WPF dialog. + /// + public sealed class GacEntry + { + readonly AssemblyNameReference reference; + string? formattedVersion; + string? publicKeyToken; + + public GacEntry(AssemblyNameReference reference, string fileName) + { + this.reference = reference; + FileName = fileName; + } + + public string FileName { get; } + + public string FullName => reference.FullName; + + public string ShortName => reference.Name; + + public System.Version? Version => reference.Version; + + public string FormattedVersion => formattedVersion ??= (Version?.ToString() ?? string.Empty); + + public string Culture + => string.IsNullOrEmpty(reference.Culture) ? "neutral" : reference.Culture!; + + public string PublicKeyToken => publicKeyToken ??= FormatPublicKeyToken(reference.PublicKeyToken); + + static string FormatPublicKeyToken(byte[]? token) + { + if (token == null || token.Length == 0) + return "null"; + var sb = new StringBuilder(token.Length * 2); + foreach (var b in token) + sb.AppendFormat("{0:x2}", b); + return sb.ToString(); + } + + public override string ToString() => FullName; + } +} diff --git a/ILSpy/Views/OpenFromGacDialog.axaml b/ILSpy/Views/OpenFromGacDialog.axaml index 0687c75d9..0a7d8c045 100644 --- a/ILSpy/Views/OpenFromGacDialog.axaml +++ b/ILSpy/Views/OpenFromGacDialog.axaml @@ -1,25 +1,46 @@ - + CanResize="True"> + - + - - - - - - - - - -