diff --git a/docs/design/FeatureSwitches.md b/docs/design/FeatureSwitches.md index d90f10718d52..1befc0728ee9 100644 --- a/docs/design/FeatureSwitches.md +++ b/docs/design/FeatureSwitches.md @@ -6,6 +6,7 @@ The following switches are toggled for applications running on Mono for `TrimMod | MSBuild Property Name | AppContext Setting | Description | |-|-|-| +| MauiCssEnabled | Microsoft.Maui.RuntimeFeature.IsCssEnabled | When disabled, CSS stylesheets cannot be used. Defaults to `false` when no `MauiCss` items are present in the project, and `true` otherwise. | | MauiEnableIVisualAssemblyScanning | Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled | When enabled, MAUI will scan assemblies for types implementing `IVisual` and for `[assembly: Visual(...)]` attributes and register these types. | | MauiShellSearchResultsRendererDisplayMemberNameSupported | Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported | When disabled, it is necessary to always set `ItemTemplate` of any `SearchHandler`. Displaying search results through `DisplayMemberName` will not work. | | MauiQueryPropertyAttributeSupport | Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported | When disabled, the `[QueryProperty(...)]` attributes won't be used to set values to properties when navigating. | @@ -18,6 +19,22 @@ The following switches are toggled for applications running on Mono for `TrimMod | EnableMauiDiagnostics | Microsoft.Maui.RuntimeFeature.EnableMauiDiagnostics | Enables MAUI specific diagnostics, like VisualDiagnostics and BindingDiagnostics. Defaults to EnableDiagnostics | | _EnableMauiAspire | Microsoft.Maui.RuntimeFeature.EnableMauiAspire | When enabled, MAUI Aspire integration features are available. **Warning**: Using Aspire outside of Debug configuration may introduce performance and security risks in production. | +## MauiCssEnabled + +When this feature is disabled, CSS stylesheets cannot be parsed or applied to UI elements. Any attempt to use CSS will throw a `NotSupportedException`. + +**Default behavior**: This feature is automatically disabled when your project has no `MauiCss` items (CSS files). If your project includes any CSS files, the feature is automatically enabled. + +**When to enable manually**: If your app loads CSS stylesheets dynamically at runtime (e.g., from a network source or embedded resources) rather than including them as `MauiCss` build items, you need to explicitly enable this feature: + +```xml + + true + +``` + +**Trimming benefits**: When disabled, the .NET trimmer can eliminate CSS-related code paths, reducing the final application size. + ## MauiEnableIVisualAssemblyScanning When this feature is not enabled, custom and third party `IVisual` types will not be automatically discovered and registered. diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets index 6eee58090437..216553d77887 100644 --- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets +++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets @@ -303,6 +303,7 @@ false true false + false false @@ -339,10 +340,14 @@ Condition="'$(MauiHybridWebViewSupported)' != ''" Value="$(MauiHybridWebViewSupported)" Trim="true" /> + + Trim="true" /> > LoadStyleSheets() { var properties = new Dictionary>(StringComparer.Ordinal); + + if (!RuntimeFeature.IsCssEnabled) + return properties; + if (DisableCSS) return properties; - var assembly = typeof(StylePropertyAttribute).Assembly; - var styleAttributes = assembly.GetCustomAttributesSafe(typeof(StylePropertyAttribute)); - var stylePropertiesLength = styleAttributes?.Length ?? 0; - for (var i = 0; i < stylePropertiesLength; i++) + + // Note: these attributes were previously used as actual attributes [assembly: StylePropertyAttribute(...)] + // but this caused problem for trimming, so instead of scanning for the global assemblies, the attributes were moved here + // with the least amount of changes to the existing code. + StylePropertyAttribute[] styleAttributes = [ + new StylePropertyAttribute("background-color", typeof(VisualElement), nameof(VisualElement.BackgroundColorProperty)), + new StylePropertyAttribute("background", typeof(VisualElement), nameof(VisualElement.BackgroundProperty)), + new StylePropertyAttribute("background-image", typeof(Page), nameof(Page.BackgroundImageSourceProperty)), + new StylePropertyAttribute("border-color", typeof(IBorderElement), nameof(BorderElement.BorderColorProperty)), + new StylePropertyAttribute("border-color", typeof(IBorderView), nameof(Border.StrokeProperty)), + new StylePropertyAttribute("border-radius", typeof(ICornerElement), nameof(CornerElement.CornerRadiusProperty)), + new StylePropertyAttribute("border-radius", typeof(Button), nameof(Button.CornerRadiusProperty)), + #pragma warning disable CS0618 // Type or member is obsolete + new StylePropertyAttribute("border-radius", typeof(Frame), nameof(Frame.CornerRadiusProperty)), + #pragma warning restore CS0618 // Type or member is obsolete + new StylePropertyAttribute("border-radius", typeof(IBorderView), nameof(Border.StrokeShapeProperty)), + new StylePropertyAttribute("border-radius", typeof(ImageButton), nameof(BorderElement.CornerRadiusProperty)), + new StylePropertyAttribute("border-width", typeof(IBorderElement), nameof(BorderElement.BorderWidthProperty)), + new StylePropertyAttribute("border-width", typeof(IBorderView), nameof(Border.StrokeThicknessProperty)), + new StylePropertyAttribute("color", typeof(IColorElement), nameof(ColorElement.ColorProperty)) { Inherited = true }, + new StylePropertyAttribute("color", typeof(ITextElement), nameof(TextElement.TextColorProperty)) { Inherited = true }, + new StylePropertyAttribute("text-transform", typeof(ITextElement), nameof(TextElement.TextTransformProperty)) { Inherited = true }, + new StylePropertyAttribute("color", typeof(ProgressBar), nameof(ProgressBar.ProgressColorProperty)), + new StylePropertyAttribute("color", typeof(Switch), nameof(Switch.OnColorProperty)), + new StylePropertyAttribute("column-gap", typeof(Grid), nameof(Grid.ColumnSpacingProperty)), + new StylePropertyAttribute("direction", typeof(VisualElement), nameof(VisualElement.FlowDirectionProperty)) { Inherited = true }, + new StylePropertyAttribute("font-family", typeof(IFontElement), nameof(FontElement.FontFamilyProperty)) { Inherited = true }, + new StylePropertyAttribute("font-size", typeof(IFontElement), nameof(FontElement.FontSizeProperty)) { Inherited = true }, + new StylePropertyAttribute("font-style", typeof(IFontElement), nameof(FontElement.FontAttributesProperty)) { Inherited = true }, + new StylePropertyAttribute("height", typeof(VisualElement), nameof(VisualElement.HeightRequestProperty)), + new StylePropertyAttribute("margin", typeof(View), nameof(View.MarginProperty)), + new StylePropertyAttribute("margin-left", typeof(View), nameof(View.MarginLeftProperty)), + new StylePropertyAttribute("margin-top", typeof(View), nameof(View.MarginTopProperty)), + new StylePropertyAttribute("margin-right", typeof(View), nameof(View.MarginRightProperty)), + new StylePropertyAttribute("margin-bottom", typeof(View), nameof(View.MarginBottomProperty)), + new StylePropertyAttribute("max-lines", typeof(Label), nameof(Label.MaxLinesProperty)), + new StylePropertyAttribute("min-height", typeof(VisualElement), nameof(VisualElement.MinimumHeightRequestProperty)), + new StylePropertyAttribute("min-width", typeof(VisualElement), nameof(VisualElement.MinimumWidthRequestProperty)), + new StylePropertyAttribute("opacity", typeof(VisualElement), nameof(VisualElement.OpacityProperty)), + new StylePropertyAttribute("padding", typeof(IPaddingElement), nameof(PaddingElement.PaddingProperty)), + new StylePropertyAttribute("padding-left", typeof(IPaddingElement), nameof(PaddingElement.PaddingLeftProperty)) { PropertyOwnerType = typeof(PaddingElement) }, + new StylePropertyAttribute("padding-top", typeof(IPaddingElement), nameof(PaddingElement.PaddingTopProperty)) { PropertyOwnerType = typeof(PaddingElement) }, + new StylePropertyAttribute("padding-right", typeof(IPaddingElement), nameof(PaddingElement.PaddingRightProperty)) { PropertyOwnerType = typeof(PaddingElement) }, + new StylePropertyAttribute("padding-bottom", typeof(IPaddingElement), nameof(PaddingElement.PaddingBottomProperty)) { PropertyOwnerType = typeof(PaddingElement) }, + new StylePropertyAttribute("row-gap", typeof(Grid), nameof(Grid.RowSpacingProperty)), + new StylePropertyAttribute("text-align", typeof(ITextAlignmentElement), nameof(TextAlignmentElement.HorizontalTextAlignmentProperty)) { Inherited = true }, + new StylePropertyAttribute("text-decoration", typeof(IDecorableTextElement), nameof(DecorableTextElement.TextDecorationsProperty)), + new StylePropertyAttribute("transform", typeof(VisualElement), nameof(VisualElement.TransformProperty)), + new StylePropertyAttribute("transform-origin", typeof(VisualElement), nameof(VisualElement.TransformOriginProperty)), + new StylePropertyAttribute("vertical-align", typeof(ITextAlignmentElement), nameof(TextAlignmentElement.VerticalTextAlignmentProperty)), + new StylePropertyAttribute("visibility", typeof(VisualElement), nameof(VisualElement.IsVisibleProperty)) { Inherited = true }, + new StylePropertyAttribute("width", typeof(VisualElement), nameof(VisualElement.WidthRequestProperty)), + new StylePropertyAttribute("letter-spacing", typeof(ITextElement), nameof(TextElement.CharacterSpacingProperty)) { Inherited = true }, +#pragma warning disable CS0618 // Type or member is obsolete + new StylePropertyAttribute("line-height", typeof(ILineHeightElement), nameof(LineHeightElement.LineHeightProperty)) { Inherited = true }, +#pragma warning restore CS0618 // Type or member is obsolete + + //flex + new StylePropertyAttribute("align-content", typeof(FlexLayout), nameof(FlexLayout.AlignContentProperty)), + new StylePropertyAttribute("align-items", typeof(FlexLayout), nameof(FlexLayout.AlignItemsProperty)), + new StylePropertyAttribute("align-self", typeof(VisualElement), nameof(FlexLayout.AlignSelfProperty)) { PropertyOwnerType = typeof(FlexLayout) }, + new StylePropertyAttribute("flex-direction", typeof(FlexLayout), nameof(FlexLayout.DirectionProperty)), + new StylePropertyAttribute("flex-basis", typeof(VisualElement), nameof(FlexLayout.BasisProperty)) { PropertyOwnerType = typeof(FlexLayout) }, + new StylePropertyAttribute("flex-grow", typeof(VisualElement), nameof(FlexLayout.GrowProperty)) { PropertyOwnerType = typeof(FlexLayout) }, + new StylePropertyAttribute("flex-shrink", typeof(VisualElement), nameof(FlexLayout.ShrinkProperty)) { PropertyOwnerType = typeof(FlexLayout) }, + new StylePropertyAttribute("flex-wrap", typeof(VisualElement), nameof(FlexLayout.WrapProperty)) { PropertyOwnerType = typeof(FlexLayout) }, + new StylePropertyAttribute("justify-content", typeof(FlexLayout), nameof(FlexLayout.JustifyContentProperty)), + new StylePropertyAttribute("order", typeof(VisualElement), nameof(FlexLayout.OrderProperty)) { PropertyOwnerType = typeof(FlexLayout) }, + new StylePropertyAttribute("position", typeof(FlexLayout), nameof(FlexLayout.PositionProperty)), + + //xf specific + new StylePropertyAttribute("-maui-placeholder", typeof(IPlaceholderElement), nameof(PlaceholderElement.PlaceholderProperty)), + new StylePropertyAttribute("-maui-placeholder-color", typeof(IPlaceholderElement), nameof(PlaceholderElement.PlaceholderColorProperty)), + new StylePropertyAttribute("-maui-max-length", typeof(InputView), nameof(InputView.MaxLengthProperty)), + new StylePropertyAttribute("-maui-bar-background-color", typeof(IBarElement), nameof(BarElement.BarBackgroundColorProperty)), + new StylePropertyAttribute("-maui-bar-text-color", typeof(IBarElement), nameof(BarElement.BarTextColorProperty)), + new StylePropertyAttribute("-maui-orientation", typeof(ScrollView), nameof(ScrollView.OrientationProperty)), + new StylePropertyAttribute("-maui-horizontal-scroll-bar-visibility", typeof(ScrollView), nameof(ScrollView.HorizontalScrollBarVisibilityProperty)), + new StylePropertyAttribute("-maui-vertical-scroll-bar-visibility", typeof(ScrollView), nameof(ScrollView.VerticalScrollBarVisibilityProperty)), + new StylePropertyAttribute("-maui-min-track-color", typeof(Slider), nameof(Slider.MinimumTrackColorProperty)), + new StylePropertyAttribute("-maui-max-track-color", typeof(Slider), nameof(Slider.MaximumTrackColorProperty)), + new StylePropertyAttribute("-maui-thumb-color", typeof(Slider), nameof(Slider.ThumbColorProperty)), + new StylePropertyAttribute("-maui-spacing", typeof(StackBase), nameof(StackBase.SpacingProperty)), + new StylePropertyAttribute("-maui-orientation", typeof(StackLayout), nameof(StackLayout.OrientationProperty)), + + new StylePropertyAttribute("-maui-visual", typeof(VisualElement), nameof(VisualElement.VisualProperty)), + new StylePropertyAttribute("-maui-vertical-text-alignment", typeof(Label), nameof(TextAlignmentElement.VerticalTextAlignmentProperty)), + new StylePropertyAttribute("-maui-thumb-color", typeof(Switch), nameof(Switch.ThumbColorProperty)), + + new StylePropertyAttribute("-maui-shadow", typeof(VisualElement), nameof(VisualElement.ShadowProperty)), + + //shell + new StylePropertyAttribute("-maui-flyout-background", typeof(Shell), nameof(Shell.FlyoutBackgroundColorProperty)), + new StylePropertyAttribute("-maui-shell-background", typeof(Element), nameof(Shell.BackgroundColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-disabled", typeof(Element), nameof(Shell.DisabledColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-foreground", typeof(Element), nameof(Shell.ForegroundColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-tabbar-background", typeof(Element), nameof(Shell.TabBarBackgroundColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-tabbar-disabled", typeof(Element), nameof(Shell.TabBarDisabledColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-tabbar-foreground", typeof(Element), nameof(Shell.TabBarForegroundColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-tabbar-title", typeof(Element), nameof(Shell.TabBarTitleColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-tabbar-unselected", typeof(Element), nameof(Shell.TabBarUnselectedColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-title", typeof(Element), nameof(Shell.TitleColorProperty)) { PropertyOwnerType = typeof(Shell) }, + new StylePropertyAttribute("-maui-shell-unselected", typeof(Element), nameof(Shell.UnselectedColorProperty)) { PropertyOwnerType = typeof(Shell) }, + ]; + + foreach (var attribute in styleAttributes) { - var attribute = (StylePropertyAttribute)styleAttributes[i]; if (properties.TryGetValue(attribute.CssPropertyName, out var attrList)) attrList.Add(attribute); else diff --git a/src/Controls/src/Core/StyleSheets/Style.cs b/src/Controls/src/Core/StyleSheets/Style.cs index 16d8458dde39..b0d5e9d2f166 100644 --- a/src/Controls/src/Core/StyleSheets/Style.cs +++ b/src/Controls/src/Core/StyleSheets/Style.cs @@ -19,6 +19,12 @@ sealed class Style public static Style Parse(CssReader reader, char stopChar = '\0') { + if (!RuntimeFeature.IsCssEnabled) + throw new NotSupportedException( + "CSS stylesheets are disabled because no MauiCss items were found in the project. " + + "To enable CSS support, add true to your project file, " + + "or add CSS files as MauiCss build items."); + Style style = new Style(); string propertyName = null, propertyValue = null; @@ -62,6 +68,12 @@ public static Style Parse(CssReader reader, char stopChar = '\0') public void Apply(VisualElement styleable, Selector.SelectorSpecificity selectorSpecificity = default, bool inheriting = false) { + if (!RuntimeFeature.IsCssEnabled) + throw new NotSupportedException( + "CSS stylesheets are disabled because no MauiCss items were found in the project. " + + "To enable CSS support, add true to your project file, " + + "or add CSS files as MauiCss build items."); + if (styleable == null) throw new ArgumentNullException(nameof(styleable)); diff --git a/src/Controls/src/Core/VisualElement/VisualElement_StyleSheet.cs b/src/Controls/src/Core/VisualElement/VisualElement_StyleSheet.cs index ea95013515a1..66dc90be2091 100644 --- a/src/Controls/src/Core/VisualElement/VisualElement_StyleSheet.cs +++ b/src/Controls/src/Core/VisualElement/VisualElement_StyleSheet.cs @@ -1,4 +1,5 @@ #nullable disable +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -16,6 +17,12 @@ public partial class VisualElement : IStylable { BindableProperty IStylable.GetProperty(string key, bool inheriting) { + if (!RuntimeFeature.IsCssEnabled) + throw new NotSupportedException( + "CSS stylesheets are disabled because no MauiCss items were found in the project. " + + "To enable CSS support, add true to your project file, " + + "or add CSS files as MauiCss build items."); + if (!Internals.Registrar.StyleProperties.TryGetValue(key, out var attrList)) return null; diff --git a/src/Controls/tests/Core.UnitTests/StyleSheets/StyleTests.cs b/src/Controls/tests/Core.UnitTests/StyleSheets/StyleTests.cs index 6b0aa9e54086..84ec6d0819cd 100644 --- a/src/Controls/tests/Core.UnitTests/StyleSheets/StyleTests.cs +++ b/src/Controls/tests/Core.UnitTests/StyleSheets/StyleTests.cs @@ -8,7 +8,9 @@ namespace Microsoft.Maui.Controls.StyleSheets.UnitTests { using StackLayout = Microsoft.Maui.Controls.Compatibility.StackLayout; - + // All CSS-related tests share this collection to prevent parallel execution, + // since CssDisabledTests modifies global AppContext state + [Collection("StyleSheet")] public class StyleTests : BaseTestFixture { public StyleTests() @@ -169,4 +171,65 @@ public void CSSStyleAppliedAfterReEnablingInitiallyDisabledButton_Issue12550() } } -} \ No newline at end of file + + // All CSS-related tests share this collection to prevent parallel execution, + // since CssDisabledTests modifies global AppContext state + [Collection("StyleSheet")] + public class CssDisabledTests : BaseTestFixture + { + const string CssSwitchName = "Microsoft.Maui.RuntimeFeature.IsCssEnabled"; + readonly bool _originalCssEnabled; + + public CssDisabledTests() + { + // Save original value + AppContext.TryGetSwitch(CssSwitchName, out _originalCssEnabled); + + // Disable CSS for these tests + AppContext.SetSwitch(CssSwitchName, false); + ApplicationExtensions.CreateAndSetMockApplication(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // Restore original CSS setting + AppContext.SetSwitch(CssSwitchName, _originalCssEnabled); + Application.ClearCurrent(); + } + + base.Dispose(disposing); + } + + [Fact] + public void StyleParseThrowsWhenCssDisabled() + { + var styleString = @"background-color: #ff0000;"; + Assert.Throws(() => + Style.Parse(new CssReader(new StringReader(styleString)), '}')); + } + + [Fact] + public void SettingStyleClassDoesNotThrowWhenCssDisabled() + { + // This simulates what happens in the test host app when CSS is disabled + // Setting StyleClass should not throw even when CSS is disabled + var label = new Label(); + + // This should NOT throw - the app should work even when CSS is disabled + // and no CSS stylesheets are actually used + label.StyleClass = new[] { "myclass" }; + } + + [Fact] + public void GetPropertyThrowsWhenCssDisabled() + { + // When CSS is disabled, GetProperty should throw because CSS operations are not supported + var ve = new VisualElement(); + var stylable = (IStylable)ve; + + Assert.Throws(() => stylable.GetProperty("background-color", false)); + } + } +} diff --git a/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj b/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj index 10150dce2aac..d6befbbd7449 100644 --- a/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj +++ b/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj @@ -8,6 +8,8 @@ true false enable + + true maccatalyst-x64;maccatalyst-arm64 iossimulator-x64;iossimulator-arm64 diff --git a/src/Core/src/RuntimeFeature.cs b/src/Core/src/RuntimeFeature.cs index 0bce1aefc61d..71e12cb91cb3 100644 --- a/src/Core/src/RuntimeFeature.cs +++ b/src/Core/src/RuntimeFeature.cs @@ -28,6 +28,7 @@ static class RuntimeFeature const bool IsMeterSupportedByDefault = true; const bool EnableAspireByDefault = true; const bool IsMaterial3EnabledByDefault = false; + const bool IsCssEnabledByDefault = true; #pragma warning disable IL4000 // Return value does not match FeatureGuardAttribute 'System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute'. #if NET9_0_OR_GREATER @@ -157,5 +158,13 @@ internal set : IsMaterial3EnabledByDefault; #pragma warning restore IL4000 + +#if NET9_0_OR_GREATER + [FeatureSwitchDefinition("Microsoft.Maui.RuntimeFeature.IsCssEnabled")] +#endif + internal static bool IsCssEnabled => + AppContext.TryGetSwitch("Microsoft.Maui.RuntimeFeature.IsCssEnabled", out bool isEnabled) + ? isEnabled + : IsCssEnabledByDefault; } }