Browse Source

Double-click metadata row opens its entity in the decompiler

DataGrid.DoubleTapped raises a RowActivated event on the metadata
page model. The dock workspace subscribes during AttachCustomContent
and resolves the row's metadataFile + Token to an IEntity via the
type system, then selects the matching tree node — which triggers
ShowSelectedNode → CreateTab → decompiler view, the same path a
click in the assembly tree takes. Falls through silently for rows
whose token doesn't resolve to an IEntity (heap rows, AssemblyRef,
constants).

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
22651965dd
  1. 85
      ILSpy.Tests/Metadata/MetadataRowActivationTests.cs
  2. 55
      ILSpy/Docking/DockWorkspace.cs
  3. 10
      ILSpy/ViewModels/MetadataTablePageModel.cs
  4. 15
      ILSpy/Views/MetadataTablePage.axaml.cs

85
ILSpy.Tests/Metadata/MetadataRowActivationTests.cs

@ -0,0 +1,85 @@
// 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 System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ILSpy.AppEnv;
using ILSpy.Metadata;
using ILSpy.Metadata.CorTables;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Metadata;
[TestFixture]
public class MetadataRowActivationTests
{
[AvaloniaTest]
public async Task Activating_A_TypeDef_Row_Selects_The_Matching_TypeTreeNode()
{
// Open the TypeDef table for CoreLib, pick the row whose Name == "Object", fire
// RowActivated, and confirm the assembly tree's TypeTreeNode for System.Object
// is now selected — that's the gateway to the decompiler view.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var coreLibName = typeof(object).Assembly.GetName().Name!;
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
metadataNode.EnsureLazyChildren();
var tablesNode = metadataNode.Children.OfType<MetadataTablesTreeNode>().Single();
tablesNode.EnsureLazyChildren();
var typeDefNode = tablesNode.Children.OfType<TypeDefTableTreeNode>().Single();
vm.AssemblyTreeModel.SelectNode(typeDefNode);
var tab = await vm.DockWorkspace.WaitForMetadataTabAsync();
var objectRow = tab.Items.Cast<TypeDefTableTreeNode.TypeDefEntry>()
.First(e => e.Name == "Object" && e.Namespace == "System");
tab.RaiseRowActivated(objectRow);
await Waiters.WaitForAsync(
() => vm.AssemblyTreeModel.SelectedItem is TypeTreeNode tn
&& tn.Member is global::ICSharpCode.Decompiler.TypeSystem.ITypeDefinition td
&& td.FullName == "System.Object");
(((TypeTreeNode)vm.AssemblyTreeModel.SelectedItem!).Member
as global::ICSharpCode.Decompiler.TypeSystem.ITypeDefinition)!
.FullName.Should().Be("System.Object");
}
}
internal static class MetadataRowActivationTestExtensions
{
public static void RaiseRowActivated(this MetadataTablePageModel page, object row) =>
typeof(MetadataTablePageModel)
.GetMethod("RaiseRowActivated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
.Invoke(page, [row]);
}

55
ILSpy/Docking/DockWorkspace.cs

@ -32,6 +32,8 @@ using Dock.Model.Core;
using Dock.Model.Core.Events; using Dock.Model.Core.Events;
using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.TreeView; using ICSharpCode.ILSpyX.TreeView;
using ILSpy.AssemblyTree; using ILSpy.AssemblyTree;
@ -306,11 +308,18 @@ namespace ILSpy.Docking
void AttachCustomContent(ContentTabPage main, TabPageModel newContent) void AttachCustomContent(ContentTabPage main, TabPageModel newContent)
{ {
// Detach navigation handlers from the outgoing content; subscribe on the // Detach navigation handlers from the outgoing content; subscribe on the
// incoming one so token clicks route through OnMetadataCellClicked. // incoming one so token clicks route through OnMetadataCellClicked and
// row activation routes through OnMetadataRowActivated.
if (main.Content is MetadataTablePageModel oldMeta) if (main.Content is MetadataTablePageModel oldMeta)
{
oldMeta.NavigateToCellRequested -= OnMetadataCellClicked; oldMeta.NavigateToCellRequested -= OnMetadataCellClicked;
oldMeta.RowActivated -= OnMetadataRowActivated;
}
if (newContent is MetadataTablePageModel newMeta) if (newContent is MetadataTablePageModel newMeta)
{
newMeta.NavigateToCellRequested += OnMetadataCellClicked; newMeta.NavigateToCellRequested += OnMetadataCellClicked;
newMeta.RowActivated += OnMetadataRowActivated;
}
main.Content = newContent; main.Content = newContent;
} }
@ -328,6 +337,50 @@ namespace ILSpy.Docking
NavigateToToken(new MetadataTokenReference(metadataFile, MetadataTokens.EntityHandle(token))); NavigateToToken(new MetadataTokenReference(metadataFile, MetadataTokens.EntityHandle(token)));
} }
internal void OnMetadataRowActivated(object row)
{
// Row double-click: try to resolve the row's Token + metadataFile to a real
// IEntity via the type system, then select the matching tree node. That
// triggers ShowSelectedNode → CreateTab → decompiler view, the same path a
// click in the assembly tree takes.
var fileField = row.GetType().GetField("metadataFile", BindingFlags.Instance | BindingFlags.NonPublic);
if (fileField?.GetValue(row) is not MetadataFile metadataFile)
return;
var tokenProp = row.GetType().GetProperty("Token");
if (tokenProp?.GetValue(row) is not int token || token == 0)
return;
var handle = MetadataTokens.EntityHandle(token);
if (handle.IsNil)
return;
var assemblies = assemblyTreeModel.AssemblyList?.GetAssemblies();
if (assemblies is null)
return;
LoadedAssembly? owningAssembly = null;
foreach (var a in assemblies)
{
if (ReferenceEquals(a.GetMetadataFileOrNull(), metadataFile))
{
owningAssembly = a;
break;
}
}
if (owningAssembly is null)
return;
var ts = owningAssembly.GetTypeSystemOrNull();
if (ts?.MainModule is not MetadataModule metadataModule)
return;
IEntity? entity;
try
{ entity = metadataModule.ResolveEntity(handle); }
catch { return; }
if (entity is null)
return;
var node = assemblyTreeModel.FindTreeNode(entity);
if (node is not null)
assemblyTreeModel.SelectedItem = node;
}
public void NavigateToToken(MetadataTokenReference reference) public void NavigateToToken(MetadataTokenReference reference)
{ {
if (reference.Handle.IsNil) if (reference.Handle.IsNil)

10
ILSpy/ViewModels/MetadataTablePageModel.cs

@ -103,6 +103,16 @@ namespace ILSpy.ViewModels
internal void RaiseNavigateToCell(object row, string columnName) internal void RaiseNavigateToCell(object row, string columnName)
=> NavigateToCellRequested?.Invoke(new MetadataCellNavigationEventArgs(row, columnName)); => NavigateToCellRequested?.Invoke(new MetadataCellNavigationEventArgs(row, columnName));
/// <summary>
/// Raised when the user double-clicks (or otherwise activates) a metadata grid
/// row. The dock workspace resolves the row's <c>metadataFile</c> + <c>Token</c>
/// to an <see cref="ICSharpCode.Decompiler.TypeSystem.IEntity"/> and selects the
/// matching tree node, which opens the entity in the decompiler view.
/// </summary>
public event Action<object>? RowActivated;
internal void RaiseRowActivated(object row) => RowActivated?.Invoke(row);
static readonly ConcurrentDictionary<(Type Type, string Column), PropertyInfo?> propertyLookupCache = new(); static readonly ConcurrentDictionary<(Type Type, string Column), PropertyInfo?> propertyLookupCache = new();
static readonly ConcurrentDictionary<string, Regex?> regexCache = new(); static readonly ConcurrentDictionary<string, Regex?> regexCache = new();

15
ILSpy/Views/MetadataTablePage.axaml.cs

@ -57,6 +57,21 @@ namespace ILSpy.Views
AddHandler(PointerMovedEvent, OnPointerMovedOverGrid); AddHandler(PointerMovedEvent, OnPointerMovedOverGrid);
AddHandler(KeyDownEvent, OnKeyDown); AddHandler(KeyDownEvent, OnKeyDown);
AttachContextMenu(TryGetContextMenuEntries()); AttachContextMenu(TryGetContextMenuEntries());
var grid = this.FindControl<DataGrid>("Grid");
if (grid is not null)
grid.DoubleTapped += OnGridDoubleTapped;
}
void OnGridDoubleTapped(object? sender, global::Avalonia.Input.TappedEventArgs e)
{
// Dispatch row-activation through the page model. The dock workspace's
// subscriber resolves the row's metadataFile + Token to an IEntity and selects
// the matching tree node; that opens the entity in the decompiler view.
if (DataContext is not MetadataTablePageModel page)
return;
var grid = this.FindControl<DataGrid>("Grid");
if (grid?.SelectedItem is { } row)
page.RaiseRowActivated(row);
} }
void OnKeyDown(object? sender, KeyEventArgs e) void OnKeyDown(object? sender, KeyEventArgs e)

Loading…
Cancel
Save