diff --git a/docs/runtime-configuration.md b/docs/runtime-configuration.md
index a578b6a7eac..85ca4472950 100644
--- a/docs/runtime-configuration.md
+++ b/docs/runtime-configuration.md
@@ -135,6 +135,24 @@ For example, the content of `[appname].runtimeconfig.json` generated from above
}
```
+#### Enabling modern rendering for composite controls
+
+`ToolStrip`, `MenuStrip`, and `DataGridView` use Classic rendering when `VisualStylesMode`
+is `Net11` or `Latest` by default. Applications can opt in to modern rendering with these
+switches:
+
+```json
+{
+ "configProperties": {
+ "System.Windows.Forms.ToolStripModernRendering": true,
+ "System.Windows.Forms.DataGridViewModernRendering": true
+ }
+}
+```
+
+`System.Windows.Forms.ToolStripModernRendering` controls both `ToolStrip` and `MenuStrip`.
+Both switches default to `false`.
+
The target framework information added to `[appname].runtimeconfig.json` file always match with the application target framework irrespective of runtime/SDK installed on the machine or runtime used ([roll-forward scenarios](https://docs.microsoft.com/dotnet/core/versions/selection#framework-dependent-apps-roll-forward)) by the Windows Forms application.
#### .NET runtime support in reading `runtimeconfig.json`
diff --git a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs
index effd0dd4629..8634da996eb 100644
--- a/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs
+++ b/src/System.Windows.Forms.Primitives/src/System/LocalAppContextSwitches/LocalAppContextSwitches.cs
@@ -27,6 +27,8 @@ internal static partial class LocalAppContextSwitches
internal const string TreeNodeCollectionAddRangeRespectsSortOrderSwitchName = "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder";
internal const string MoveTreeViewTextLocationOnePixelSwitchName = "System.Windows.Forms.TreeView.MoveTreeViewTextLocationOnePixel";
internal const string DataGridViewDarkModeThemingSwitchName = "System.Windows.Forms.DataGridViewDarkModeTheming";
+ internal const string ToolStripModernRenderingSwitchName = "System.Windows.Forms.ToolStripModernRendering";
+ internal const string DataGridViewModernRenderingSwitchName = "System.Windows.Forms.DataGridViewModernRendering";
private static int s_scaleTopLevelFormMinMaxSizeForDpi;
private static int s_anchorLayoutV2;
@@ -41,6 +43,8 @@ internal static partial class LocalAppContextSwitches
private static int s_moveTreeViewTextLocationOnePixel;
private static int s_dataGridViewDarkModeTheming;
+ private static int s_toolStripModernRendering;
+ private static int s_dataGridViewModernRendering;
private static FrameworkName? s_targetFrameworkName;
@@ -254,4 +258,24 @@ public static bool DataGridViewDarkModeTheming
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetCachedSwitchValue(DataGridViewDarkModeThemingSwitchName, ref s_dataGridViewDarkModeTheming);
}
+
+ ///
+ /// Indicates whether ToolStrip controls, including MenuStrip, support modern visual styles.
+ /// Defaults to .
+ ///
+ public static bool ToolStripModernRendering
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => GetCachedSwitchValue(ToolStripModernRenderingSwitchName, ref s_toolStripModernRendering);
+ }
+
+ ///
+ /// Indicates whether DataGridView controls support modern visual styles.
+ /// Defaults to .
+ ///
+ public static bool DataGridViewModernRendering
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => GetCachedSwitchValue(DataGridViewModernRenderingSwitchName, ref s_dataGridViewModernRendering);
+ }
}
diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs
index d0df1503dbd..cb67f146d3e 100644
--- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs
+++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs
@@ -924,18 +924,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.
@@ -7443,20 +7455,62 @@ 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))
{
- // A local value isolates this subtree from parent changes.
+ if (ParentInternal is not { } parent
+ || Properties.GetValueOrDefault(s_visualStylesModeProperty)
+ != parent.UncoercedVisualStylesMode
+ || parent.UncoercedVisualStylesMode == parent.EffectiveVisualStylesMode)
+ {
+ // A local value isolates this subtree from parent changes.
+ return;
+ }
+
+ VisualStylesMode oldEffectiveVisualStylesMode = EffectiveVisualStylesMode;
+
+ // Inherit the parent's coerced mode when the local value matches its uncoerced mode.
+ Properties.RemoveValue(s_visualStylesModeProperty);
+
+ VisualStylesMode newEffectiveVisualStylesMode = EffectiveVisualStylesMode;
+ if (oldEffectiveVisualStylesMode == newEffectiveVisualStylesMode)
+ {
+ return;
+ }
+
+ OnVisualStylesModeChanged(
+ transition?.CreateForControl(
+ this,
+ oldEffectiveVisualStylesMode,
+ newEffectiveVisualStylesMode)
+ ?? e);
+
return;
}
- if (e is VisualStylesModeChangeEventArgs transition
- && (!transition.IsCurrent
- || ParentInternal?.EffectiveVisualStylesMode != transition.NewEffectiveVisualStylesMode))
+ if (transition is not null)
{
+ 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);
}
@@ -7523,19 +7577,34 @@ 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(
+ 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.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.OldSettings.HighContrastEnabled),
- control.GetEffectiveVisualStylesMode(systemVisualSettingsTransition.NewSettings.HighContrastEnabled),
- systemVisualSettingsTransition);
- }
+ oldEffectiveVisualStylesMode,
+ 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..1fc715f54b2 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,11 @@ namespace System.Windows.Forms;
[SRDescription(nameof(SR.DescriptionDataGridView))]
public partial class DataGridView : Control, ISupportInitialize
{
+ private protected override VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode)
+ => mode < VisualStylesMode.Net11 || AppContextSwitches.DataGridViewModernRendering
+ ? mode
+ : VisualStylesMode.Classic;
+
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..a829b6b1c51 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,11 @@ namespace System.Windows.Forms;
[DefaultEvent(nameof(ItemClicked))]
public partial class ToolStrip : ScrollableControl, IArrangedElement, ISupportToolStripPanel
{
+ private protected override VisualStylesMode GetSupportedVisualStylesMode(VisualStylesMode mode)
+ => mode < VisualStylesMode.Net11 || AppContextSwitches.ToolStripModernRendering
+ ? mode
+ : VisualStylesMode.Classic;
+
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/ControlTests.VisualStylesMode.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.VisualStylesMode.cs
index 80983fcfe0b..aad305e074c 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
@@ -563,6 +563,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()
{
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..60d8e5baf67 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,80 @@ 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 AppContextSwitchScope scope = new(
+ WinFormsAppContextSwitchNames.DataGridViewModernRendering,
+ enable: false);
+ 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_ModernRenderingEnabled_UsesRequestedEffectiveMode(
+ VisualStylesMode value)
+ {
+ using AppContextSwitchScope scope = new(
+ WinFormsAppContextSwitchNames.DataGridViewModernRendering,
+ enable: true);
+ using SubDataGridView control = new() { VisualStylesMode = value };
+
+ Assert.Equal(value, control.VisualStylesMode);
+ Assert.Equal(value, 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
public const string TreeNodeCollectionAddRangeRespectsSortOrder
= "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder";
+
+ ///
+ /// The switch that controls whether ToolStrip controls, including MenuStrip, use modern rendering.
+ ///
+ public const string ToolStripModernRendering
+ = "System.Windows.Forms.ToolStripModernRendering";
+
+ ///
+ /// The switch that controls whether DataGridView controls use modern rendering.
+ ///
+ public const string DataGridViewModernRendering
+ = "System.Windows.Forms.DataGridViewModernRendering";
}