diff --git a/ILSpy.Tests/Metadata/MetadataCellTooltipTests.cs b/ILSpy.Tests/Metadata/MetadataCellTooltipTests.cs
new file mode 100644
index 000000000..ccca5724c
--- /dev/null
+++ b/ILSpy.Tests/Metadata/MetadataCellTooltipTests.cs
@@ -0,0 +1,78 @@
+// 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 AwesomeAssertions;
+
+using ILSpy.Metadata;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Metadata;
+
+[TestFixture]
+public class MetadataCellTooltipTests
+{
+ sealed class EntryWithTooltips
+ {
+ public string Name { get; set; } = "Foo";
+ public string NameTooltip => $"heap-offset for \"{Name}\"";
+
+ public int Flags { get; set; } = 0x42;
+ // Non-string tooltip values flow through ToString() — the helper just normalises
+ // whatever the entry exposes into a string, leaving styled-tooltip rendering for
+ // a later phase.
+ public object FlagsTooltip => $"Flags=0x{Flags:X4}";
+ }
+
+ sealed class EntryWithoutTooltips
+ {
+ public string Name { get; set; } = "Bar";
+ }
+
+ [Test]
+ public void Resolve_Returns_String_Tooltip_When_Property_Is_Present()
+ {
+ // The convention is `{columnName}Tooltip` — looked up by reflection on the bound row.
+ var entry = new EntryWithTooltips { Name = "System.Object" };
+ MetadataCellTooltip.Resolve(entry, "Name").Should().Be("heap-offset for \"System.Object\"");
+ }
+
+ [Test]
+ public void Resolve_Returns_Null_When_The_Tooltip_Property_Is_Missing()
+ {
+ // Many entry types expose tooltips for only a subset of columns; missing means "no
+ // tooltip", not "throw".
+ var entry = new EntryWithoutTooltips();
+ MetadataCellTooltip.Resolve(entry, "Name").Should().BeNull();
+ }
+
+ [Test]
+ public void Resolve_Stringifies_Non_String_Tooltip_Values()
+ {
+ // Some WPF entries return rich FlagsTooltip objects; stringification is the safe
+ // fallback so the helper still produces something usable.
+ var entry = new EntryWithTooltips { Flags = 0x1234 };
+ MetadataCellTooltip.Resolve(entry, "Flags").Should().Be("Flags=0x1234");
+ }
+
+ [Test]
+ public void Resolve_Returns_Null_When_The_Item_Is_Null()
+ {
+ MetadataCellTooltip.Resolve(null!, "Name").Should().BeNull();
+ }
+}
diff --git a/ILSpy/Metadata/MetadataCellTooltip.cs b/ILSpy/Metadata/MetadataCellTooltip.cs
new file mode 100644
index 000000000..3caf4fa5a
--- /dev/null
+++ b/ILSpy/Metadata/MetadataCellTooltip.cs
@@ -0,0 +1,57 @@
+// 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;
+using System.Collections.Concurrent;
+using System.Reflection;
+
+namespace ILSpy.Metadata
+{
+ ///
+ /// Resolves per-cell tooltips for a metadata-table row. Entry classes opt in by exposing
+ /// a public {ColumnName}Tooltip property — NameTooltip, FlagsTooltip,
+ /// BaseTypeTooltip, and so on. The resolver returns a stringified rendering, so
+ /// callers can flow it straight into ToolTip.SetTip.
+ ///
+ public static class MetadataCellTooltip
+ {
+ static readonly ConcurrentDictionary<(Type Type, string Column), PropertyInfo?> propertyCache = new();
+
+ ///
+ /// Looks up Tooltip on 's
+ /// runtime type. Returns the property's stringified value, or
+ /// if the item is null, the tooltip property is absent, or the value is null / blank.
+ ///
+ public static string? Resolve(object item, string columnName)
+ {
+ if (item is null)
+ return null;
+ var prop = propertyCache.GetOrAdd((item.GetType(), columnName),
+ static key => key.Type.GetProperty(key.Column + "Tooltip",
+ BindingFlags.Public | BindingFlags.Instance));
+ if (prop is null)
+ return null;
+ object? value;
+ try
+ { value = prop.GetValue(item); }
+ catch { return null; }
+ var s = value?.ToString();
+ return string.IsNullOrWhiteSpace(s) ? null : s;
+ }
+ }
+}
diff --git a/ILSpy/Views/MetadataTablePage.axaml.cs b/ILSpy/Views/MetadataTablePage.axaml.cs
index 9aeb33c33..537f90372 100644
--- a/ILSpy/Views/MetadataTablePage.axaml.cs
+++ b/ILSpy/Views/MetadataTablePage.axaml.cs
@@ -19,10 +19,14 @@
using System;
using System.ComponentModel;
+using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Markup.Xaml;
+using Avalonia.VisualTree;
+using ILSpy.Metadata;
using ILSpy.ViewModels;
namespace ILSpy.Views
@@ -37,15 +41,38 @@ namespace ILSpy.Views
{
MetadataTablePageModel? boundModel;
DataGridCollectionView? itemsView;
+ DataGridCell? hoveredCell;
public MetadataTablePage()
{
InitializeComponent();
DataContextChanged += (_, _) => RebindModel();
+ AddHandler(PointerMovedEvent, OnPointerMovedOverGrid);
}
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
+ void OnPointerMovedOverGrid(object? sender, PointerEventArgs e)
+ {
+ // Walk up the visual tree from the pointer's source to the cell. Cells nest
+ // arbitrary content (TextBlock for plain columns, Button for token columns), so
+ // a per-cell PointerEntered subscription would miss most events.
+ var visual = e.Source as Visual;
+ while (visual is not null and not DataGridCell)
+ visual = visual.GetVisualParent();
+ var cell = visual as DataGridCell;
+ if (cell == hoveredCell)
+ return;
+ hoveredCell = cell;
+ if (cell is null)
+ return;
+ var columnName = cell.OwningColumn?.Header?.ToString();
+ if (columnName is null)
+ return;
+ var tip = MetadataCellTooltip.Resolve(cell.DataContext!, columnName);
+ ToolTip.SetTip(cell, tip);
+ }
+
void RebindModel()
{
if (boundModel != null)