From c525f2d651b49650d185f721ebcda4fbc6d2c7b7 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Thu, 3 Sep 2026 10:18:10 +0800 Subject: [PATCH 1/2] Fix issue 15031: Disable NET11 VisualStylesMode for composite controls (ToolStrip, DataGridView, etc.) until NET11 layout modernization is completed --- .../System/Windows/Forms/Control.cs | 60 ++++++++++++++----- .../Controls/DataGridView/DataGridView.cs | 3 + .../Forms/Controls/ToolStrips/ToolStrip.cs | 3 + .../System/Windows/Forms/DataGridViewTests.cs | 57 ++++++++++++++++++ .../System/Windows/Forms/ToolStripTests.cs | 57 ++++++++++++++++++ 5 files changed, 166 insertions(+), 14 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index 1dad772dc70..2b5390ff76b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -899,10 +899,10 @@ public virtual partial VisualStylesMode VisualStylesMode VisualStylesMode oldEffectiveValue = EffectiveVisualStylesMode; - // Inherit was requested explicitly, or the requested value matches the ambient (parent) value: + // Inherit was requested explicitly, or the requested value matches the uncoerced ambient value: // drop any local override so the value is inherited again. if (value == VisualStylesMode.Inherit - || (ParentInternal is { } parent && parent.ResolvedVisualStylesMode == value)) + || (ParentInternal is { } parent && parent.UncoercedVisualStylesMode == value)) { Properties.RemoveValue(s_visualStylesModeProperty); } @@ -927,18 +927,30 @@ private bool ShouldSerializeVisualStylesMode() private void ResetVisualStylesMode() => VisualStylesMode = VisualStylesMode.Inherit; - private VisualStylesMode ResolvedVisualStylesMode + private VisualStylesMode UncoercedVisualStylesMode { get { VisualStylesMode value = VisualStylesMode; return value == VisualStylesMode.Inherit - ? ParentInternal?.ResolvedVisualStylesMode ?? DefaultVisualStylesMode + ? ParentInternal?.UncoercedVisualStylesMode ?? DefaultVisualStylesMode : value; } } + private VisualStylesMode ResolvedVisualStylesMode + => GetSupportedVisualStylesMode( + VisualStylesMode == VisualStylesMode.Inherit + ? ParentInternal?.ResolvedVisualStylesMode ?? DefaultVisualStylesMode + : VisualStylesMode); + + /// + /// Coerces a requested visual styles mode to one supported by this control. + /// + private protected virtual VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode) + => mode; + /// /// Gets the renderer-authoritative that controls must honor when deciding /// or paint behavior, after applying the High Contrast and disabled clamps. @@ -7449,7 +7461,7 @@ protected virtual void OnParentVisualStylesModeChanged(EventArgs e) if (Properties.ContainsKey(s_visualStylesModeProperty)) { if (Properties.GetValueOrDefault(s_visualStylesModeProperty) - == ParentInternal?.ResolvedVisualStylesMode) + == ParentInternal?.UncoercedVisualStylesMode) { // Same as the parent value, make it ambient again by removing it. Properties.RemoveValue(s_visualStylesModeProperty); @@ -7460,14 +7472,28 @@ protected virtual void OnParentVisualStylesModeChanged(EventArgs e) return; } - if (e is VisualStylesModeChangeEventArgs transition - && (!transition.IsCurrent - || ParentInternal?.EffectiveVisualStylesMode != transition.NewEffectiveVisualStylesMode)) + if (e is VisualStylesModeChangeEventArgs transition) { + if (!transition.IsCurrent + || ParentInternal?.EffectiveVisualStylesMode != transition.NewEffectiveVisualStylesMode) + { + return; + } + + VisualStylesModeChangeEventArgs transitionForControl = transition.CreateForControl(this); + if (transitionForControl.OldEffectiveVisualStylesMode + != transitionForControl.NewEffectiveVisualStylesMode) + { + OnVisualStylesModeChanged(transitionForControl); + } + else if (ChildControls is { } children) + { + CascadeVisualStylesModeChanged(children, transitionForControl, transitionForControl); + } + return; } - // In every other case we're going to raise the event. OnVisualStylesModeChanged(e); } @@ -7534,18 +7560,24 @@ public bool IsCurrent public VisualStylesModeChangeEventArgs CreateForControl(Control control) { - if (SystemVisualSettingsTransition is not { } systemVisualSettingsTransition) + if (SystemVisualSettingsTransition is { } systemVisualSettingsTransition) { - return this; + return new( + _state, + control, + control.Properties.GetValueOrDefault(s_visualStylesModeChangeVersionProperty, 0), + control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.OldSettings.HighContrastEnabled), + control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.NewSettings.HighContrastEnabled), + systemVisualSettingsTransition); } return new( _state, control, control.Properties.GetValueOrDefault(s_visualStylesModeChangeVersionProperty, 0), - control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.OldSettings.HighContrastEnabled), - control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.NewSettings.HighContrastEnabled), - systemVisualSettingsTransition); + control.GetSupportedVisualStylesMode(OldEffectiveVisualStylesMode), + control.GetSupportedVisualStylesMode(NewEffectiveVisualStylesMode), + systemVisualSettingsTransition: null); } public void PerformLayouts() diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs index 5f1af6e83c4..d1189b02919 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/DataGridView/DataGridView.cs @@ -16,6 +16,9 @@ namespace System.Windows.Forms; [SRDescription(nameof(SR.DescriptionDataGridView))] public partial class DataGridView : Control, ISupportInitialize { + private protected override VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode) + => mode >= VisualStylesMode.Net11 ? VisualStylesMode.Classic : mode; + private static readonly object s_allowUserToAddRowsChangedEvent = new(); private static readonly object s_allowUserToDeleteRowsChangedEvent = new(); private static readonly object s_allowUserToOrderColumnsChangedEvent = new(); diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs index 6e0c698d38b..eb5205b5403 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs @@ -23,6 +23,9 @@ namespace System.Windows.Forms; [DefaultEvent(nameof(ItemClicked))] public partial class ToolStrip : ScrollableControl, IArrangedElement, ISupportToolStripPanel { + private protected override VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode) + => mode >= VisualStylesMode.Net11 ? VisualStylesMode.Classic : mode; + private static Size s_onePixel = new(1, 1); internal static Point s_invalidMouseEnter = new(int.MaxValue, int.MaxValue); diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs index d79cca0216b..dfae66ba01b 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/DataGridViewTests.cs @@ -38,6 +38,61 @@ public void DataGridView_Ctor_Default() Assert.Same(control.RowTemplate, control.RowTemplate); } + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void DataGridView_VisualStylesMode_ModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using SubDataGridView control = new() { VisualStylesMode = value }; + + Assert.Equal(value, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void DataGridView_VisualStylesMode_InheritedModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using Control parent = new() { VisualStylesMode = value }; + using SubDataGridView control = new(); + parent.Controls.Add(control); + + Assert.Equal(VisualStylesMode.Inherit, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsFact] + public void DataGridView_VisualStylesMode_ParentChangesToModernMode_DoesNotRaiseChanged() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Classic }; + using SubDataGridView control = new(); + parent.Controls.Add(control); + int callCount = 0; + control.VisualStylesModeChanged += (sender, e) => callCount++; + + parent.VisualStylesMode = VisualStylesMode.Net11; + + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.Equal(0, callCount); + } + + [WinFormsFact] + public void DataGridView_VisualStylesMode_ExplicitClassic_RemainsLocalOverride() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Net11 }; + using SubDataGridView control = new(); + parent.Controls.Add(control); + control.VisualStylesMode = VisualStylesMode.Classic; + + parent.VisualStylesMode = VisualStylesMode.Disabled; + + Assert.Equal(VisualStylesMode.Classic, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + } + private const int DefaultColumnHeadersHeight = 23; public static IEnumerable ColumnHeadersHeight_Set_TestData() @@ -2892,6 +2947,8 @@ private class SubDataGridViewCell : DataGridViewCell private class SubDataGridView : DataGridView { + public VisualStylesMode EffectiveVisualStylesModeAccessor => base.EffectiveVisualStylesMode; + public new void OnColumnHeadersHeightChanged(EventArgs e) => base.OnColumnHeadersHeightChanged(e); public new void OnColumnHeadersHeightSizeModeChanged(DataGridViewAutoSizeModeEventArgs e) => base.OnColumnHeadersHeightSizeModeChanged(e); diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs index d7f5f10b511..8f305b633ff 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ToolStripTests.cs @@ -16,6 +16,61 @@ namespace System.Windows.Forms.Tests; public partial class ToolStripTests : IDisposable { + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void ToolStrip_VisualStylesMode_ModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using SubToolStrip control = new() { VisualStylesMode = value }; + + Assert.Equal(value, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsTheory] + [InlineData(VisualStylesMode.Net11)] + [InlineData(VisualStylesMode.Latest)] + public void ToolStrip_VisualStylesMode_InheritedModernMode_UsesClassicEffectiveMode(VisualStylesMode value) + { + using Control parent = new() { VisualStylesMode = value }; + using SubToolStrip control = new(); + parent.Controls.Add(control); + + Assert.Equal(VisualStylesMode.Inherit, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.False(control.IsHandleCreated); + } + + [WinFormsFact] + public void ToolStrip_VisualStylesMode_ParentChangesToModernMode_DoesNotRaiseChanged() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Classic }; + using SubToolStrip control = new(); + parent.Controls.Add(control); + int callCount = 0; + control.VisualStylesModeChanged += (sender, e) => callCount++; + + parent.VisualStylesMode = VisualStylesMode.Net11; + + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + Assert.Equal(0, callCount); + } + + [WinFormsFact] + public void ToolStrip_VisualStylesMode_ExplicitClassic_RemainsLocalOverride() + { + using Control parent = new() { VisualStylesMode = VisualStylesMode.Net11 }; + using SubToolStrip control = new(); + parent.Controls.Add(control); + control.VisualStylesMode = VisualStylesMode.Classic; + + parent.VisualStylesMode = VisualStylesMode.Disabled; + + Assert.Equal(VisualStylesMode.Classic, control.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, control.EffectiveVisualStylesModeAccessor); + } + [WinFormsFact] public void ToolStrip_Ctor_Default() { @@ -7537,6 +7592,8 @@ public SubToolStrip(ToolStripItem[] items) : base(items) public new bool CanRaiseEvents => base.CanRaiseEvents; + public VisualStylesMode EffectiveVisualStylesModeAccessor => base.EffectiveVisualStylesMode; + public new CreateParams CreateParams => base.CreateParams; public new Cursor DefaultCursor => base.DefaultCursor; From ff2ec3f009509adfbf9a39ebb26334720ef66b84 Mon Sep 17 00:00:00 2001 From: "Simon Zhao (BEYONDSOFT CONSULTING INC)" Date: Thu, 3 Sep 2026 16:06:26 +0800 Subject: [PATCH 2/2] Handle feedback --- .../System/Windows/Forms/Control.cs | 57 ++++++++++++++----- .../Forms/ControlTests.VisualStylesMode.cs | 27 +++++++++ 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index 2b5390ff76b..68354fae131 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -7458,28 +7458,46 @@ protected virtual void OnParentVisualStylesModeChanged(EventArgs e) return; } + VisualStylesModeChangeEventArgs? transition = e as VisualStylesModeChangeEventArgs; + if (transition is not null + && (!transition.IsCurrent + || ParentInternal?.EffectiveVisualStylesMode != transition.NewEffectiveVisualStylesMode)) + { + return; + } + if (Properties.ContainsKey(s_visualStylesModeProperty)) { if (Properties.GetValueOrDefault(s_visualStylesModeProperty) - == ParentInternal?.UncoercedVisualStylesMode) + != ParentInternal?.UncoercedVisualStylesMode) { - // Same as the parent value, make it ambient again by removing it. - Properties.RemoveValue(s_visualStylesModeProperty); + // A local value isolates this subtree from parent changes. + return; } - // A local value isolates this subtree from parent changes. If the local value matched the - // parent's new value, removing it preserves the effective value while making it ambient again. - return; - } + VisualStylesMode oldEffectiveVisualStylesMode = EffectiveVisualStylesMode; - if (e is VisualStylesModeChangeEventArgs transition) - { - if (!transition.IsCurrent - || ParentInternal?.EffectiveVisualStylesMode != transition.NewEffectiveVisualStylesMode) + // Same as the parent value, make it ambient again by removing it. + Properties.RemoveValue(s_visualStylesModeProperty); + + VisualStylesMode newEffectiveVisualStylesMode = EffectiveVisualStylesMode; + if (oldEffectiveVisualStylesMode == newEffectiveVisualStylesMode) { return; } + OnVisualStylesModeChanged( + transition?.CreateForControl( + this, + oldEffectiveVisualStylesMode, + newEffectiveVisualStylesMode) + ?? e); + + return; + } + + if (transition is not null) + { VisualStylesModeChangeEventArgs transitionForControl = transition.CreateForControl(this); if (transitionForControl.OldEffectiveVisualStylesMode != transitionForControl.NewEffectiveVisualStylesMode) @@ -7571,14 +7589,23 @@ public VisualStylesModeChangeEventArgs CreateForControl(Control control) systemVisualSettingsTransition); } - return new( + return CreateForControl( + control, + control.GetSupportedVisualStylesMode(OldEffectiveVisualStylesMode), + control.GetSupportedVisualStylesMode(NewEffectiveVisualStylesMode)); + } + + public VisualStylesModeChangeEventArgs CreateForControl( + Control control, + VisualStylesMode oldEffectiveVisualStylesMode, + VisualStylesMode newEffectiveVisualStylesMode) + => new( _state, control, control.Properties.GetValueOrDefault(s_visualStylesModeChangeVersionProperty, 0), - control.GetSupportedVisualStylesMode(OldEffectiveVisualStylesMode), - control.GetSupportedVisualStylesMode(NewEffectiveVisualStylesMode), + oldEffectiveVisualStylesMode, + newEffectiveVisualStylesMode, systemVisualSettingsTransition: null); - } public void PerformLayouts() { diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs index 61bbbf947bb..45cb5c08d9d 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs @@ -496,6 +496,33 @@ public void Control_VisualStylesMode_ReentrantChange_SuppressesStaleChildCascade Assert.Equal(1, childChangedCallCount); } + [WinFormsFact] + public void Control_VisualStylesMode_LocalOverrideRemovedByCoercedParent_RaisesChangedAndCascades() + { + using DataGridView parent = new() { VisualStylesMode = VisualStylesMode.Disabled }; + using SubControlWithVisualStyles child = new() + { + HighContrast = false, + VisualStylesMode = VisualStylesMode.Latest + }; + using SubControlWithVisualStyles grandchild = new() { HighContrast = false }; + child.Controls.Add(grandchild); + parent.Controls.Add(child); + + int childChangedCallCount = 0; + child.VisualStylesModeChanged += (sender, e) => childChangedCallCount++; + int grandchildChangedCallCount = 0; + grandchild.VisualStylesModeChanged += (sender, e) => grandchildChangedCallCount++; + + parent.VisualStylesMode = VisualStylesMode.Latest; + + Assert.Equal(VisualStylesMode.Inherit, child.VisualStylesMode); + Assert.Equal(VisualStylesMode.Classic, child.EffectiveVisualStylesModeAccessor); + Assert.Equal(VisualStylesMode.Classic, grandchild.EffectiveVisualStylesModeAccessor); + Assert.Equal(1, childChangedCallCount); + Assert.Equal(1, grandchildChangedCallCount); + } + [WinFormsFact] public void Appearance_ToggleSwitch_HasExpectedValue() {