From 3eb090bfa47ce9eb4bd2b15f300692030b116429 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 9 Jun 2026 19:40:46 +0200 Subject: [PATCH] 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 --- ILSpy/Docking/DockWorkspace.cs | 103 ++++------------------ ILSpy/Metadata/MetadataNavigator.cs | 132 ++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 88 deletions(-) create mode 100644 ILSpy/Metadata/MetadataNavigator.cs diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index f903b4968..843abb60b 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -68,6 +68,7 @@ namespace ILSpy.Docking readonly ILSpyDockFactory factory; readonly AssemblyTreeModel assemblyTreeModel; readonly LanguageService languageService; + readonly Metadata.MetadataNavigator metadataNavigator; readonly NavigationHistory 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 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(NavigateToHistory, @@ -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 } /// - /// 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 when the row's token doesn't - /// resolve to an 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 + /// when the row's token doesn't resolve to an entity the tree models. /// 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()) - { - if (ReferenceEquals(child.LoadedAssembly.GetMetadataFileOrNull(), reference.MetadataFile)) - { - targetAssembly = child; - break; - } - } - } - if (targetAssembly == null) - return; - targetAssembly.EnsureLazyChildren(); - var metaNode = targetAssembly.Children.OfType().FirstOrDefault(); - if (metaNode == null) - return; - metaNode.EnsureLazyChildren(); - var tablesNode = metaNode.Children.OfType().FirstOrDefault(); - if (tablesNode == null) - return; - tablesNode.EnsureLazyChildren(); - var tableIndex = (TableIndex)(int)reference.Handle.Kind; - var tableNode = tablesNode.Children.OfType() - .FirstOrDefault(t => t.Kind == tableIndex); + var tableNode = metadataNavigator.FindTableNode(reference); if (tableNode == null) return; diff --git a/ILSpy/Metadata/MetadataNavigator.cs b/ILSpy/Metadata/MetadataNavigator.cs new file mode 100644 index 000000000..03721ee3a --- /dev/null +++ b/ILSpy/Metadata/MetadataNavigator.cs @@ -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 +{ + /// + /// 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 + /// DockWorkspace -- 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 in a private + /// readonly field of the same name and exposes the row token as an int property, so both are + /// read by reflection. + /// + [Export] + [Shared] + public sealed class MetadataNavigator + { + readonly AssemblyTreeModel assemblyTreeModel; + + [ImportingConstructor] + public MetadataNavigator(AssemblyTreeModel assemblyTreeModel) + { + this.assemblyTreeModel = assemblyTreeModel; + } + + /// + /// Reads the token in the cell of as a + /// reference, or null when the cell holds no (non-zero) token. + /// + public MetadataTokenReference? ReadCellToken(object row, string columnName) + => TryReadRow(row, columnName, out var file, out var handle) + ? new MetadataTokenReference(file!, handle) + : null; + + /// + /// Resolves a double-clicked metadata row (its Token column + owning module) to the + /// matching assembly-tree node, or null when the token doesn't resolve to an + /// the tree models (heap rows, refs into unloaded assemblies, ...). + /// + 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); + } + + /// + /// Finds the metadata-table tree node a token points at, drilling + /// the referenced module's Metadata -> Tables container and matching the handle's kind (which + /// doubles as for table-backed entities). Null for heap handles or an + /// unloaded module. + /// + public MetadataTableTreeNode? FindTableNode(MetadataTokenReference reference) + { + var targetAssembly = assemblyTreeModel.Root?.Children + .OfType() + .FirstOrDefault(a => ReferenceEquals(a.LoadedAssembly.GetMetadataFileOrNull(), reference.MetadataFile)); + if (targetAssembly == null) + return null; + targetAssembly.EnsureLazyChildren(); + var metaNode = targetAssembly.Children.OfType().FirstOrDefault(); + if (metaNode == null) + return null; + metaNode.EnsureLazyChildren(); + var tablesNode = metaNode.Children.OfType().FirstOrDefault(); + if (tablesNode == null) + return null; + tablesNode.EnsureLazyChildren(); + var tableIndex = (TableIndex)(int)reference.Handle.Kind; + return tablesNode.Children.OfType() + .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; + } + } +}