diff --git a/ILSpy.Tests/Compare/CompareViewRenderTests.cs b/ILSpy.Tests/Compare/CompareViewRenderTests.cs
new file mode 100644
index 000000000..0fd3e06ea
--- /dev/null
+++ b/ILSpy.Tests/Compare/CompareViewRenderTests.cs
@@ -0,0 +1,109 @@
+// 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.Controls;
+using Avalonia.Headless.NUnit;
+using Avalonia.VisualTree;
+
+using AwesomeAssertions;
+
+using ICSharpCode.ILSpyX.TreeView;
+
+using ILSpy;
+using ILSpy.AppEnv;
+using ILSpy.Commands;
+using ILSpy.Compare;
+using ILSpy.TreeNodes;
+using ILSpy.ViewModels;
+using ILSpy.Views;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Compare;
+
+///
+/// End-to-end render checks for the assembly-comparison view. Pure context-menu and engine
+/// tests don't catch the case where the new tab opens but renders empty because the
+/// hierarchical DataGrid was never wired with a model — these tests mount the view, walk
+/// the visual tree, and assert rows materialise.
+///
+[TestFixture]
+public class CompareViewRenderTests
+{
+ [AvaloniaTest]
+ public async Task CompareView_Binds_A_HierarchicalModel_When_Opened()
+ {
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2);
+
+ var entry = AppComposition.Current.GetExport()
+ .Entries.Single(e => e.Metadata.Header == "Compare...").Value;
+ var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies()
+ .Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList();
+ var nodes = assemblies.Select(a =>
+ (SharpTreeNode)vm.AssemblyTreeModel.FindNode(a.ShortName)).ToArray();
+
+ entry.Execute(new TextViewContext { SelectedTreeNodes = nodes });
+
+ var view = await window.WaitForComponent();
+ var grid = await view.WaitForComponent();
+
+ await Waiters.WaitForAsync(() => grid.HierarchicalModel != null,
+ description: "CompareView must populate the DataGrid's HierarchicalModel — without it "
+ + "the hierarchical column is rendered over an empty source and the tab shows nothing");
+ grid.HierarchicalModel.Should().NotBeNull();
+ }
+
+ [AvaloniaTest]
+ public async Task CompareView_Renders_Rows_For_The_Merged_Tree()
+ {
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2);
+
+ var entry = AppComposition.Current.GetExport()
+ .Entries.Single(e => e.Metadata.Header == "Compare...").Value;
+ var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies()
+ .Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList();
+ var nodes = assemblies.Select(a =>
+ (SharpTreeNode)vm.AssemblyTreeModel.FindNode(a.ShortName)).ToArray();
+
+ entry.Execute(new TextViewContext { SelectedTreeNodes = nodes });
+
+ var view = await window.WaitForComponent();
+ var grid = await view.WaitForComponent();
+
+ // Default state: ShowIdentical=false, so we expect at least one diff row to be present
+ // when comparing two non-empty assemblies against themselves *or* against a sibling.
+ // (Comparing an assembly with itself yields zero diffs; if the two assemblies are the
+ // same instance we toggle ShowIdentical to force the tree to surface rows.)
+ var page = (CompareTabPageModel)view.DataContext!;
+ if (ReferenceEquals(page.LeftAssembly, page.RightAssembly))
+ page.ShowIdentical = true;
+
+ await Waiters.WaitForAsync(
+ () => grid.GetVisualDescendants().OfType().Any(),
+ description: "the diff grid must render at least one row once the merged tree is bound");
+ }
+}
diff --git a/ILSpy/Views/CompareView.axaml b/ILSpy/Views/CompareView.axaml
index c67d927f4..7c48d6a77 100644
--- a/ILSpy/Views/CompareView.axaml
+++ b/ILSpy/Views/CompareView.axaml
@@ -22,8 +22,7 @@
HierarchicalRowsEnabled="True"
GridLinesVisibility="None"
CanUserResizeColumns="False"
- SelectionMode="Single"
- ItemsSource="{Binding RootEntry.Children}">
+ SelectionMode="Single">
{
+ ChildrenSelector = node => {
+ if (node is TreeNodes.ILSpyTreeNode ilspy)
+ ilspy.EnsureChildrenFiltered();
+ else
+ node.EnsureLazyChildren();
+ return node.Children;
+ },
+ IsLeafSelector = node => !node.ShowExpander,
+ VirtualizeChildren = false,
+ IsExpandedPropertyPath = nameof(SharpTreeNode.IsExpanded),
+ };
+ var model = new HierarchicalModel(options);
+ model.SetRoots(root.Children);
+ DiffGrid.HierarchicalModel = model;
+ }
}
}
diff --git a/ILSpy/Views/ContentTabPageView.axaml b/ILSpy/Views/ContentTabPageView.axaml
index 323f7075e..ce1b06dda 100644
--- a/ILSpy/Views/ContentTabPageView.axaml
+++ b/ILSpy/Views/ContentTabPageView.axaml
@@ -6,6 +6,7 @@
xmlns:textView="using:ILSpy.TextView"
xmlns:metaViews="using:ILSpy.Views"
xmlns:options="using:ILSpy.Options"
+ xmlns:compare="using:ILSpy.Compare"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
x:Class="ILSpy.Views.ContentTabPageView"
x:DataType="vm:ContentTabPage">
@@ -19,6 +20,7 @@
+
diff --git a/ILSpy/Views/ContentTabPageView.axaml.cs b/ILSpy/Views/ContentTabPageView.axaml.cs
index e5a544cde..8a010ad74 100644
--- a/ILSpy/Views/ContentTabPageView.axaml.cs
+++ b/ILSpy/Views/ContentTabPageView.axaml.cs
@@ -21,6 +21,7 @@ using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using ILSpy.Compare;
using ILSpy.Options;
using ILSpy.TextView;
using ILSpy.ViewModels;
@@ -39,6 +40,7 @@ namespace ILSpy.Views
DecompilerTextView decompilerView = null!;
MetadataTablePage metadataView = null!;
OptionsPageView optionsView = null!;
+ CompareView compareView = null!;
public ContentTabPageView()
{
@@ -46,6 +48,7 @@ namespace ILSpy.Views
decompilerView = this.FindControl("DecompilerView")!;
metadataView = this.FindControl("MetadataView")!;
optionsView = this.FindControl("OptionsView")!;
+ compareView = this.FindControl("CompareViewSlot")!;
DataContextChanged += (_, _) => RebindPage();
}
@@ -103,6 +106,17 @@ namespace ILSpy.Views
optionsView.IsVisible = false;
optionsView.DataContext = null;
}
+
+ if (content is CompareTabPageModel compare)
+ {
+ compareView.DataContext = compare;
+ compareView.IsVisible = true;
+ }
+ else
+ {
+ compareView.IsVisible = false;
+ compareView.DataContext = null;
+ }
}
}
}