Browse Source

Disable language switching for metadata + compare tabs

The Language and Language-Version pickers in the main toolbar have no
effect when the active tab renders PE-header / metadata-table fields
straight from the metadata, or shows a structural compare diff —
language choice doesn't change what's drawn. Mirrors WPF's per-tab
TabPageModel.SupportsLanguageSwitching flag.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
fcd9ebb4b8
  1. 129
      ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs
  2. 21
      ILSpy/Docking/DockWorkspace.cs
  3. 4
      ILSpy/ViewModels/CompareTabPageModel.cs
  4. 16
      ILSpy/ViewModels/ContentTabPage.cs
  5. 5
      ILSpy/ViewModels/MetadataTablePageModel.cs
  6. 13
      ILSpy/ViewModels/TabPageModel.cs
  7. 6
      ILSpy/Views/MainToolBar.axaml

129
ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs

@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
// 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 ILSpy;
using ILSpy.AppEnv;
using ILSpy.Metadata;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Metadata;
[TestFixture]
public class MetadataDisablesLanguageSwitchingTests
{
[AvaloniaTest]
public async Task MetadataTablePageModel_Defaults_SupportsLanguageSwitching_False()
{
// Unit-level guard: every fresh MetadataTablePageModel must declare itself
// language-agnostic in its ctor. Mirrors WPF's CoffHeaderTreeNode / DataDirectories /
// MetadataTreeNode etc. setting tabPage.SupportsLanguageSwitching=false.
var model = new MetadataTablePageModel();
model.SupportsLanguageSwitching.Should().BeFalse(
"metadata grids render PE-header / table fields straight from metadata — language choice doesn't affect what's shown");
}
[AvaloniaTest]
public async Task DecompilerTabPageModel_Defaults_SupportsLanguageSwitching_True()
{
// Decompiler tabs ARE the place where Language / Language-Version matter. The
// default-true is what enables the toolbar pickers on a fresh decompile tab.
var model = new global::ILSpy.TextView.DecompilerTabPageModel();
model.SupportsLanguageSwitching.Should().BeTrue();
}
[AvaloniaTest]
public async Task ContentTabPage_Mirrors_Its_Contents_SupportsLanguageSwitching_Flag()
{
// The toolbar pickers bind through ContentTabPage (the dockable wrapper), not the
// inner Content viewmodel. ContentTabPage must reflect whatever the current Content
// declared — and re-evaluate on Content swap so a single tab going decompiler →
// metadata → decompiler over its lifetime keeps the toolbar in sync.
var tab = new ContentTabPage();
tab.SupportsLanguageSwitching.Should().BeTrue("default before any content is set");
var meta = new MetadataTablePageModel();
tab.Content = meta;
tab.SupportsLanguageSwitching.Should().BeFalse(
"swapping in a MetadataTablePageModel must propagate its false flag up to the wrapper");
var dec = new global::ILSpy.TextView.DecompilerTabPageModel();
tab.Content = dec;
tab.SupportsLanguageSwitching.Should().BeTrue(
"swapping back to a decompiler tab must restore the true flag");
}
[AvaloniaTest]
public async Task LanguageComboBox_Disables_When_Metadata_Tab_Active()
{
// End-to-end: selecting a metadata node populates the MainTab with a
// MetadataTablePageModel; the toolbar's LanguageComboBox.IsEnabled binding through
// DockWorkspace.ActiveContentTabPage.SupportsLanguageSwitching must flip to false.
// Selecting a decompilable type after that must flip it back to true.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var toolbar = await window.WaitForComponent<MainToolBar>();
var languageCombo = toolbar.GetVisualDescendants().OfType<ComboBox>()
.Single(c => c.Name == "LanguageComboBox");
// Baseline: no metadata tab active yet, picker should be enabled.
languageCombo.IsEnabled.Should().BeTrue(
"baseline: with no metadata tab active, the LanguageComboBox is enabled");
// Navigate to a metadata node (DOS Header is a stable choice — every PE has one).
var coreLibName = typeof(object).Assembly.GetName().Name!;
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
metadataNode.EnsureLazyChildren();
var dosHeader = metadataNode.Children.OfType<DosHeaderTreeNode>().Single();
vm.AssemblyTreeModel.SelectNode(dosHeader);
await Waiters.WaitForAsync(
() => vm.DockWorkspace.ActiveContentTabPage?.Content is MetadataTablePageModel,
System.TimeSpan.FromSeconds(10));
languageCombo.IsEnabled.Should().BeFalse(
"with a MetadataTablePageModel as the active content, the LanguageComboBox must disable");
// Now navigate to a decompilable type node and verify the picker re-enables.
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
coreLibName, "System", "System.Object");
vm.AssemblyTreeModel.SelectNode(typeNode);
await vm.DockWorkspace.WaitForDecompiledTextAsync(System.TimeSpan.FromSeconds(30));
languageCombo.IsEnabled.Should().BeTrue(
"swapping back to a decompiler tab must re-enable the LanguageComboBox");
}
}

21
ILSpy/Docking/DockWorkspace.cs

@ -28,6 +28,7 @@ using System.Reflection.Metadata.Ecma335; @@ -28,6 +28,7 @@ using System.Reflection.Metadata.Ecma335;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Dock.Model.Controls;
@ -52,8 +53,18 @@ namespace ILSpy.Docking @@ -52,8 +53,18 @@ namespace ILSpy.Docking
{
[Export]
[Shared]
public class DockWorkspace
public partial class DockWorkspace : ObservableObject
{
/// <summary>
/// The active <see cref="ContentTabPage"/> in the documents dock, or null when no
/// content tab is active (transient layout states, tool-pane focus, etc.). Updated
/// whenever <c>factory.Documents.ActiveDockable</c> changes via the existing
/// <see cref="OnDocumentsPropertyChanged"/> subscription. The main-toolbar pickers
/// bind through this — see <c>ActiveContentTabPage.SupportsLanguageSwitching</c>.
/// </summary>
[ObservableProperty]
private ContentTabPage? activeContentTabPage;
readonly ILSpyDockFactory factory;
readonly AssemblyTreeModel assemblyTreeModel;
readonly LanguageService languageService;
@ -202,6 +213,10 @@ namespace ILSpy.Docking @@ -202,6 +213,10 @@ namespace ILSpy.Docking
// (typically MainTab) — InitLayout ran before the subscriptions above were
// in place, so no DockableAdded event fired for those initial members.
SyncTabPageMenuItems();
// Seed ActiveContentTabPage from the freshly-initialised layout so the toolbar
// pickers' IsEnabled bindings have the right value at first paint. The
// subscription below will keep it in sync from here on.
ActiveContentTabPage = factory.Documents?.ActiveDockable as ContentTabPage;
// Dock's IFactory.ActiveDockableChanged only fires from InitActiveDockable (layout
// structural init), not when the user clicks a different tab — that path sets
// dock.ActiveDockable = X directly on the dock model. Subscribe to the model's own
@ -401,6 +416,10 @@ namespace ILSpy.Docking @@ -401,6 +416,10 @@ namespace ILSpy.Docking
{
if (e.PropertyName != nameof(IDocumentDock.ActiveDockable))
return;
// Mirror the dock's active document into ActiveContentTabPage. Tool-pane
// dockables and non-content dockables fall through to null, which the toolbar
// reads as "no language-aware tab is active" — pickers stay enabled by default.
ActiveContentTabPage = factory.Documents?.ActiveDockable as ContentTabPage;
// Tell each TabPageMenuItem to re-raise IsActive so the Window menu's checkmark
// follows the dock's selection. Items resolve "am I active?" against the dock's
// current ActiveDockable on read; this notify just kicks the binding.

4
ILSpy/ViewModels/CompareTabPageModel.cs

@ -52,6 +52,10 @@ namespace ILSpy.Compare @@ -52,6 +52,10 @@ namespace ILSpy.Compare
public CompareTabPageModel(LoadedAssembly left, LoadedAssembly right)
{
// Compare view shows a structural metadata diff — language-agnostic, so the
// toolbar's Language / Language-Version pickers shouldn't affect anything while
// this tab is active. Mirrors WPF's CompareViewModel.SupportsLanguageSwitching=false.
SupportsLanguageSwitching = false;
leftAssembly = left;
rightAssembly = right;
Title = $"Compare {left.Text} - {right.Text}";

16
ILSpy/ViewModels/ContentTabPage.cs

@ -82,7 +82,9 @@ namespace ILSpy.ViewModels @@ -82,7 +82,9 @@ namespace ILSpy.ViewModels
// Bubble the inner content's Title up to the Document's Title so the tab strip
// reflects whatever the active page chose (e.g. the decompiler tab's spinner glyph
// or "DOS Header" for a metadata grid).
// or "DOS Header" for a metadata grid). Also mirror the Content's
// SupportsLanguageSwitching flag so the toolbar pickers can bind to the wrapper
// without drilling into Content's runtime type.
partial void OnContentChanged(object? oldValue, object? newValue)
{
if (oldValue is INotifyPropertyChanged oldNotify)
@ -90,12 +92,24 @@ namespace ILSpy.ViewModels @@ -90,12 +92,24 @@ namespace ILSpy.ViewModels
if (newValue is INotifyPropertyChanged newNotify)
newNotify.PropertyChanged += OnContentPropertyChanged;
SyncTitleFromContent();
SyncSupportsLanguageSwitchingFromContent();
}
void OnContentPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Title))
SyncTitleFromContent();
else if (e.PropertyName == nameof(SupportsLanguageSwitching))
SyncSupportsLanguageSwitchingFromContent();
}
void SyncSupportsLanguageSwitchingFromContent()
{
// Default true when Content is null or isn't a TabPageModel-derived viewmodel
// (e.g. the OptionsPageModel which inherits ObservableObject directly). The
// toolbar binds to this property; null/non-TabPageModel content means "no
// language-aware tab is active" — leave the pickers enabled.
SupportsLanguageSwitching = (Content as TabPageModel)?.SupportsLanguageSwitching ?? true;
}
void SyncTitleFromContent()

5
ILSpy/ViewModels/MetadataTablePageModel.cs

@ -74,6 +74,11 @@ namespace ILSpy.ViewModels @@ -74,6 +74,11 @@ namespace ILSpy.ViewModels
public MetadataTablePageModel()
{
// Metadata grids render PE-header / table fields straight from the metadata —
// they're language-agnostic, so the toolbar's Language / Language-Version pickers
// don't affect what's shown. Mirror WPF's per-tab SupportsLanguageSwitching=false
// on every metadata tree node.
SupportsLanguageSwitching = false;
ColumnFilters.CollectionChanged += OnColumnFiltersCollectionChanged;
}

13
ILSpy/ViewModels/TabPageModel.cs

@ -16,11 +16,22 @@ @@ -16,11 +16,22 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using CommunityToolkit.Mvvm.ComponentModel;
using Dock.Model.Mvvm.Controls;
namespace ILSpy.ViewModels
{
public abstract class TabPageModel : Document
public abstract partial class TabPageModel : Document
{
/// <summary>
/// True when the toolbar's Language / Language-Version pickers should be enabled while
/// this tab is active. Defaults to <see langword="true"/> — meaningful for decompiler
/// output (C# / IL / ILAst). Metadata views and the compare view set this to
/// <see langword="false"/> in their ctors because language choice doesn't affect what
/// they render. Mirrors WPF's <c>TabPageModel.SupportsLanguageSwitching</c>.
/// </summary>
[ObservableProperty]
private bool supportsLanguageSwitching = true;
}
}

6
ILSpy/Views/MainToolBar.axaml

@ -243,7 +243,8 @@ @@ -243,7 +243,8 @@
<ComboBox Name="LanguageComboBox" MinWidth="100"
ToolTip.Tip="Select output language"
ItemsSource="{Binding LanguageService.Languages}"
SelectedItem="{Binding LanguageService.CurrentLanguage, Mode=TwoWay}" />
SelectedItem="{Binding LanguageService.CurrentLanguage, Mode=TwoWay}"
IsEnabled="{Binding DockWorkspace.ActiveContentTabPage.SupportsLanguageSwitching, FallbackValue=True, TargetNullValue=True}" />
</Grid>
<!-- Hidden when the active language doesn't differentiate versions (e.g. IL).
Avalonia's IsVisible binding stands in for WPF's BooleanToVisibilityConverter.
@ -263,7 +264,8 @@ @@ -263,7 +264,8 @@
ToolTip.Tip="Select C# language version"
ItemsSource="{Binding LanguageService.CurrentLanguage.LanguageVersions}"
SelectedItem="{Binding LanguageService.CurrentVersion, Mode=TwoWay}"
IsVisible="{Binding LanguageService.CurrentLanguage.HasLanguageVersions}">
IsVisible="{Binding LanguageService.CurrentLanguage.HasLanguageVersions}"
IsEnabled="{Binding DockWorkspace.ActiveContentTabPage.SupportsLanguageSwitching, FallbackValue=True, TargetNullValue=True}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />

Loading…
Cancel
Save