Browse Source

Render token cells as hyperlink-styled buttons

[ColumnInfo(Kind=Token)] properties now produce a DataGridTemplateColumn
whose cell is a transparent-background, blue-foreground Button — the
hyperlink affordance the user clicks to jump to the target table row.
Click raises NavigateToCellRequested(Row, ColumnName) on the owning
MetadataTablePageModel; Phase 3c subscribes from the dock workspace,
resolves the token to its target table tree node, and scrolls into view.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
3ae24fc12a
  1. 21
      ILSpy.Tests/Metadata/MetadataColumnBuilderTests.cs
  2. 76
      ILSpy/Metadata/MetadataColumnBuilder.cs
  3. 13
      ILSpy/ViewModels/MetadataTablePageModel.cs

21
ILSpy.Tests/Metadata/MetadataColumnBuilderTests.cs

@ -73,4 +73,25 @@ public class MetadataColumnBuilderTests @@ -73,4 +73,25 @@ public class MetadataColumnBuilderTests
first.Should().NotBeSameAs(second);
first[0].Should().NotBeSameAs(second[0]);
}
sealed class SampleEntryWithToken
{
public int RID { get; set; }
[ColumnInfo("X8", Kind = ColumnKind.Token)]
public int Method { get; set; }
public string Name { get; set; } = "";
}
[AvaloniaTest]
public void For_Emits_Template_Column_For_Token_Kind_Properties()
{
// Properties annotated Kind=Token render as a DataGridTemplateColumn whose cell is
// a hyperlink-styled button. Plain Format-only columns stay as text.
var columns = MetadataColumnBuilder.For<SampleEntryWithToken>();
columns[0].Should().BeOfType<DataGridTextColumn>("RID has no [ColumnInfo]");
columns[1].Should().BeOfType<DataGridTemplateColumn>("Method is Kind=Token");
columns[2].Should().BeOfType<DataGridTextColumn>("Name has no [ColumnInfo]");
}
}

76
ILSpy/Metadata/MetadataColumnBuilder.cs

@ -22,9 +22,16 @@ using System.Globalization; @@ -22,9 +22,16 @@ using System.Globalization;
using System.Linq;
using System.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
using ILSpy.ViewModels;
namespace ILSpy.Metadata
{
@ -61,21 +68,70 @@ namespace ILSpy.Metadata @@ -61,21 +68,70 @@ namespace ILSpy.Metadata
continue;
var info = prop.GetCustomAttribute<ColumnInfoAttribute>();
var binding = new Binding(prop.Name) { Mode = BindingMode.OneWay };
if (!string.IsNullOrEmpty(info?.Format))
if (info?.Kind == ColumnKind.Token)
{
binding.Converter = new HexFormatConverter(info!.Format);
columns.Add(BuildTokenColumn(prop, info));
}
else
{
columns.Add(BuildTextColumn(prop, info));
}
columns.Add(new DataGridTextColumn {
Header = prop.Name,
IsReadOnly = true,
Binding = binding,
SortMemberPath = prop.Name,
});
}
return columns;
}
static DataGridTextColumn BuildTextColumn(PropertyInfo prop, ColumnInfoAttribute? info)
{
var binding = new Binding(prop.Name) { Mode = BindingMode.OneWay };
if (!string.IsNullOrEmpty(info?.Format))
binding.Converter = new HexFormatConverter(info.Format);
return new DataGridTextColumn {
Header = prop.Name,
IsReadOnly = true,
Binding = binding,
SortMemberPath = prop.Name,
};
}
static DataGridTemplateColumn BuildTokenColumn(PropertyInfo prop, ColumnInfoAttribute? info)
{
var format = info?.Format;
var template = new FuncDataTemplate<object>((row, _) => {
var btn = new Button {
Background = Brushes.Transparent,
BorderThickness = default,
Padding = new Thickness(2, 0),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Foreground = Brushes.Blue,
Cursor = new global::Avalonia.Input.Cursor(global::Avalonia.Input.StandardCursorType.Hand),
Content = FormatTokenValue(row, prop, format),
};
btn.Click += (_, _) => {
if (btn.FindAncestorOfType<Views.MetadataTablePage>()?.DataContext is MetadataTablePageModel page)
page.RaiseNavigateToCell(row, prop.Name);
};
return btn;
}, supportsRecycling: true);
return new DataGridTemplateColumn {
Header = prop.Name,
IsReadOnly = true,
CellTemplate = template,
SortMemberPath = prop.Name,
};
}
static string FormatTokenValue(object row, PropertyInfo prop, string? format)
{
var value = prop.GetValue(row);
if (value is null)
return string.Empty;
if (value is Enum enumValue)
value = System.Convert.ChangeType(enumValue, enumValue.GetTypeCode(), CultureInfo.InvariantCulture);
if (!string.IsNullOrEmpty(format) && value is IFormattable formattable)
return formattable.ToString(format, CultureInfo.InvariantCulture);
return value.ToString() ?? string.Empty;
}
}
/// <summary>

13
ILSpy/ViewModels/MetadataTablePageModel.cs

@ -46,5 +46,18 @@ namespace ILSpy.ViewModels @@ -46,5 +46,18 @@ namespace ILSpy.ViewModels
/// </summary>
[ObservableProperty]
private int? scrollToRow;
/// <summary>
/// Raised when the user clicks a hyperlink-styled token cell. The host (the dock
/// workspace) resolves the (row, columnName) pair to a metadata token and navigates
/// to the target table row.
/// </summary>
public event Action<MetadataCellNavigationEventArgs>? NavigateToCellRequested;
internal void RaiseNavigateToCell(object row, string columnName)
=> NavigateToCellRequested?.Invoke(new MetadataCellNavigationEventArgs(row, columnName));
}
/// <summary>The (row, column) pair clicked in a token cell.</summary>
public sealed record MetadataCellNavigationEventArgs(object Row, string ColumnName);
}

Loading…
Cancel
Save