From 4de7cfe174539d7fe72f133032aa4e4abffc53a9 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 13 May 2026 17:37:40 +0200 Subject: [PATCH] Analyzer pane view-model and ProDataGrid view Replace the placeholder stub with the live pane. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/Analyzers/AnalyzerTreeView.axaml | 33 ++++++- ILSpy/Analyzers/AnalyzerTreeView.axaml.cs | 109 ++++++++++++++++++++++ ILSpy/Analyzers/AnalyzerTreeViewModel.cs | 72 ++++++++++++++ 3 files changed, 211 insertions(+), 3 deletions(-) diff --git a/ILSpy/Analyzers/AnalyzerTreeView.axaml b/ILSpy/Analyzers/AnalyzerTreeView.axaml index 62dce9e1c..37c9a2c6e 100644 --- a/ILSpy/Analyzers/AnalyzerTreeView.axaml +++ b/ILSpy/Analyzers/AnalyzerTreeView.axaml @@ -6,7 +6,34 @@ mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="200" x:Class="ILSpy.Analyzers.AnalyzerTreeView" x:DataType="analyzers:AnalyzerTreeViewModel"> - + + + + + + + + + + + + + + diff --git a/ILSpy/Analyzers/AnalyzerTreeView.axaml.cs b/ILSpy/Analyzers/AnalyzerTreeView.axaml.cs index 74f32f7fc..2e9b6dfa6 100644 --- a/ILSpy/Analyzers/AnalyzerTreeView.axaml.cs +++ b/ILSpy/Analyzers/AnalyzerTreeView.axaml.cs @@ -16,15 +16,124 @@ // 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.Generic; +using System.Collections.Specialized; +using System.Linq; + using Avalonia.Controls; +using Avalonia.Controls.DataGridHierarchical; + +using ICSharpCode.ILSpyX.TreeView; namespace ILSpy.Analyzers { public partial class AnalyzerTreeView : UserControl { + bool syncingSelection; + AnalyzerTreeViewModel? boundModel; + public AnalyzerTreeView() { InitializeComponent(); } + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + DetachFromModel(); + if (DataContext is AnalyzerTreeViewModel model) + AttachToModel(model); + } + + void AttachToModel(AnalyzerTreeViewModel model) + { + boundModel = model; + BindTree(model.Root); + model.SelectedItems.CollectionChanged += OnModelSelectionChanged; + } + + void DetachFromModel() + { + if (boundModel == null) + return; + boundModel.SelectedItems.CollectionChanged -= OnModelSelectionChanged; + boundModel = null; + } + + void BindTree(SharpTreeNode root) + { + var options = new HierarchicalOptions { + ChildrenSelector = node => { + node.EnsureLazyChildren(); + return node.Children; + }, + IsLeafSelector = node => !node.ShowExpander, + VirtualizeChildren = false, + // Two-way sync of SharpTreeNode.IsExpanded with the row chevron. + IsExpandedPropertyPath = nameof(SharpTreeNode.IsExpanded), + }; + var hierarchicalModel = new HierarchicalModel(options); + hierarchicalModel.SetRoots(root.Children); + TreeGrid.HierarchicalModel = hierarchicalModel; + } + + void OnTreeGridSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (syncingSelection || boundModel == null) + return; + syncingSelection = true; + try + { + var current = new HashSet(); + foreach (var item in TreeGrid.SelectedItems) + { + var node = item is HierarchicalNode hn && hn.Item is SharpTreeNode t ? t + : item as SharpTreeNode; + if (node != null) + current.Add(node); + } + for (int i = boundModel.SelectedItems.Count - 1; i >= 0; i--) + { + if (!current.Contains(boundModel.SelectedItems[i])) + boundModel.SelectedItems.RemoveAt(i); + } + foreach (var node in current) + { + if (!boundModel.SelectedItems.Contains(node)) + boundModel.SelectedItems.Add(node); + } + } + finally + { + syncingSelection = false; + } + } + + void OnModelSelectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (syncingSelection || boundModel == null) + return; + syncingSelection = true; + try + { + var current = TreeGrid.SelectedItems.OfType().ToList(); + foreach (var item in current) + TreeGrid.SelectedItems.Remove(item); + if (TreeGrid.HierarchicalModel is IHierarchicalModel model) + { + foreach (var node in boundModel.SelectedItems) + { + var hNode = model.FindNode(node); + if (hNode != null) + TreeGrid.SelectedItems.Add(hNode); + } + } + } + finally + { + syncingSelection = false; + } + } } } diff --git a/ILSpy/Analyzers/AnalyzerTreeViewModel.cs b/ILSpy/Analyzers/AnalyzerTreeViewModel.cs index 5b7b198bd..77dc130b2 100644 --- a/ILSpy/Analyzers/AnalyzerTreeViewModel.cs +++ b/ILSpy/Analyzers/AnalyzerTreeViewModel.cs @@ -16,8 +16,15 @@ // 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.ObjectModel; using System.Composition; +using System.Linq; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.ILSpyX.TreeView; + +using ILSpy.Analyzers.TreeNodes; using ILSpy.Commands; using ILSpy.ViewModels; @@ -34,6 +41,71 @@ namespace ILSpy.Analyzers { Id = PaneContentId; Title = "Analyzer"; + // IsRoot is computed (Parent == null) — the node is the root because we never + // add it to another node's Children collection. + Root = new AnalyzerRootNode(); + } + + /// + /// Root of the analyzer pane's tree. Pre-populated as a child-less node; entries are + /// added through . + /// + public AnalyzerRootNode Root { get; } + + /// + /// Two-way binding sink for the tree grid's selection. Drives the context-menu's + /// TextViewContext.SelectedTreeNodes so menu entries see the live selection. + /// + public ObservableCollection SelectedItems { get; } = new(); + + /// + /// Adds to the analyzer pane (reusing the existing row if + /// the entity is already analysed) and selects it. The pane's focus / dock-activation + /// is the caller's responsibility — typically the context-menu entry that triggered + /// the analysis. + /// + public AnalyzerEntityTreeNode Analyze(IEntity entity) + { + ArgumentNullException.ThrowIfNull(entity); + var existing = Root.Children + .OfType() + .FirstOrDefault(n => IsSameEntity(n.Member, entity)); + if (existing != null) + { + SyncSelection(existing); + return existing; + } + var node = Wrap(entity); + Root.Children.Add(node); + SyncSelection(node); + return node; + } + + static bool IsSameEntity(IEntity? a, IEntity b) + { + if (a == null) + return false; + return a.MetadataToken == b.MetadataToken + && ReferenceEquals(a.ParentModule, b.ParentModule); + } + + void SyncSelection(SharpTreeNode node) + { + SelectedItems.Clear(); + SelectedItems.Add(node); + } + + static AnalyzerEntityTreeNode Wrap(IEntity entity) + { + return entity switch { + ITypeDefinition type => new AnalyzedTypeTreeNode(type, source: entity), + IMethod method => new AnalyzedMethodTreeNode(method, source: entity), + IField field => new AnalyzedFieldTreeNode(field, source: entity), + IProperty property => new AnalyzedPropertyTreeNode(property, source: entity), + IEvent ev => new AnalyzedEventTreeNode(ev, source: entity), + _ => throw new ArgumentOutOfRangeException(nameof(entity), + $"Entity {entity.GetType().FullName} is not supported by the analyzer pane.") + }; } } }