Browse Source

Move metadata-grid resolution into MetadataNavigator

DockWorkspace carried the reflection-heavy metadata-grid resolution: read a
clicked token cell, resolve a double-clicked row to its tree node, and find
the metadata-table node a token points at -- with the "read the row's
metadataFile private field + int token" reflection block copy-pasted between
the cell-click and row-activation paths. As the WPF host kept this metadata
logic out of the main workspace, lift it into a MetadataNavigator service.

DockWorkspace keeps the dock-side actions (select, scroll-to-row, history
stamping, open-in-new-tab) and now delegates resolution; the public
NavigateToToken / TryResolveRowToTreeNode surface is unchanged, so the
external callers (ShowInMetadata, DecompileMetadataRowInNewTab) don't move.
The duplicated reflection collapses into one TryReadRow.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3755/head
Siegfried Pammer 4 weeks ago
parent
commit
3eb090bfa4
  1. 103
      ILSpy/Docking/DockWorkspace.cs
  2. 132
      ILSpy/Metadata/MetadataNavigator.cs

103
ILSpy/Docking/DockWorkspace.cs

@ -68,6 +68,7 @@ namespace ILSpy.Docking @@ -68,6 +68,7 @@ namespace ILSpy.Docking
readonly ILSpyDockFactory factory;
readonly AssemblyTreeModel assemblyTreeModel;
readonly LanguageService languageService;
readonly Metadata.MetadataNavigator metadataNavigator;
readonly NavigationHistory<NavigationEntry> history = new();
// Set true while a Back/Forward navigation is rewriting state (SelectedItem,
// active dockable) so the change notifications don't push fresh entries onto the
@ -160,11 +161,13 @@ namespace ILSpy.Docking @@ -160,11 +161,13 @@ namespace ILSpy.Docking
public DockWorkspace(
AssemblyTreeModel assemblyTreeModel,
ToolPaneRegistry toolPaneRegistry,
LanguageService languageService)
LanguageService languageService,
Metadata.MetadataNavigator metadataNavigator)
{
ILSpy.AppEnv.AppLog.Mark("DockWorkspace ctor entered");
this.assemblyTreeModel = assemblyTreeModel;
this.languageService = languageService;
this.metadataNavigator = metadataNavigator;
NavigateBackCommand = new RelayCommand(NavigateBack, () => history.CanNavigateBack);
NavigateForwardCommand = new RelayCommand(NavigateForward, () => history.CanNavigateForward);
NavigateToHistoryCommand = new RelayCommand<NavigationEntry>(NavigateToHistory,
@ -853,24 +856,16 @@ namespace ILSpy.Docking @@ -853,24 +856,16 @@ namespace ILSpy.Docking
void OnMetadataCellClicked(MetadataCellNavigationEventArgs e)
{
// (row, columnName) → metadata token + module → matching table tree node + row index.
// Resolved via reflection because each entry struct buries its MetadataFile in a
// private readonly field with the same name across all 43 typed table viewmodels.
var prop = e.Row.GetType().GetProperty(e.ColumnName);
if (prop?.GetValue(e.Row) is not int token || token == 0)
return;
var fileField = e.Row.GetType().GetField("metadataFile", BindingFlags.Instance | BindingFlags.NonPublic);
if (fileField?.GetValue(e.Row) is not MetadataFile metadataFile)
return;
NavigateToToken(new MetadataTokenReference(metadataFile, MetadataTokens.EntityHandle(token)));
// A clicked token cell -> the metadata table + row the token points at.
if (metadataNavigator.ReadCellToken(e.Row, e.ColumnName) is { } reference)
NavigateToToken(reference);
}
internal void OnMetadataRowActivated(MetadataRowActivationEventArgs e)
{
// Row double-click: resolve the row's Token + metadataFile to an IEntity, then
// either select the matching tree node (single-tab reuse) or open it in a fresh
// tab (MMB / context-menu carve-out).
var node = TryResolveRowToTreeNode(e.Row);
// Row double-click: resolve the row to its tree node, then either select it (single-tab
// reuse) or open it in a fresh tab (MMB / context-menu carve-out).
var node = metadataNavigator.ResolveRowToTreeNode(e.Row);
if (node is null)
return;
if (e.OpenInNewTab)
@ -912,86 +907,18 @@ namespace ILSpy.Docking @@ -912,86 +907,18 @@ namespace ILSpy.Docking
}
/// <summary>
/// Reflection-based resolution of a metadata-grid row to the matching assembly-tree
/// node, sharing the path between row double-click and the "Decompile to new tab"
/// context-menu entry. Returns <see langword="null"/> when the row's token doesn't
/// resolve to an <see cref="IEntity"/> the tree knows how to model (heap rows,
/// AssemblyRef rows pointing at unloaded assemblies, etc.).
/// Resolves a metadata-grid row to the matching assembly-tree node, shared between row
/// double-click and the "Decompile to new tab" context-menu entry. Returns
/// <see langword="null"/> when the row's token doesn't resolve to an entity the tree models.
/// </summary>
internal ILSpyTreeNode? TryResolveRowToTreeNode(object row)
{
var fileField = row.GetType().GetField("metadataFile", BindingFlags.Instance | BindingFlags.NonPublic);
if (fileField?.GetValue(row) is not MetadataFile metadataFile)
return null;
var tokenProp = row.GetType().GetProperty("Token");
if (tokenProp?.GetValue(row) is not int token || token == 0)
return null;
var handle = MetadataTokens.EntityHandle(token);
if (handle.IsNil)
return null;
var assemblies = assemblyTreeModel.AssemblyList?.GetAssemblies();
if (assemblies is null)
return null;
LoadedAssembly? owningAssembly = null;
foreach (var a in assemblies)
{
if (ReferenceEquals(a.GetMetadataFileOrNull(), metadataFile))
{
owningAssembly = a;
break;
}
}
if (owningAssembly is null)
return null;
var ts = owningAssembly.GetTypeSystemOrNull();
if (ts?.MainModule is not MetadataModule metadataModule)
return null;
IEntity? entity;
try
{ entity = metadataModule.ResolveEntity(handle); }
catch { return null; }
if (entity is null)
return null;
return assemblyTreeModel.FindTreeNode(entity);
}
=> metadataNavigator.ResolveRowToTreeNode(row);
public void NavigateToToken(MetadataTokenReference reference)
{
if (reference.Handle.IsNil)
return;
// Find the MetadataTreeNode for the referenced module, drill down to its
// Tables container, and pick the table whose Kind matches the handle's runtime
// kind — HandleKind values for table-backed entities double as TableIndex.
var assemblies = assemblyTreeModel.AssemblyList?.GetAssemblies();
if (assemblies == null)
return;
AssemblyTreeNode? targetAssembly = null;
if (assemblyTreeModel.Root != null)
{
foreach (var child in assemblyTreeModel.Root.Children.OfType<AssemblyTreeNode>())
{
if (ReferenceEquals(child.LoadedAssembly.GetMetadataFileOrNull(), reference.MetadataFile))
{
targetAssembly = child;
break;
}
}
}
if (targetAssembly == null)
return;
targetAssembly.EnsureLazyChildren();
var metaNode = targetAssembly.Children.OfType<MetadataTreeNode>().FirstOrDefault();
if (metaNode == null)
return;
metaNode.EnsureLazyChildren();
var tablesNode = metaNode.Children.OfType<MetadataTablesTreeNode>().FirstOrDefault();
if (tablesNode == null)
return;
tablesNode.EnsureLazyChildren();
var tableIndex = (TableIndex)(int)reference.Handle.Kind;
var tableNode = tablesNode.Children.OfType<MetadataTableTreeNode>()
.FirstOrDefault(t => t.Kind == tableIndex);
var tableNode = metadataNavigator.FindTableNode(reference);
if (tableNode == null)
return;

132
ILSpy/Metadata/MetadataNavigator.cs

@ -0,0 +1,132 @@ @@ -0,0 +1,132 @@
// 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.Composition;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
using ILSpy.AssemblyTree;
using ILSpy.TreeNodes;
namespace ILSpy.Metadata
{
/// <summary>
/// Resolves metadata-grid interactions (a clicked token cell, a double-clicked row, a token
/// reference) to the matching assembly-tree node or metadata-table node. Split out of
/// <c>DockWorkspace</c> -- which keeps only the dock-side actions (select, scroll, history,
/// open-in-new-tab) -- so the reflection-heavy "read a typed metadata row" knowledge lives in
/// one place. Each typed table viewmodel buries its <see cref="MetadataFile"/> in a private
/// readonly field of the same name and exposes the row token as an int property, so both are
/// read by reflection.
/// </summary>
[Export]
[Shared]
public sealed class MetadataNavigator
{
readonly AssemblyTreeModel assemblyTreeModel;
[ImportingConstructor]
public MetadataNavigator(AssemblyTreeModel assemblyTreeModel)
{
this.assemblyTreeModel = assemblyTreeModel;
}
/// <summary>
/// Reads the token in the <paramref name="columnName"/> cell of <paramref name="row"/> as a
/// reference, or null when the cell holds no (non-zero) token.
/// </summary>
public MetadataTokenReference? ReadCellToken(object row, string columnName)
=> TryReadRow(row, columnName, out var file, out var handle)
? new MetadataTokenReference(file!, handle)
: null;
/// <summary>
/// Resolves a double-clicked metadata row (its <c>Token</c> column + owning module) to the
/// matching assembly-tree node, or null when the token doesn't resolve to an
/// <see cref="IEntity"/> the tree models (heap rows, refs into unloaded assemblies, ...).
/// </summary>
public ILSpyTreeNode? ResolveRowToTreeNode(object row)
{
if (!TryReadRow(row, "Token", out var file, out var handle) || handle.IsNil)
return null;
var owningAssembly = assemblyTreeModel.AssemblyList?.GetAssemblies()
.FirstOrDefault(a => ReferenceEquals(a.GetMetadataFileOrNull(), file));
if (owningAssembly is null)
return null;
if (owningAssembly.GetTypeSystemOrNull()?.MainModule is not MetadataModule metadataModule)
return null;
IEntity? entity;
try
{ entity = metadataModule.ResolveEntity(handle); }
catch
{ return null; }
return entity is null ? null : assemblyTreeModel.FindTreeNode(entity);
}
/// <summary>
/// Finds the metadata-table tree node a token <paramref name="reference"/> points at, drilling
/// the referenced module's Metadata -> Tables container and matching the handle's kind (which
/// doubles as <see cref="TableIndex"/> for table-backed entities). Null for heap handles or an
/// unloaded module.
/// </summary>
public MetadataTableTreeNode? FindTableNode(MetadataTokenReference reference)
{
var targetAssembly = assemblyTreeModel.Root?.Children
.OfType<AssemblyTreeNode>()
.FirstOrDefault(a => ReferenceEquals(a.LoadedAssembly.GetMetadataFileOrNull(), reference.MetadataFile));
if (targetAssembly == null)
return null;
targetAssembly.EnsureLazyChildren();
var metaNode = targetAssembly.Children.OfType<MetadataTreeNode>().FirstOrDefault();
if (metaNode == null)
return null;
metaNode.EnsureLazyChildren();
var tablesNode = metaNode.Children.OfType<MetadataTablesTreeNode>().FirstOrDefault();
if (tablesNode == null)
return null;
tablesNode.EnsureLazyChildren();
var tableIndex = (TableIndex)(int)reference.Handle.Kind;
return tablesNode.Children.OfType<MetadataTableTreeNode>()
.FirstOrDefault(t => t.Kind == tableIndex);
}
// Each typed metadata-row viewmodel buries its MetadataFile in a private readonly field of the
// same name across all ~43 table viewmodels and exposes the row token as an int property;
// resolve both by reflection. token == 0 means "no token" (e.g. a Nil parent reference).
static bool TryReadRow(object row, string tokenProperty, out MetadataFile? file, out EntityHandle handle)
{
file = null;
handle = default;
var fileField = row.GetType().GetField("metadataFile", BindingFlags.Instance | BindingFlags.NonPublic);
if (fileField?.GetValue(row) is not MetadataFile metadataFile)
return false;
if (row.GetType().GetProperty(tokenProperty)?.GetValue(row) is not int token || token == 0)
return false;
file = metadataFile;
handle = MetadataTokens.EntityHandle(token);
return true;
}
}
}
Loading…
Cancel
Save