Browse Source

Unify inner tab content under a single ContentPageModel base

The document tabs had two parallel hierarchies: the ContentTabPage dockable wrapper, and four unrelated content viewmodels in its object? Content slot -- three TabPageModel subclasses plus OptionsPageModel, which derived from a bare ObservableObject. The wrapper bridged the gap by reflection (reading Title off the runtime type) and the router duck-typed IsStaticContent across two unrelated classes.

Collapse the content side under one abstract ContentPageModel base (named to avoid a clash with Avalonia.Controls.ContentPage). OptionsPageModel joins it, dropping its duplicate Title/IsStaticContent. ContentTabPage.Content becomes ContentPageModel?, so title/language-switching read directly and IsWritablePreview collapses from two runtime type-tests to one IsStaticContent check. CreateTab and the dock-router helpers are typed through. Morph-in-place is unchanged -- the One still swaps its Content in place; this only strengthens the types behind it.
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
5b497b3155
  1. 62
      ILSpy.Tests/Docking/ContentPageHierarchyTests.cs
  2. 6
      ILSpy.Tests/Docking/LayoutPersistenceTests.cs
  3. 14
      ILSpy/Docking/DockWorkspace.cs
  4. 2
      ILSpy/Metadata/BlobHeapTreeNode.cs
  5. 2
      ILSpy/Metadata/CoffHeaderTreeNode.cs
  6. 2
      ILSpy/Metadata/DataDirectoriesTreeNode.cs
  7. 2
      ILSpy/Metadata/DebugDirectoryTreeNode.cs
  8. 2
      ILSpy/Metadata/DosHeaderTreeNode.cs
  9. 2
      ILSpy/Metadata/GuidHeapTreeNode.cs
  10. 2
      ILSpy/Metadata/MetadataTableTreeNode.cs
  11. 2
      ILSpy/Metadata/OptionalHeaderTreeNode.cs
  12. 2
      ILSpy/Metadata/StringHeapTreeNode.cs
  13. 2
      ILSpy/Metadata/UserStringHeapTreeNode.cs
  14. 13
      ILSpy/Options/OptionsPageModel.cs
  15. 12
      ILSpy/TextView/DecompilerTabPageModel.cs
  16. 2
      ILSpy/TreeNodes/ILSpyTreeNode.cs
  17. 2
      ILSpy/ViewModels/CompareTabPageModel.cs
  18. 39
      ILSpy/ViewModels/ContentPageModel.cs
  19. 26
      ILSpy/ViewModels/ContentTabPage.cs
  20. 2
      ILSpy/ViewModels/MetadataTablePageModel.cs

62
ILSpy.Tests/Docking/ContentPageHierarchyTests.cs

@ -0,0 +1,62 @@
// 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.ViewModels;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
/// <summary>
/// Locks the single inner-content hierarchy: every viewmodel that may occupy a
/// <see cref="ContentTabPage"/>'s Content slot derives from the one <see cref="ContentPageModel"/>
/// base, and <c>ContentTabPage.Content</c> is typed to it (not <c>object</c>). Guards against a
/// future content type sneaking back in as a bare ObservableObject or the slot weakening to object.
/// </summary>
[TestFixture]
public class ContentPageHierarchyTests
{
[Test]
public void All_Four_Content_Types_Derive_From_ContentPageModel()
{
typeof(global::ILSpy.TextView.DecompilerTabPageModel).Should().BeAssignableTo<ContentPageModel>();
typeof(MetadataTablePageModel).Should().BeAssignableTo<ContentPageModel>();
typeof(global::ILSpy.Compare.CompareTabPageModel).Should().BeAssignableTo<ContentPageModel>();
typeof(global::ILSpy.Options.OptionsPageModel).Should().BeAssignableTo<ContentPageModel>(
"OptionsPageModel must join the content hierarchy, not stay a bare ObservableObject");
}
[Test]
public void ContentTabPage_Content_Is_Typed_To_ContentPageModel()
{
typeof(ContentTabPage).GetProperty(nameof(ContentTabPage.Content))!.PropertyType
.Should().Be(typeof(ContentPageModel), "the Content slot must be strongly typed, not object");
}
[Test]
public void IsStaticContent_Is_A_Single_Inherited_Member()
{
// One IsStaticContent, declared on the base -- not duck-typed across unrelated classes.
typeof(ContentPageModel).GetProperty(nameof(ContentPageModel.IsStaticContent))
.Should().NotBeNull("IsStaticContent lives on ContentPageModel");
typeof(global::ILSpy.Options.OptionsPageModel).GetProperty("IsStaticContent")!.DeclaringType
.Should().Be(typeof(ContentPageModel), "OptionsPageModel must inherit IsStaticContent, not redeclare it");
}
}

6
ILSpy.Tests/Docking/LayoutPersistenceTests.cs

@ -183,9 +183,9 @@ public class LayoutPersistenceTests
// Open two extra documents on top of the persistent MainTab. Content type is // Open two extra documents on top of the persistent MainTab. Content type is
// irrelevant for this test — the bug is structural, about ContentTabPage shells // irrelevant for this test — the bug is structural, about ContentTabPage shells
// being persisted at all. // being persisted at all — so any ContentPageModel will do.
dockWorkspace.OpenNewTab(new object()); dockWorkspace.OpenNewTab(new global::ILSpy.TextView.DecompilerTabPageModel());
dockWorkspace.OpenNewTab(new object()); dockWorkspace.OpenNewTab(new global::ILSpy.TextView.DecompilerTabPageModel());
TestCapture.Step("three-document-tabs-open"); TestCapture.Step("three-document-tabs-open");
var sourceDocs = dockWorkspace.Documents!.VisibleDockables! var sourceDocs = dockWorkspace.Documents!.VisibleDockables!
.OfType<ContentTabPage>().Count(); .OfType<ContentTabPage>().Count();

14
ILSpy/Docking/DockWorkspace.cs

@ -724,7 +724,7 @@ namespace ILSpy.Docking
// deterministic without going through Dock's add+close lifecycle. // deterministic without going through Dock's add+close lifecycle.
// Carve-outs ("Open in new tab", "Freeze tab") aren't implemented yet and would // Carve-outs ("Open in new tab", "Freeze tab") aren't implemented yet and would
// branch off this path before the Content swap. // branch off this path before the Content swap.
TabPageModel? customContent; ContentPageModel? customContent;
using (ILSpy.AppEnv.AppLog.Phase("ShowSelectedNode: node.CreateTab")) using (ILSpy.AppEnv.AppLog.Phase("ShowSelectedNode: node.CreateTab"))
customContent = nodes.Length == 1 ? nodes[0].CreateTab() : null; customContent = nodes.Length == 1 ? nodes[0].CreateTab() : null;
@ -778,7 +778,7 @@ namespace ILSpy.Docking
} }
// Pure predicate: returns the tab if it is a writable preview ContentTabPage -- IsPreview // Pure predicate: returns the tab if it is a writable preview ContentTabPage -- IsPreview
// true AND not hosting static content (Options, About). Otherwise null. Options/About are // true AND not hosting static content (Options, About). Otherwise null. Static pages are
// born frozen (IsPreview=false) AND carry IsStaticContent=true; either flag alone would // born frozen (IsPreview=false) AND carry IsStaticContent=true; either flag alone would
// suffice, both guard against drift. // suffice, both guard against drift.
static ContentTabPage? IsWritablePreview(IDockable? dockable) static ContentTabPage? IsWritablePreview(IDockable? dockable)
@ -787,9 +787,7 @@ namespace ILSpy.Docking
return null; return null;
if (!tab.IsPreview) if (!tab.IsPreview)
return null; return null;
if (tab.Content is DecompilerTabPageModel { IsStaticContent: true }) if (tab.Content is { IsStaticContent: true })
return null;
if (tab.Content is Options.OptionsPageModel)
return null; return null;
return tab; return tab;
} }
@ -810,7 +808,7 @@ namespace ILSpy.Docking
factory.SetActiveDockable(main); factory.SetActiveDockable(main);
} }
void AttachCustomContent(ContentTabPage main, TabPageModel newContent) void AttachCustomContent(ContentTabPage main, ContentPageModel newContent)
{ {
// Detach navigation handlers from the outgoing content; subscribe on the // Detach navigation handlers from the outgoing content; subscribe on the
// incoming one so token clicks route through OnMetadataCellClicked and // incoming one so token clicks route through OnMetadataCellClicked and
@ -996,7 +994,7 @@ namespace ILSpy.Docking
return tab; return tab;
} }
static void CopyContentState(object source, object target) static void CopyContentState(ContentPageModel source, ContentPageModel target)
{ {
if (source is MetadataTablePageModel newMeta && target is MetadataTablePageModel oldMeta) if (source is MetadataTablePageModel newMeta && target is MetadataTablePageModel oldMeta)
{ {
@ -1101,7 +1099,7 @@ namespace ILSpy.Docking
} }
} }
public ContentTabPage OpenNewTab(object content, SharpTreeNode? sourceNode = null) public ContentTabPage OpenNewTab(ContentPageModel content, SharpTreeNode? sourceNode = null)
{ {
// Carve-outs are born frozen — they survive tree-node selections instead of // Carve-outs are born frozen — they survive tree-node selections instead of
// being replaced like the preview MainTab. // being replaced like the preview MainTab.

2
ILSpy/Metadata/BlobHeapTreeNode.cs

@ -45,7 +45,7 @@ namespace ILSpy.Metadata
public override object Text => $"Blob Heap ({EnsureEntries().Count})"; public override object Text => $"Blob Heap ({EnsureEntries().Count})";
public override string ToString() => "Blob Heap"; public override string ToString() => "Blob Heap";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "Blob Heap", Title = "Blob Heap",

2
ILSpy/Metadata/CoffHeaderTreeNode.cs

@ -45,7 +45,7 @@ namespace ILSpy.Metadata
public override object Icon => Images.Images.Header; public override object Icon => Images.Images.Header;
public override string ToString() => "COFF Header"; public override string ToString() => "COFF Header";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "COFF Header", Title = "COFF Header",

2
ILSpy/Metadata/DataDirectoriesTreeNode.cs

@ -47,7 +47,7 @@ namespace ILSpy.Metadata
public override object ExpandedIcon => Images.Images.ListFolderOpen; public override object ExpandedIcon => Images.Images.ListFolderOpen;
public override string ToString() => "Data Directories"; public override string ToString() => "Data Directories";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "Data Directories", Title = "Data Directories",

2
ILSpy/Metadata/DebugDirectoryTreeNode.cs

@ -51,7 +51,7 @@ namespace ILSpy.Metadata
public override object ExpandedIcon => Images.Images.ListFolderOpen; public override object ExpandedIcon => Images.Images.ListFolderOpen;
public override string ToString() => "Debug Directory"; public override string ToString() => "Debug Directory";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "Debug Directory", Title = "Debug Directory",

2
ILSpy/Metadata/DosHeaderTreeNode.cs

@ -44,7 +44,7 @@ namespace ILSpy.Metadata
public override object Icon => Images.Images.Header; public override object Icon => Images.Images.Header;
public override string ToString() => "DOS Header"; public override string ToString() => "DOS Header";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "DOS Header", Title = "DOS Header",

2
ILSpy/Metadata/GuidHeapTreeNode.cs

@ -44,7 +44,7 @@ namespace ILSpy.Metadata
public override object Text => $"Guid Heap ({EnsureEntries().Count})"; public override object Text => $"Guid Heap ({EnsureEntries().Count})";
public override string ToString() => "Guid Heap"; public override string ToString() => "Guid Heap";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "Guid Heap", Title = "Guid Heap",

2
ILSpy/Metadata/MetadataTableTreeNode.cs

@ -72,7 +72,7 @@ namespace ILSpy.Metadata
protected abstract IReadOnlyList<TEntry> LoadTable(); protected abstract IReadOnlyList<TEntry> LoadTable();
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
// IReadOnlyList<T> is covariant on T, so a strongly-typed list passes through to // IReadOnlyList<T> is covariant on T, so a strongly-typed list passes through to
// MetadataTablePageModel.Items (declared as IReadOnlyList<object>) without a // MetadataTablePageModel.Items (declared as IReadOnlyList<object>) without a

2
ILSpy/Metadata/OptionalHeaderTreeNode.cs

@ -46,7 +46,7 @@ namespace ILSpy.Metadata
public override object Icon => Images.Images.Header; public override object Icon => Images.Images.Header;
public override string ToString() => "Optional Header"; public override string ToString() => "Optional Header";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "Optional Header", Title = "Optional Header",

2
ILSpy/Metadata/StringHeapTreeNode.cs

@ -45,7 +45,7 @@ namespace ILSpy.Metadata
public override object Text => $"String Heap ({EnsureEntries().Count})"; public override object Text => $"String Heap ({EnsureEntries().Count})";
public override string ToString() => "String Heap"; public override string ToString() => "String Heap";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "String Heap", Title = "String Heap",

2
ILSpy/Metadata/UserStringHeapTreeNode.cs

@ -44,7 +44,7 @@ namespace ILSpy.Metadata
public override object Text => $"UserString Heap ({EnsureEntries().Count})"; public override object Text => $"UserString Heap ({EnsureEntries().Count})";
public override string ToString() => "UserString Heap"; public override string ToString() => "UserString Heap";
public override TabPageModel CreateTab() public override ContentPageModel CreateTab()
{ {
var page = new MetadataTablePageModel { var page = new MetadataTablePageModel {
Title = "UserString Heap", Title = "UserString Heap",

13
ILSpy/Options/OptionsPageModel.cs

@ -25,6 +25,8 @@ using CommunityToolkit.Mvvm.Input;
using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.Properties;
using ILSpy.ViewModels;
namespace ILSpy.Options namespace ILSpy.Options
{ {
/// <summary> /// <summary>
@ -38,7 +40,7 @@ namespace ILSpy.Options
/// <see cref="SettingsService.Save"/> at app exit. /// <see cref="SettingsService.Save"/> at app exit.
/// </para> /// </para>
/// </summary> /// </summary>
public sealed partial class OptionsPageModel : ObservableObject public sealed partial class OptionsPageModel : ContentPageModel
{ {
public OptionsPageModel(SettingsService settingsService, IEnumerable<ExportFactory<IOptionPage, IOptionsMetadata>> pageFactories) public OptionsPageModel(SettingsService settingsService, IEnumerable<ExportFactory<IOptionPage, IOptionsMetadata>> pageFactories)
{ {
@ -54,17 +56,12 @@ namespace ILSpy.Options
// Bare "Options" string — the menu item's Resources._Options carries the // Bare "Options" string — the menu item's Resources._Options carries the
// accelerator underscore + "..." suffix which are menu-only conventions. // accelerator underscore + "..." suffix which are menu-only conventions.
Title = Resources.Options; Title = Resources.Options;
// Tree-node selections must not replace the Options tab (ContentPageModel.IsStaticContent).
IsStaticContent = true;
ResetCurrentPageCommand = new RelayCommand(ResetCurrentPage); ResetCurrentPageCommand = new RelayCommand(ResetCurrentPage);
} }
public string Title { get; }
/// <summary>Marker for the dock router: tree-node selections must not replace this
/// tab. Mirrors the <see cref="ILSpy.TextView.DecompilerTabPageModel.IsStaticContent"/>
/// convention used for the About tab.</summary>
public bool IsStaticContent => true;
public IReadOnlyList<IOptionPage> Pages { get; } public IReadOnlyList<IOptionPage> Pages { get; }
[ObservableProperty] [ObservableProperty]

12
ILSpy/TextView/DecompilerTabPageModel.cs

@ -47,7 +47,7 @@ namespace ILSpy.TextView
/// <see cref="CurrentNode"/> changes; previous in-flight decompilations are cancelled so a /// <see cref="CurrentNode"/> changes; previous in-flight decompilations are cancelled so a
/// rapid tree-selection sweep doesn't pile up background work. /// rapid tree-selection sweep doesn't pile up background work.
/// </summary> /// </summary>
public sealed partial class DecompilerTabPageModel : TabPageModel public sealed partial class DecompilerTabPageModel : ContentPageModel
{ {
CancellationTokenSource? activeCts; CancellationTokenSource? activeCts;
@ -202,13 +202,9 @@ namespace ILSpy.TextView
// between selection and the title update. // between selection and the title update.
string cachedBaseTitle = "(unnamed)"; string cachedBaseTitle = "(unnamed)";
/// <summary> // IsStaticContent is inherited from ContentPageModel: true for static pages (e.g. About)
/// True for tabs whose content is a static page (e.g. About) rather than the result // excludes this tab from the "current decompile target" lookup, so later tree-node
/// of decompiling a tree-node selection. Static tabs are excluded from the lookup // clicks reuse a different tab and leave the static content intact.
/// that resolves "the current decompile target", so subsequent tree-node clicks open
/// or reuse a different tab and leave the static content intact.
/// </summary>
public bool IsStaticContent { get; set; }
[RelayCommand] [RelayCommand]
void CancelDecompilation() void CancelDecompilation()

2
ILSpy/TreeNodes/ILSpyTreeNode.cs

@ -166,7 +166,7 @@ namespace ILSpy.TreeNodes
/// through the decompiler-text path. Lets metadata-table nodes show their own /// through the decompiler-text path. Lets metadata-table nodes show their own
/// DataGrid view while the rest of the tree keeps decompiling. /// DataGrid view while the rest of the tree keeps decompiling.
/// </summary> /// </summary>
public virtual TabPageModel? CreateTab() => null; public virtual ContentPageModel? CreateTab() => null;
/// <summary> /// <summary>
/// Applies <see cref="Filter"/> to every newly-added child and writes the result /// Applies <see cref="Filter"/> to every newly-added child and writes the result

2
ILSpy/ViewModels/CompareTabPageModel.cs

@ -31,7 +31,7 @@ namespace ILSpy.Compare
/// assemblies and picks "Compare…". Static content (no language switching, no /// assemblies and picks "Compare…". Static content (no language switching, no
/// re-decompile) — the merge is computed once at construction. /// re-decompile) — the merge is computed once at construction.
/// </summary> /// </summary>
public sealed partial class CompareTabPageModel : TabPageModel public sealed partial class CompareTabPageModel : ContentPageModel
{ {
readonly LoadedAssembly leftAssembly; readonly LoadedAssembly leftAssembly;
readonly LoadedAssembly rightAssembly; readonly LoadedAssembly rightAssembly;

39
ILSpy/ViewModels/ContentPageModel.cs

@ -0,0 +1,39 @@
// 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.
namespace ILSpy.ViewModels
{
/// <summary>
/// Base for the concrete viewmodels that occupy a <see cref="ContentTabPage"/>'s
/// <see cref="ContentTabPage.Content"/> slot: the decompiler text view, the metadata grid,
/// the compare view, and the Options panel. Collapsing them under one base lets
/// <c>ContentTabPage</c> type its <c>Content</c> strongly and read <c>Title</c> /
/// <c>SupportsLanguageSwitching</c> / <see cref="IsStaticContent"/> directly instead of by
/// reflection or runtime type-tests scattered across the dock router.
/// </summary>
public abstract class ContentPageModel : TabPageModel
{
/// <summary>
/// True for content a tree-node selection must NOT replace in the preview tab -- the
/// static pages (Options, About, embedded resources). Defaults false (ordinary decompile
/// / metadata content); static pages set it true. The dock router's "is this the writable
/// preview?" and "what's the current decompile target?" checks key off this.
/// </summary>
public bool IsStaticContent { get; set; }
}
}

26
ILSpy/ViewModels/ContentTabPage.cs

@ -57,7 +57,7 @@ namespace ILSpy.ViewModels
bool IDeferredContentPresentation.DeferContentPresentation => false; bool IDeferredContentPresentation.DeferContentPresentation => false;
[ObservableProperty] [ObservableProperty]
private object? content; private ContentPageModel? content;
/// <summary> /// <summary>
/// The assembly-tree node this tab represents. Used by <c>DockWorkspace</c> to /// The assembly-tree node this tab represents. Used by <c>DockWorkspace</c> to
@ -84,12 +84,12 @@ namespace ILSpy.ViewModels
// or "DOS Header" for a metadata grid). Also mirror the Content's // or "DOS Header" for a metadata grid). Also mirror the Content's
// SupportsLanguageSwitching flag so the toolbar pickers can bind to the wrapper // SupportsLanguageSwitching flag so the toolbar pickers can bind to the wrapper
// without drilling into Content's runtime type. // without drilling into Content's runtime type.
partial void OnContentChanged(object? oldValue, object? newValue) partial void OnContentChanged(ContentPageModel? oldValue, ContentPageModel? newValue)
{ {
if (oldValue is INotifyPropertyChanged oldNotify) if (oldValue is not null)
oldNotify.PropertyChanged -= OnContentPropertyChanged; oldValue.PropertyChanged -= OnContentPropertyChanged;
if (newValue is INotifyPropertyChanged newNotify) if (newValue is not null)
newNotify.PropertyChanged += OnContentPropertyChanged; newValue.PropertyChanged += OnContentPropertyChanged;
SyncTitleFromContent(); SyncTitleFromContent();
SyncSupportsLanguageSwitchingFromContent(); SyncSupportsLanguageSwitchingFromContent();
} }
@ -104,19 +104,15 @@ namespace ILSpy.ViewModels
void SyncSupportsLanguageSwitchingFromContent() void SyncSupportsLanguageSwitchingFromContent()
{ {
// Default true when Content is null or isn't a TabPageModel-derived viewmodel // The toolbar binds to this property. Default true when no content is set — "no
// (e.g. the OptionsPageModel which inherits ObservableObject directly). The // language-aware tab is active" leaves the pickers enabled; otherwise mirror the
// toolbar binds to this property; null/non-TabPageModel content means "no // content's own flag (metadata / compare / options report false).
// language-aware tab is active" — leave the pickers enabled. SupportsLanguageSwitching = Content?.SupportsLanguageSwitching ?? true;
SupportsLanguageSwitching = (Content as TabPageModel)?.SupportsLanguageSwitching ?? true;
} }
void SyncTitleFromContent() void SyncTitleFromContent()
{ {
if (Content is null) if (Content?.Title is { } s)
return;
var titleProp = Content.GetType().GetProperty(nameof(Title), typeof(string));
if (titleProp?.GetValue(Content) is string s)
Title = s; Title = s;
} }
} }

2
ILSpy/ViewModels/MetadataTablePageModel.cs

@ -39,7 +39,7 @@ namespace ILSpy.ViewModels
/// <c>Columns</c> collection isn't an Avalonia property, so the model can't bind it /// <c>Columns</c> collection isn't an Avalonia property, so the model can't bind it
/// declaratively — the view assigns it imperatively from <see cref="Columns"/>. /// declaratively — the view assigns it imperatively from <see cref="Columns"/>.
/// </summary> /// </summary>
public sealed partial class MetadataTablePageModel : TabPageModel public sealed partial class MetadataTablePageModel : ContentPageModel
{ {
[ObservableProperty] [ObservableProperty]
private IReadOnlyList<object> items = Array.Empty<object>(); private IReadOnlyList<object> items = Array.Empty<object>();

Loading…
Cancel
Save