From 9b0ad866bde931f033999d5f292a61dfea6c9d24 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 10:56:32 +0100 Subject: [PATCH 01/13] [net11.0][XSG] Trimmable Styles Squashed for clean rebase. --- .../BindablePropertyConverter.cs | 46 ++- .../src/Build.Tasks/CreateObjectVisitor.cs | 4 + .../src/Build.Tasks/ExpandMarkupsVisitor.cs | 4 + .../src/Build.Tasks/SetFieldVisitor.cs | 4 + .../SetNamescopesAndRegisterNamesVisitor.cs | 4 + .../src/Build.Tasks/SetPropertiesVisitor.cs | 4 + .../src/Build.Tasks/SetResourcesVisitor.cs | 4 + src/Controls/src/Core/IStyle.cs | 6 +- src/Controls/src/Core/MergedStyle.cs | 16 +- .../net-android/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 4 + .../net-maccatalyst/PublicAPI.Unshipped.txt | 4 + .../net-tizen/PublicAPI.Unshipped.txt | 3 + .../net-windows/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net/PublicAPI.Unshipped.txt | 3 + .../netstandard/PublicAPI.Unshipped.txt | 2 + src/Controls/src/Core/ResourceDictionary.cs | 2 +- src/Controls/src/Core/Style.cs | 117 +++++- .../src/SourceGen/NodeSGExtensions.cs | 8 + .../SourceGen/Visitors/CreateValuesVisitor.cs | 103 ++++- .../Visitors/ExpandMarkupsVisitor.cs | 3 + .../Visitors/SetFieldsForXNamesVisitor.cs | 7 +- .../Visitors/SetNamescopesAndRegisterNames.cs | 33 +- .../Visitors/SetPropertiesVisitor.cs | 169 ++++++++- .../SourceGen/Visitors/SetResourcesVisitor.cs | 14 +- .../src/Xaml/ApplyPropertiesVisitor.cs | 3 + src/Controls/src/Xaml/CreateValuesVisitor.cs | 3 + src/Controls/src/Xaml/ExpandMarkupsVisitor.cs | 3 + .../Xaml/FillResourceDictionariesVisitor.cs | 3 + src/Controls/src/Xaml/NamescopingVisitor.cs | 3 + .../src/Xaml/PruneIgnoredNodesVisitor.cs | 3 + .../src/Xaml/RegisterXNamesVisitor.cs | 3 + .../src/Xaml/RemoveDuplicateDesignNodes.cs | 3 + .../src/Xaml/SimplifyOnPlatformVisitor.cs | 3 + .../src/Xaml/SimplifyTypeExtensionVisitor.cs | 3 + src/Controls/src/Xaml/XamlNode.cs | 17 +- src/Controls/src/Xaml/XamlNodeVisitor.cs | 6 + src/Controls/src/Xaml/XmlName.cs | 1 + .../tests/Core.UnitTests/StyleTests.cs | 195 ++++++++++ .../SetterCompiledConverters.cs | 3 +- .../InitializeComponent/SimplifyOnPlatform.cs | 5 +- .../SourceGen.UnitTests/Maui32879Tests.cs | 1 + .../StyleSourceGenTests.cs | 351 ++++++++++++++++++ .../Xaml.UnitTests/Issues/Bz51567.xaml.cs | 5 + .../Xaml.UnitTests/Issues/Maui21757.xaml.cs | 10 + .../Issues/Unreported009.xaml.cs | 5 + .../tests/Xaml.UnitTests/StyleTests.xaml.cs | 13 +- 47 files changed, 1144 insertions(+), 68 deletions(-) create mode 100644 src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs diff --git a/src/Controls/src/Build.Tasks/CompiledConverters/BindablePropertyConverter.cs b/src/Controls/src/Build.Tasks/CompiledConverters/BindablePropertyConverter.cs index 881d418251e8..58febacb15a1 100644 --- a/src/Controls/src/Build.Tasks/CompiledConverters/BindablePropertyConverter.cs +++ b/src/Controls/src/Build.Tasks/CompiledConverters/BindablePropertyConverter.cs @@ -38,7 +38,7 @@ public FieldReference GetBindablePropertyFieldReference(string value, ILContext { if (parent.XmlType.IsOfAnyType(nameof(Trigger), nameof(DataTrigger), nameof(MultiTrigger), nameof(Style))) { - typeName = GetTargetTypeName(parent); + typeName = GetTargetTypeFromElement(parent, node.NamespaceResolver, (IXmlLineInfo)node); } else if (parent.XmlType.IsOfAnyType(nameof(VisualState))) { @@ -47,7 +47,7 @@ public FieldReference GetBindablePropertyFieldReference(string value, ILContext } else if (node.Parent is ElementNode { XmlType: XmlType xt1 } && xt1.IsOfAnyType(nameof(Trigger))) { - typeName = GetTargetTypeName(node.Parent); + typeName = GetTargetTypeFromElement(node.Parent as ElementNode, node.NamespaceResolver, (IXmlLineInfo)node); } propertyName = parts[0]; } @@ -71,38 +71,44 @@ public FieldReference GetBindablePropertyFieldReference(string value, ILContext if (bpRef == null) throw new BuildException(PropertyResolution, node, null, propertyName, typeRef.Name); return bpRef; - - static XmlType GetTargetTypeName(INode node) - { - var targetType = ((node as ElementNode).Properties[new XmlName("", "TargetType")] as ValueNode)?.Value as string; - return TypeArgumentsParser.ParseSingle(targetType, node.NamespaceResolver, (IXmlLineInfo)node); - } } static XmlType FindTypeNameForVisualState(ElementNode parent, IXmlLineInfo lineInfo, ILContext context) { - //1. parent is VisualState, don't check that + // 1. parent is VisualState, don't check that - //2. check that the VS is in a VSG - // if (!(parent.Parent is IElementNode target) || target.XmlType.NamespaceUri != XamlParser.MauiUri || target.XmlType.Name != nameof(VisualStateGroup)) + // 2. check that the VS is in a VSG if (parent.Parent is not ElementNode target || !target.XmlType.IsOfAnyType(nameof(VisualStateGroup))) throw new XamlParseException($"Expected {nameof(VisualStateGroup)} but found {parent.Parent}", lineInfo); - //3. if the VSG is in a VSGL, skip that as it could be implicit - if ( target.Parent is ListNode + // 3. if the VSG is in a VSGL, skip that as it could be implicit + if (target.Parent is ListNode || target.Parent is ElementNode { XmlType: XmlType xt } && xt.IsOfAnyType(nameof(VisualStateGroupList))) target = target.Parent.Parent as ElementNode; else target = target.Parent as ElementNode; - //4. target is now a Setter in a Style, or a VE + // 4. target is now a Setter in a Style, or a VE if (target.XmlType.IsOfAnyType(nameof(Setter))) - { - var targetType = ((target?.Parent as ElementNode)?.Properties[new XmlName("", "TargetType")] as ValueNode)?.Value as string; - return TypeArgumentsParser.ParseSingle(targetType, parent.NamespaceResolver, lineInfo); - } - else - return target.XmlType; + return GetTargetTypeFromElement(target?.Parent as ElementNode, parent.NamespaceResolver, lineInfo); + + return target.XmlType; + } + + /// + /// Extracts the TargetType attribute from an element node and parses it as an XmlType. + /// Returns null if the element is null, has no TargetType, or TargetType is empty. + /// + static XmlType GetTargetTypeFromElement(ElementNode element, IXmlNamespaceResolver namespaceResolver, IXmlLineInfo lineInfo) + { + if (element?.Properties.TryGetValue(new XmlName("", "TargetType"), out var targetTypeNode) != true) + return null; + + var targetType = (targetTypeNode as ValueNode)?.Value as string; + if (string.IsNullOrEmpty(targetType)) + return null; + + return TypeArgumentsParser.ParseSingle(targetType, namespaceResolver, lineInfo); } public static FieldReference GetBindablePropertyFieldReference(XamlCache cache, TypeReference typeRef, string propertyName, ModuleDefinition module) diff --git a/src/Controls/src/Build.Tasks/CreateObjectVisitor.cs b/src/Controls/src/Build.Tasks/CreateObjectVisitor.cs index 214e1b11bc16..c0513fe8450f 100644 --- a/src/Controls/src/Build.Tasks/CreateObjectVisitor.cs +++ b/src/Controls/src/Build.Tasks/CreateObjectVisitor.cs @@ -20,6 +20,8 @@ class CreateObjectVisitor(ILContext context) : IXamlNodeVisitor public bool StopOnDataTemplate => true; public bool StopOnResourceDictionary => false; public bool VisitNodeOnDataTemplate => false; + public bool StopOnStyle => false; + public bool VisitNodeOnStyle => true; public bool SkipChildren(INode node, INode parentNode) => false; public bool IsResourceDictionary(ElementNode node) @@ -29,6 +31,8 @@ public bool IsResourceDictionary(ElementNode node) || parentVar.VariableType.Resolve().BaseType?.FullName == "Microsoft.Maui.Controls.ResourceDictionary"; } + public bool IsStyle(ElementNode node) => false; + public void Visit(ValueNode node, INode parentNode) { Context.Values[node] = node.Value; diff --git a/src/Controls/src/Build.Tasks/ExpandMarkupsVisitor.cs b/src/Controls/src/Build.Tasks/ExpandMarkupsVisitor.cs index d1688e47d29b..9e8c43da74cf 100644 --- a/src/Controls/src/Build.Tasks/ExpandMarkupsVisitor.cs +++ b/src/Controls/src/Build.Tasks/ExpandMarkupsVisitor.cs @@ -22,6 +22,8 @@ class ExpandMarkupsVisitor(ILContext context) : IXamlNodeVisitor public bool StopOnDataTemplate => false; public bool StopOnResourceDictionary => false; public bool VisitNodeOnDataTemplate => true; + public bool StopOnStyle => false; + public bool VisitNodeOnStyle => true; public bool SkipChildren(INode node, INode parentNode) => false; public bool IsResourceDictionary(ElementNode node) @@ -31,6 +33,8 @@ public bool IsResourceDictionary(ElementNode node) || parentVar.VariableType.Resolve().BaseType?.FullName == "Microsoft.Maui.Controls.ResourceDictionary"; } + public bool IsStyle(ElementNode node) => false; + public void Visit(ValueNode node, INode parentNode) { } diff --git a/src/Controls/src/Build.Tasks/SetFieldVisitor.cs b/src/Controls/src/Build.Tasks/SetFieldVisitor.cs index 114c1dae927c..3544f1848d0e 100644 --- a/src/Controls/src/Build.Tasks/SetFieldVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetFieldVisitor.cs @@ -12,6 +12,8 @@ class SetFieldVisitor(ILContext context) : IXamlNodeVisitor public bool StopOnDataTemplate => true; public bool StopOnResourceDictionary => false; public bool VisitNodeOnDataTemplate => false; + public bool StopOnStyle => false; + public bool VisitNodeOnStyle => true; public bool SkipChildren(INode node, INode parentNode) => false; public bool IsResourceDictionary(ElementNode node) @@ -21,6 +23,8 @@ public bool IsResourceDictionary(ElementNode node) || parentVar.VariableType.Resolve().BaseType?.FullName == "Microsoft.Maui.Controls.ResourceDictionary"; } + public bool IsStyle(ElementNode node) => false; + public void Visit(ValueNode node, INode parentNode) { if (!IsXNameProperty(node, parentNode)) diff --git a/src/Controls/src/Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs b/src/Controls/src/Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs index 6ced1a68d6d3..ac9513bbea1f 100644 --- a/src/Controls/src/Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetNamescopesAndRegisterNamesVisitor.cs @@ -14,6 +14,8 @@ class SetNamescopesAndRegisterNamesVisitor(ILContext context) : IXamlNodeVisitor public bool StopOnDataTemplate => true; public bool StopOnResourceDictionary => false; public bool VisitNodeOnDataTemplate => false; + public bool StopOnStyle => false; + public bool VisitNodeOnStyle => true; public bool SkipChildren(INode node, INode parentNode) => false; public bool IsResourceDictionary(ElementNode node) @@ -23,6 +25,8 @@ public bool IsResourceDictionary(ElementNode node) || parentVar.VariableType.Resolve().BaseType?.FullName == "Microsoft.Maui.Controls.ResourceDictionary"; } + public bool IsStyle(ElementNode node) => false; + public void Visit(ValueNode node, INode parentNode) { Context.Scopes[node] = Context.Scopes[parentNode]; diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index 14954818e9c4..f0343f4351a0 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -32,6 +32,8 @@ class SetPropertiesVisitor(ILContext context, bool stopOnResourceDictionary = fa public TreeVisitingMode VisitingMode => TreeVisitingMode.BottomUp; public bool StopOnDataTemplate => true; public bool VisitNodeOnDataTemplate => true; + public bool StopOnStyle => false; + public bool VisitNodeOnStyle => true; public bool SkipChildren(INode node, INode parentNode) => false; public bool IsResourceDictionary(ElementNode node) @@ -41,6 +43,8 @@ public bool IsResourceDictionary(ElementNode node) || parentVar.VariableType.Resolve().BaseType?.FullName == "Microsoft.Maui.Controls.ResourceDictionary"; } + public bool IsStyle(ElementNode node) => false; + ModuleDefinition Module { get; } = context.Body.Method.Module; // Track properties that have been set to detect duplicates diff --git a/src/Controls/src/Build.Tasks/SetResourcesVisitor.cs b/src/Controls/src/Build.Tasks/SetResourcesVisitor.cs index 0d9758c83f11..f9a256764747 100644 --- a/src/Controls/src/Build.Tasks/SetResourcesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetResourcesVisitor.cs @@ -12,6 +12,10 @@ class SetResourcesVisitor(ILContext context) : IXamlNodeVisitor public bool StopOnDataTemplate => true; public bool StopOnResourceDictionary => false; public bool VisitNodeOnDataTemplate => false; + public bool StopOnStyle => false; + public bool VisitNodeOnStyle => true; + + public bool IsStyle(ElementNode node) => false; public void Visit(ValueNode node, INode parentNode) { diff --git a/src/Controls/src/Core/IStyle.cs b/src/Controls/src/Core/IStyle.cs index 05d45756545b..7c2c8b1a3d66 100644 --- a/src/Controls/src/Core/IStyle.cs +++ b/src/Controls/src/Core/IStyle.cs @@ -1,11 +1,13 @@ -#nullable disable using System; namespace Microsoft.Maui.Controls { interface IStyle { - Type TargetType { get; } + /// + /// Gets the target type for this style. May return null for lazy styles if the type was trimmed. + /// + Type? TargetType { get; } void Apply(BindableObject bindable, SetterSpecificity specificity); void UnApply(BindableObject bindable); diff --git a/src/Controls/src/Core/MergedStyle.cs b/src/Controls/src/Core/MergedStyle.cs index 179315528749..c6449bbd6c64 100644 --- a/src/Controls/src/Core/MergedStyle.cs +++ b/src/Controls/src/Core/MergedStyle.cs @@ -30,10 +30,12 @@ sealed class MergedStyle : IStyle IList _styleClass; + readonly Type _targetType; + public MergedStyle(Type targetType, BindableObject target) { Target = target; - TargetType = targetType; + _targetType = targetType; // RegisterImplicitStyles handles the initial apply via OnImplicitStyleChanged -> SetStyle. // An explicit Apply(Target) call here would double-attach event handlers when // Application.Current.Resources already contains the implicit style (#24152). @@ -47,8 +49,10 @@ public IStyle Style { if (_style == value) return; - if (value != null && !value.TargetType.IsAssignableFrom(TargetType)) - MauiLogger + + +"""; + + var compilation = CreateMauiCompilation(); + var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml)); + + // Check for errors after output + var errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + Assert.Empty(errors); + + // Find the actual generated code (the .xsg.cs file) + var generatedCode = GetGeneratedCode(result); + Assert.Contains("new global::Microsoft.Maui.Controls.Style(\"Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls\")", generatedCode, StringComparison.Ordinal); + Assert.Contains("Label.TextColorProperty", generatedCode, StringComparison.Ordinal); + Assert.Contains("style.LazyInitialization = (__style, __target) =>", generatedCode, StringComparison.Ordinal); + } + + [Fact] + public void StyleWithMultipleSetters() + { + var xaml = +""" + + + + + + +"""; + + var compilation = CreateMauiCompilation(); + var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml)); + + var errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + Assert.Empty(errors); + + var generatedCode = GetGeneratedCode(result); + Assert.Contains("new global::Microsoft.Maui.Controls.Style(\"Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls\")", generatedCode, StringComparison.Ordinal); + Assert.Contains("Label.TextColorProperty", generatedCode, StringComparison.Ordinal); + Assert.Contains("Label.FontSizeProperty", generatedCode, StringComparison.Ordinal); + Assert.Contains("Label.FontAttributesProperty", generatedCode, StringComparison.Ordinal); + Assert.Contains("style.LazyInitialization = (__style, __target) =>", generatedCode, StringComparison.Ordinal); + } + + [Fact] + public void StyleWithoutSetters() + { + var xaml = +""" + + + + + + + +"""; + + var compilation = CreateMauiCompilation(); + var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml)); + + var errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + Assert.Empty(errors); + + var generatedCode = GetGeneratedCode(result); + + // Verify key elements exist + Assert.Contains("new global::Microsoft.Maui.Controls.Style(\"Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls\")", generatedCode, StringComparison.Ordinal); + Assert.Contains("style.LazyInitialization = (__style, __target) =>", generatedCode, StringComparison.Ordinal); + Assert.Contains("label.SetValue(global::Microsoft.Maui.Controls.VisualElement.StyleProperty, style)", generatedCode, StringComparison.Ordinal); + Assert.Contains("Label.TextColorProperty", generatedCode, StringComparison.Ordinal); + + // CRITICAL: Verify the ORDER - Initializer must be set BEFORE SetValue(StyleProperty) + var initializerSetIndex = generatedCode.IndexOf("style.LazyInitialization = (__style, __target) =>", StringComparison.Ordinal); + var setValueIndex = generatedCode.IndexOf("label.SetValue(global::Microsoft.Maui.Controls.VisualElement.StyleProperty, style)", StringComparison.Ordinal); + + Assert.True(initializerSetIndex >= 0, "style.LazyInitialization assignment not found in generated code"); + Assert.True(setValueIndex >= 0, "label.SetValue(StyleProperty) not found in generated code"); + Assert.True(initializerSetIndex < setValueIndex, + $"style.LazyInitialization must be set BEFORE label.SetValue(StyleProperty, style).\n" + + $"Initializer set at index {initializerSetIndex}, SetValue at index {setValueIndex}.\n" + + $"Generated code:\n{generatedCode}"); + } + + [Fact] + public void SimpleStyleWithSetterFullSnapshot() + { + // Full snapshot test to verify the complete lazy style pattern + var xaml = +""" + + + + + + +"""; + + var compilation = CreateMauiCompilation(); + var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml)); + + var errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList(); + Assert.Empty(errors); + + var generatedCode = GetGeneratedCode(result); + var expected = Normalize(""" +//------------------------------------------------------------------------------ +// +// This code was generated by a .NET MAUI source generator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable +#pragma warning disable CS0219 // Variable is assigned but its value is never used +namespace Test; +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=11.0.0.0, Culture=neutral, PublicKeyToken=null", "11.0.0.0")] +public partial class TestPage +{ + private partial void InitializeComponent() + { + // Fallback to Runtime inflation if the page was updated by HotReload + static string? getPathForType(global::System.Type type) + { + var assembly = type.Assembly; + foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes(assembly)) + { + if (xria.Type == type) + return xria.Path; + } + return null; + } + var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery + { + AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(), + ResourcePath = getPathForType(typeof(global::Test.TestPage)), + Instance = this, + }); + if (rlr?.ResourceContent != null) + { + this.InitializeComponentRuntime(); + return; + } + var style = new global::Microsoft.Maui.Controls.Style("Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls"); + global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4); + var __root = this; + global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2); +#if !_MAUIXAML_SG_NAMESCOPE_DISABLE + global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope(); +#endif +#if !_MAUIXAML_SG_NAMESCOPE_DISABLE + global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); +#endif + style.LazyInitialization = (__style, __target) => + { + if (__target is not global::Microsoft.Maui.Controls.Label) return; +#if !_MAUIXAML_SG_NAMESCOPE_DISABLE + global::Microsoft.Maui.Controls.Internals.INameScope iNameScope1 = new global::Microsoft.Maui.Controls.Internals.NameScope(); +#endif +#if !_MAUIXAML_SG_NAMESCOPE_DISABLE + global::Microsoft.Maui.Controls.Internals.INameScope iNameScope2 = new global::Microsoft.Maui.Controls.Internals.NameScope(); +#endif + var setter = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = global::Microsoft.Maui.Graphics.Colors.Red}; + if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter!) == null) + global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 5); +#line 8 "Test.xaml" + ((global::System.Collections.Generic.ICollection)__style.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter); +#line default + }; + __root.Resources["TestStyle"] = style; + } +} +"""); + AssertSnapshot(expected, generatedCode); + } +} diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs index 1cae55d9634a..8a6dc9af5ac6 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs @@ -18,6 +18,11 @@ internal void SetterWithElementValue(XamlInflator inflator) { var page = new Bz51567(inflator); var style = page.Resources["ListText"] as Style; + + // For SourceGen, styles are lazy - force initialization before inspecting Setters + if (inflator == XamlInflator.SourceGen) + style.InitializeIfNeeded(new Label()); + var setter = style.Setters[1]; Assert.NotNull(setter); } diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs index 0a50fd9e45cc..0b55f0a208bc 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs @@ -36,12 +36,22 @@ internal void TypeLiteralAndXTypeCanBeUsedInterchangeably(XamlInflator inflator) var styleA = resourceDictionary["A"] as Style; Assert.NotNull(styleA); + + // For SourceGen, styles are lazy - force initialization before inspecting Setters + if (inflator == XamlInflator.SourceGen) + styleA.InitializeIfNeeded(new BoxView()); + Assert.Equal(typeof(BoxView), styleA.TargetType); Assert.Equal(BoxView.ColorProperty, styleA.Setters[0].Property); Assert.Equal(Color.FromArgb("#C8C8C8"), styleA.Setters[0].Value); var styleB = resourceDictionary["B"] as Style; Assert.NotNull(styleB); + + // For SourceGen, styles are lazy - force initialization before inspecting Setters + if (inflator == XamlInflator.SourceGen) + styleB.InitializeIfNeeded(new BoxView()); + Assert.Equal(typeof(BoxView), styleB.TargetType); Assert.Equal(BoxView.ColorProperty, styleB.Setters[0].Property); Assert.Equal(Color.FromArgb("#C8C8C8"), styleB.Setters[0].Value); diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs index 15be4a93661d..31e0e676895c 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs @@ -15,6 +15,11 @@ internal void AllowSetterValueAsElementProperties(XamlInflator inflator) { var p = new Unreported009(inflator); var s = p.Resources["Default"] as Style; + + // For SourceGen, styles are lazy - force initialization before inspecting Setters + if (inflator == XamlInflator.SourceGen) + s.InitializeIfNeeded(new ContentView()); + Assert.Equal("Bananas!", (s.Setters[0].Value as Label).Text); } } diff --git a/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs b/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs index 0230af0f9aa9..7f4d219ccaeb 100644 --- a/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs @@ -33,6 +33,11 @@ internal void TestConversionOnSetters(XamlInflator inflator) { var layout = new StyleTests(inflator); Style style = layout.style1; + + // For SourceGen, styles are lazy - force initialization before inspecting Setters + if (inflator == XamlInflator.SourceGen) + style.InitializeIfNeeded(new Label()); + Setter setter; //Test built-in conversions @@ -65,6 +70,11 @@ internal void PropertyDoesNotNeedTypes(XamlInflator inflator) { var layout = new StyleTests(inflator); Style style2 = layout.style2; + + // For SourceGen, styles are lazy - force initialization before inspecting Setters + if (inflator == XamlInflator.SourceGen) + style2.InitializeIfNeeded(new Label()); + var s0 = style2.Setters[0]; var s1 = style2.Setters[1]; Assert.Equal(Label.TextProperty, s0.Property); @@ -120,7 +130,8 @@ public partial class StyleTests : ContentPage .RunMauiSourceGenerator(typeof(StyleTests)); Assert.False(result.Diagnostics.Any()); var initComp = result.GeneratedInitializeComponent(); - Assert.Contains("new global::Microsoft.Maui.Controls.Style(typeof(global::Microsoft.Maui.Controls.Label))", initComp, StringComparison.InvariantCulture); + // Trimmable styles use string-based constructor for AOT compatibility + Assert.Contains("new global::Microsoft.Maui.Controls.Style(\"Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls\")", initComp, StringComparison.InvariantCulture); } } } \ No newline at end of file From 64ede1b94de82d9c7b1a1f27ce1efe506da6ee17 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 11:34:35 +0100 Subject: [PATCH 02/13] Remove InitializeIfNeeded, inline into IStyle.Apply - Inline the lazy initialization lock+check directly into IStyle.Apply - Remove the internal InitializeIfNeeded method (was only needed for tests) - Update tests to use ((IStyle)style).Apply() instead of InitializeIfNeeded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Controls/src/Core/Style.cs | 27 ++++++------------- .../StyleSourceGenTests.cs | 4 +-- .../Xaml.UnitTests/Issues/Bz51567.xaml.cs | 2 +- .../Xaml.UnitTests/Issues/Maui21757.xaml.cs | 4 +-- .../Issues/Unreported009.xaml.cs | 2 +- .../tests/Xaml.UnitTests/StyleTests.xaml.cs | 4 +-- 6 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/Controls/src/Core/Style.cs b/src/Controls/src/Core/Style.cs index 22d2e498dd5d..8d109c9991db 100644 --- a/src/Controls/src/Core/Style.cs +++ b/src/Controls/src/Core/Style.cs @@ -141,7 +141,14 @@ public string BaseResourceKey void IStyle.Apply(BindableObject bindable, SetterSpecificity specificity) { - InitializeIfNeeded(bindable); + lock (_initializerLock) + { + if (LazyInitialization is not null) + { + LazyInitialization(this, bindable); + LazyInitialization = null; + } + } lock (_targets) { @@ -203,24 +210,6 @@ internal ReadOnlySpan TargetTypeFullName } } - /// - /// Initializes the lazy style if it hasn't been initialized yet. - /// This is primarily intended for testing scenarios where setters need to be inspected - /// before the style is applied to any element. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - internal void InitializeIfNeeded(BindableObject target) - { - lock (_initializerLock) - { - if (LazyInitialization is null) - return; - - LazyInitialization(this, target); - LazyInitialization = null; - } - } - void IStyle.UnApply(BindableObject bindable) { UnApplyCore(bindable, BasedOn ?? GetBasedOnResource(bindable)); diff --git a/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs b/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs index 60d7a67f17fa..33d52ce57c66 100644 --- a/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs @@ -206,8 +206,8 @@ public void StyleInitializerIsSetBeforeStyleIsAppliedToElement() { // This test verifies that when a Style with Setters is applied to an element, // the Initializer is assigned BEFORE the SetValue(StyleProperty, style) call. - // This is critical because IStyle.Apply calls InitializeIfNeeded which needs - // the _initializer to be set, otherwise Setters won't be populated. + // This is critical because IStyle.Apply runs the lazy initializer which needs + // to be set, otherwise Setters won't be populated. var xaml = """ diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs index 8a6dc9af5ac6..e5cd5fd29794 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs @@ -21,7 +21,7 @@ internal void SetterWithElementValue(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - style.InitializeIfNeeded(new Label()); + ((IStyle)style).Apply(new Label(), new SetterSpecificity()); var setter = style.Setters[1]; Assert.NotNull(setter); diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs index 0b55f0a208bc..e3ac9f086320 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs @@ -39,7 +39,7 @@ internal void TypeLiteralAndXTypeCanBeUsedInterchangeably(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - styleA.InitializeIfNeeded(new BoxView()); + ((IStyle)styleA).Apply(new BoxView(), new SetterSpecificity()); Assert.Equal(typeof(BoxView), styleA.TargetType); Assert.Equal(BoxView.ColorProperty, styleA.Setters[0].Property); @@ -50,7 +50,7 @@ internal void TypeLiteralAndXTypeCanBeUsedInterchangeably(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - styleB.InitializeIfNeeded(new BoxView()); + ((IStyle)styleB).Apply(new BoxView(), new SetterSpecificity()); Assert.Equal(typeof(BoxView), styleB.TargetType); Assert.Equal(BoxView.ColorProperty, styleB.Setters[0].Property); diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs index 31e0e676895c..b1c79b638972 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs @@ -18,7 +18,7 @@ internal void AllowSetterValueAsElementProperties(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - s.InitializeIfNeeded(new ContentView()); + ((IStyle)s).Apply(new ContentView(), new SetterSpecificity()); Assert.Equal("Bananas!", (s.Setters[0].Value as Label).Text); } diff --git a/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs b/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs index 7f4d219ccaeb..7c7cba2bd56c 100644 --- a/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs @@ -36,7 +36,7 @@ internal void TestConversionOnSetters(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - style.InitializeIfNeeded(new Label()); + ((IStyle)style).Apply(new Label(), new SetterSpecificity()); Setter setter; @@ -73,7 +73,7 @@ internal void PropertyDoesNotNeedTypes(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - style2.InitializeIfNeeded(new Label()); + ((IStyle)style2).Apply(new Label(), new SetterSpecificity()); var s0 = style2.Setters[0]; var s1 = style2.Setters[1]; From 597eec1d20936cb7ff88f07b44fd1e202fe01901 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 11:36:23 +0100 Subject: [PATCH 03/13] Remove duplicate *REMOVED* entry from PublicAPI.Unshipped.txt Duplicate was introduced during rebase conflict resolution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt | 1 - src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt | 1 - .../src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt | 1 - .../src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt | 1 - .../src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt | 1 - src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt | 1 - 6 files changed, 6 deletions(-) diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index fba3d42e7462..9046e88e35b2 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -47,6 +47,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnInterceptTouchEvent(Android.Views.MotionEvent e) -> bool ~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnTouchEvent(Android.Views.MotionEvent e) -> bool ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList -*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 9edd5c813beb..c0fe4ec62e68 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -48,7 +48,6 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList -*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 9edd5c813beb..c0fe4ec62e68 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -48,7 +48,6 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList -*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 0ba2cd2fc7f6..69c5209d4140 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -45,6 +45,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(string key, System.Func factory, bool shared = true) -> void ~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList -*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 0ba2cd2fc7f6..69c5209d4140 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -45,6 +45,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(string key, System.Func factory, bool shared = true) -> void ~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList -*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 0ba2cd2fc7f6..69c5209d4140 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -45,6 +45,5 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~Microsoft.Maui.Controls.ResourceDictionary.AddFactory(string key, System.Func factory, bool shared = true) -> void ~const Microsoft.Maui.Controls.AppThemeBinding.AppThemeResource = "__MAUI_ApplicationTheme__" -> string ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList -*REMOVED*~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> System.Collections.Generic.IList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void From 82c28641f858ee4bf25ce9f08aa69b34a2ac962e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 11:52:05 +0100 Subject: [PATCH 04/13] Fix null reference issues in CanBeAppliedTo and TargetTypeFullName - CanBeAppliedTo: add null check after BaseType walk (reaches null at top of hierarchy before hitting Element) - TargetTypeFullName: guard against Type.FullName being null for generic/special types; fall through to AQN parsing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Controls/src/Core/Style.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Controls/src/Core/Style.cs b/src/Controls/src/Core/Style.cs index 8d109c9991db..55f1437ca4a9 100644 --- a/src/Controls/src/Core/Style.cs +++ b/src/Controls/src/Core/Style.cs @@ -189,8 +189,8 @@ internal ReadOnlySpan TargetTypeFullName { get { - // If we have the type already, use it - if (_targetType is not null) + // If we have the type already, use it (FullName may be null for special/generic types) + if (_targetType?.FullName is not null) return _targetType.FullName.AsSpan(); // Extract FullName from AQN: "Namespace.TypeName, AssemblyName, ..." @@ -227,12 +227,14 @@ internal bool CanBeAppliedTo(Type targetType) return true; if (!ApplyToDerivedTypes) return false; - do + while (targetType != typeof(Element)) { targetType = targetType.BaseType; + if (targetType is null) + return false; if (TargetTypeFullName.SequenceEqual(targetType.FullName)) return true; - } while (targetType != typeof(Element)); + } return false; } From 306217a916bf9164bfdefc4f94d7eceb1dbb5f09 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 12:11:25 +0100 Subject: [PATCH 05/13] Remove unnecessary changes from rebase conflict resolution - PublicAPI net-ios/net-maccatalyst: remove duplicate ShellFlyoutRenderer entry - Revert whitespace-only changes in 3 SourceGen test snapshot files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt | 1 - .../Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt | 1 - .../InitializeComponent/SetterCompiledConverters.cs | 3 +-- .../InitializeComponent/SimplifyOnPlatform.cs | 5 ++--- src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs | 1 - 5 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index c0fe4ec62e68..2cd675405a0a 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -50,4 +50,3 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void -~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index c0fe4ec62e68..2cd675405a0a 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -50,4 +50,3 @@ virtual Microsoft.Maui.Controls.LongPressingEventArgs.GetPosition(Microsoft.Maui ~static Microsoft.Maui.Controls.VisualStateManager.GetVisualStateGroups(Microsoft.Maui.Controls.VisualElement visualElement) -> Microsoft.Maui.Controls.VisualStateGroupList ~Microsoft.Maui.Controls.Style.LazyInitialization.set -> void ~Microsoft.Maui.Controls.Style.Style(string assemblyQualifiedTargetTypeName) -> void -~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterCompiledConverters.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterCompiledConverters.cs index 87d422e9373e..1d8ec9036106 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterCompiledConverters.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterCompiledConverters.cs @@ -45,7 +45,6 @@ public TestPage() } """; - var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); var expected = $$""" @@ -143,7 +142,7 @@ private partial void InitializeComponent() var (result, generated) = RunGenerator(xaml, code); Assert.False(result.Diagnostics.Any()); Assert.Equal(expected, generated, ignoreLineEndingDifferences: true); - + // Explicitly verify that XamlTypeResolver is not used anywhere in the generated code // This is critical because XamlTypeResolver is not AOT-compatible Assert.DoesNotContain("XamlTypeResolver", generated, StringComparison.Ordinal); diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs index 4aa2adcac436..b0de84267be5 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs @@ -44,10 +44,8 @@ public TestPage() } """; - var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); - var expected = -$$""" + var expected = $$""" //------------------------------------------------------------------------------ // // This code was generated by a .NET MAUI source generator. @@ -133,6 +131,7 @@ private partial void InitializeComponent() var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0-android"); Assert.False(result.Diagnostics.Any()); + Assert.Equal(expected, generated, ignoreLineEndingDifferences: true); } diff --git a/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs b/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs index 8441ad4dbea1..dc3669328cd0 100644 --- a/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs @@ -48,7 +48,6 @@ public TestPage() } """; - var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); var expected = $$""" From 688133d859ffd36c68ee15e21301d5c774837c00 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 12:14:19 +0100 Subject: [PATCH 06/13] Extract ForceInitialize helper for lazy style tests Replace 6 occurrences of ((IStyle)style).Apply(target, new SetterSpecificity()) with style.ForceInitialize(target) extension method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs | 2 +- .../tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs | 4 ++-- .../Xaml.UnitTests/Issues/Unreported009.xaml.cs | 2 +- .../tests/Xaml.UnitTests/LazyStyleTestHelper.cs | 15 +++++++++++++++ .../tests/Xaml.UnitTests/StyleTests.xaml.cs | 4 ++-- 5 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs index e5cd5fd29794..36d93995bc10 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Bz51567.xaml.cs @@ -21,7 +21,7 @@ internal void SetterWithElementValue(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - ((IStyle)style).Apply(new Label(), new SetterSpecificity()); + style.ForceInitialize(new Label()); var setter = style.Setters[1]; Assert.NotNull(setter); diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs index e3ac9f086320..a0d05f0a9639 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui21757.xaml.cs @@ -39,7 +39,7 @@ internal void TypeLiteralAndXTypeCanBeUsedInterchangeably(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - ((IStyle)styleA).Apply(new BoxView(), new SetterSpecificity()); + styleA.ForceInitialize(new BoxView()); Assert.Equal(typeof(BoxView), styleA.TargetType); Assert.Equal(BoxView.ColorProperty, styleA.Setters[0].Property); @@ -50,7 +50,7 @@ internal void TypeLiteralAndXTypeCanBeUsedInterchangeably(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - ((IStyle)styleB).Apply(new BoxView(), new SetterSpecificity()); + styleB.ForceInitialize(new BoxView()); Assert.Equal(typeof(BoxView), styleB.TargetType); Assert.Equal(BoxView.ColorProperty, styleB.Setters[0].Property); diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs index b1c79b638972..1946bfa91b5a 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Unreported009.xaml.cs @@ -18,7 +18,7 @@ internal void AllowSetterValueAsElementProperties(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - ((IStyle)s).Apply(new ContentView(), new SetterSpecificity()); + s.ForceInitialize(new ContentView()); Assert.Equal("Bananas!", (s.Setters[0].Value as Label).Text); } diff --git a/src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs b/src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs new file mode 100644 index 000000000000..eb4c41883730 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs @@ -0,0 +1,15 @@ +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +/// +/// Helper for lazy style tests. Forces initialization of a source-gen lazy style +/// by applying it to a target element. +/// +static class LazyStyleTestHelper +{ + /// + /// Forces a lazy style to initialize by applying it to the given target. + /// Use this when tests need to inspect Setters before the style is naturally applied. + /// + internal static void ForceInitialize(this Style style, BindableObject target) + => ((IStyle)style).Apply(target, new SetterSpecificity()); +} diff --git a/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs b/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs index 7c7cba2bd56c..70b882bf58e4 100644 --- a/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/StyleTests.xaml.cs @@ -36,7 +36,7 @@ internal void TestConversionOnSetters(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - ((IStyle)style).Apply(new Label(), new SetterSpecificity()); + style.ForceInitialize(new Label()); Setter setter; @@ -73,7 +73,7 @@ internal void PropertyDoesNotNeedTypes(XamlInflator inflator) // For SourceGen, styles are lazy - force initialization before inspecting Setters if (inflator == XamlInflator.SourceGen) - ((IStyle)style2).Apply(new Label(), new SetterSpecificity()); + style2.ForceInitialize(new Label()); var s0 = style2.Setters[0]; var s1 = style2.Setters[1]; From 8ba6a63087fb92cfccb9a53817f423093ab4d266 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 14:26:12 +0100 Subject: [PATCH 07/13] Fix compilation errors from rebase + null check in CanBeAppliedTo - SetPropertiesVisitor: remove duplicate variable declarations and duplicate getNodeValue delegate from rebase conflict - SetNamescopesAndRegisterNames: remove duplicate if-statement, keep TryGetValue guard for nodes not in Variables - Style.CanBeAppliedTo: guard against null Type.FullName Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Controls/src/Core/Style.cs | 4 ++-- .../src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs | 1 - src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs | 3 +-- src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt | 2 -- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Controls/src/Core/Style.cs b/src/Controls/src/Core/Style.cs index 55f1437ca4a9..da2ae1f37ab1 100644 --- a/src/Controls/src/Core/Style.cs +++ b/src/Controls/src/Core/Style.cs @@ -223,7 +223,7 @@ void IStyle.UnApply(BindableObject bindable) internal bool CanBeAppliedTo(Type targetType) { // Use FullName comparison to avoid resolving the type (which may have been trimmed) - if (TargetTypeFullName.SequenceEqual(targetType.FullName)) + if (targetType.FullName is not null && TargetTypeFullName.SequenceEqual(targetType.FullName)) return true; if (!ApplyToDerivedTypes) return false; @@ -232,7 +232,7 @@ internal bool CanBeAppliedTo(Type targetType) targetType = targetType.BaseType; if (targetType is null) return false; - if (TargetTypeFullName.SequenceEqual(targetType.FullName)) + if (targetType.FullName is not null && TargetTypeFullName.SequenceEqual(targetType.FullName)) return true; } return false; diff --git a/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs b/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs index c284da0d48dd..350fb17526f2 100644 --- a/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs +++ b/src/Controls/src/SourceGen/Visitors/SetNamescopesAndRegisterNames.cs @@ -83,7 +83,6 @@ public void Visit(ElementNode node, INode parentNode) return; } - if (setNameScope && Context.Variables[node].Type.InheritsFrom(Context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindableObject")!, Context)) // Check if node exists in Variables before accessing if (!Context.Variables.TryGetValue(node, out var nodeVariable)) { diff --git a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs index ecb3562920b1..dd1dcee60fcc 100644 --- a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs +++ b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs @@ -169,7 +169,7 @@ public void Visit(ElementNode node, INode parentNode) { NodeSGExtensions.GetNodeValueDelegate getNodeValue = (n, type) => { - if (!context.Variables.TryGetValue(n, out var val)) + if (!Context.Variables.TryGetValue(n, out var val)) { var nodeName = n is ElementNode en ? en.XmlType.Name : n?.GetType().Name ?? "null"; var nodeKey = n is ElementNode en2 && en2.Properties.TryGetValue(XmlName.xKey, out var kn) && kn is ValueNode vn ? vn.Value?.ToString() : "(none)"; @@ -177,7 +177,6 @@ public void Visit(ElementNode node, INode parentNode) } return val; }; - NodeSGExtensions.GetNodeValueDelegate getNodeValue = (node, type) => Context.Variables[node]; XmlName propertyName = XmlName.Empty; // Store original parentNode for lazy resource check diff --git a/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt index 8c8b74ee66bd..1685a9844c5f 100644 --- a/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Core/maps/src/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,13 +1,11 @@ #nullable enable Microsoft.Maui.Maps.IMap.ClusterClicked(System.Collections.Generic.IReadOnlyList! pins, Microsoft.Maui.Devices.Sensors.Location! location) -> bool -Microsoft.Maui.Maps.IMap.HideInfoWindow(Microsoft.Maui.Maps.IMapPin pin) -> void Microsoft.Maui.Maps.IMap.HideInfoWindow(Microsoft.Maui.Maps.IMapPin! pin) -> void Microsoft.Maui.Maps.IMap.IsClusteringEnabled.get -> bool Microsoft.Maui.Maps.IMap.LastUserLocation.get -> Microsoft.Maui.Devices.Sensors.Location? Microsoft.Maui.Maps.IMap.LongClicked(Microsoft.Maui.Devices.Sensors.Location! position) -> void Microsoft.Maui.Maps.IMap.MapStyle.get -> string? Microsoft.Maui.Maps.IMap.MoveToRegion(Microsoft.Maui.Maps.MapSpan! region, bool animated) -> void -Microsoft.Maui.Maps.IMap.ShowInfoWindow(Microsoft.Maui.Maps.IMapPin pin) -> void Microsoft.Maui.Maps.IMap.ShowInfoWindow(Microsoft.Maui.Maps.IMapPin! pin) -> void Microsoft.Maui.Maps.IMap.UserLocationUpdated(Microsoft.Maui.Devices.Sensors.Location! location) -> void Microsoft.Maui.Maps.IMapElement.Clicked() -> void From 31a13e438b34ee6875ddae0291200437289107b3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 16:57:36 +0100 Subject: [PATCH 08/13] Simplify TargetTypeFullName to string, revert maps PublicAPI - Change TargetTypeFullName from ReadOnlySpan to string, removing #if NETSTANDARD branching and Span-based comparisons - Use plain string equality in CanBeAppliedTo instead of SequenceEqual - Remove redundant .ToString() call in ResourceDictionary.Add - Revert unrelated maps PublicAPI.Unshipped.txt changes from rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Controls/src/Core/ResourceDictionary.cs | 2 +- src/Controls/src/Core/Style.cs | 18 ++++++------------ .../src/PublicAPI/net/PublicAPI.Unshipped.txt | 2 ++ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Controls/src/Core/ResourceDictionary.cs b/src/Controls/src/Core/ResourceDictionary.cs index 4442870b4bea..33f5a610196e 100644 --- a/src/Controls/src/Core/ResourceDictionary.cs +++ b/src/Controls/src/Core/ResourceDictionary.cs @@ -422,7 +422,7 @@ event EventHandler IResourceDictionary.ValuesChanged public void Add(Style style) { if (string.IsNullOrEmpty(style.Class)) - Add(style.TargetTypeFullName.ToString(), style); + Add(style.TargetTypeFullName, style); else { IList - - +xmlns="http://schemas.microsoft.com/dotnet/2021/maui" +xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" +x:Class="Test.TestPage"> + + + + + """; - var code = +var code = """ using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Xaml; @@ -38,113 +38,31 @@ namespace Test; [XamlProcessing(XamlInflator.SourceGen)] public partial class TestPage : ContentPage { - public TestPage() - { - InitializeComponent(); - } +public TestPage() +{ +InitializeComponent(); +} } """; - var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); - var expected = -$$""" -//------------------------------------------------------------------------------ -// -// This code was generated by a .NET MAUI source generator. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable CS0219 // Variable is assigned but its value is never used +var (result, generated) = RunGenerator(xaml, code); +Assert.False(result.Diagnostics.Any()); -namespace Test; +// Verify trimmable style constructor is used (not typeof) +Assert.Contains("new global::Microsoft.Maui.Controls.Style(\"Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls\")", generated, StringComparison.Ordinal); -[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=11.0.0.0, Culture=neutral, PublicKeyToken=null", "11.0.0.0")] -public partial class TestPage -{ - private partial void InitializeComponent() - { - // Fallback to Runtime inflation if the page was updated by HotReload - static string? getPathForType(global::System.Type type) - { - var assembly = type.Assembly; - foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes(assembly)) - { - if (xria.Type == type) - return xria.Path; - } - return null; - } +// Verify setters are generated with compiled property references +Assert.Contains("Label.FontSizeProperty", generated, StringComparison.Ordinal); +Assert.Contains("Label.TextColorProperty", generated, StringComparison.Ordinal); +Assert.Contains("16D", generated, StringComparison.Ordinal); +Assert.Contains("Colors.Red", generated, StringComparison.Ordinal); - var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery - { - AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(), - ResourcePath = getPathForType(typeof(global::Test.TestPage)), - Instance = this, - }); +// Verify lazy initialization pattern for trimmable styles +Assert.Contains("LazyInitialization = (__style, __target) =>", generated, StringComparison.Ordinal); +Assert.Contains("__style.Setters", generated, StringComparison.Ordinal); - if (rlr?.ResourceContent != null) - { - this.InitializeComponentRuntime(); - return; - } - - var resourceDictionary = new global::Microsoft.Maui.Controls.ResourceDictionary(); - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(resourceDictionary!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4); - var __root = this; - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2); -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); -#endif -#line 7 "{{testXamlFilePath}}" - __root.Resources = (global::Microsoft.Maui.Controls.ResourceDictionary)resourceDictionary; -#line default - resourceDictionary.AddFactory("testStyle", () => - { - var style1 = new global::Microsoft.Maui.Controls.Style(typeof(global::Microsoft.Maui.Controls.Label)); - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style1!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 5); -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope1 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope2 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope3 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif - var setter = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.FontSizeProperty, Value = 16D}; - if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter!) == null) - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 9, 6); -#line 9 "{{testXamlFilePath}}" - ((global::System.Collections.Generic.ICollection)style1.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter); -#line default - var setter1 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = global::Microsoft.Maui.Graphics.Colors.Red}; - if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter1!) == null) - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter1!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 10, 6); -#line 10 "{{testXamlFilePath}}" - ((global::System.Collections.Generic.ICollection)style1.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter1); -#line default - return style1; - }, shared: true); -#line 7 "{{testXamlFilePath}}" - __root.Resources = (global::Microsoft.Maui.Controls.ResourceDictionary)resourceDictionary; -#line default - } +// Explicitly verify that XamlTypeResolver is not used anywhere in the generated code +// This is critical because XamlTypeResolver is not AOT-compatible +Assert.DoesNotContain("XamlTypeResolver", generated, StringComparison.Ordinal); } - -"""; - - var (result, generated) = RunGenerator(xaml, code); - Assert.False(result.Diagnostics.Any()); - Assert.Equal(expected, generated, ignoreLineEndingDifferences: true); - - // Explicitly verify that XamlTypeResolver is not used anywhere in the generated code - // This is critical because XamlTypeResolver is not AOT-compatible - Assert.DoesNotContain("XamlTypeResolver", generated, StringComparison.Ordinal); - } } diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs index b0de84267be5..e4c39143a335 100644 --- a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs +++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using Xunit; @@ -42,97 +41,21 @@ public TestPage() InitializeComponent(); } } -"""; - - var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); - var expected = $$""" -//------------------------------------------------------------------------------ -// -// This code was generated by a .NET MAUI source generator. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable CS0219 // Variable is assigned but its value is never used - -namespace Test; - -[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=11.0.0.0, Culture=neutral, PublicKeyToken=null", "11.0.0.0")] -public partial class TestPage -{ - private partial void InitializeComponent() - { - // Fallback to Runtime inflation if the page was updated by HotReload - static string? getPathForType(global::System.Type type) - { - var assembly = type.Assembly; - foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes(assembly)) - { - if (xria.Type == type) - return xria.Path; - } - return null; - } - - var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery - { - AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(), - ResourcePath = getPathForType(typeof(global::Test.TestPage)), - Instance = this, - }); - - if (rlr?.ResourceContent != null) - { - this.InitializeComponentRuntime(); - return; - } - - var __root = this; - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2); -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); -#endif - __root.Resources.AddFactory("style", () => - { - var style1 = new global::Microsoft.Maui.Controls.Style(typeof(global::Microsoft.Maui.Controls.Label)); - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style1!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 10); -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope1 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope2 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope3 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif - var setter = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.Label.TextColorProperty, Value = global::Microsoft.Maui.Graphics.Colors.Pink}; - if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter!) == null) - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 14); -#line 8 "{{testXamlFilePath}}" - ((global::System.Collections.Generic.ICollection)style1.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter); -#line default - var setter1 = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.VisualElement.IsVisibleProperty, Value = (bool)new global::Microsoft.Maui.Controls.VisualElement.VisibilityConverter().ConvertFromInvariantString("True")!}; - if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter1!) == null) - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter1!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 9, 14); -#line 9 "{{testXamlFilePath}}" - ((global::System.Collections.Generic.ICollection)style1.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter1); -#line default - return style1; - }, shared: true); - } -} - """; var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0-android"); Assert.False(result.Diagnostics.Any()); - Assert.Equal(expected, generated, ignoreLineEndingDifferences: true); + // Verify trimmable style constructor is used + Assert.Contains("new global::Microsoft.Maui.Controls.Style(\"Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls\")", generated, StringComparison.Ordinal); + + // Verify setters with platform-simplified values + Assert.Contains("Label.TextColorProperty", generated, StringComparison.Ordinal); + Assert.Contains("Colors.Pink", generated, StringComparison.Ordinal); + Assert.Contains("VisualElement.IsVisibleProperty", generated, StringComparison.Ordinal); + + // Verify lazy initialization pattern + Assert.Contains("LazyInitialization = (__style, __target) =>", generated, StringComparison.Ordinal); } [Fact] diff --git a/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs b/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs index dc3669328cd0..634bce0bbe33 100644 --- a/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using Xunit; @@ -48,93 +47,19 @@ public TestPage() } """; - var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml"); - var expected = -$$""" -//------------------------------------------------------------------------------ -// -// This code was generated by a .NET MAUI source generator. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -#nullable enable -#pragma warning disable CS0219 // Variable is assigned but its value is never used - -namespace Test; - -[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=11.0.0.0, Culture=neutral, PublicKeyToken=null", "11.0.0.0")] -public partial class TestPage -{ - private partial void InitializeComponent() - { - // Fallback to Runtime inflation if the page was updated by HotReload - static string? getPathForType(global::System.Type type) - { - var assembly = type.Assembly; - foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes(assembly)) - { - if (xria.Type == type) - return xria.Path; - } - return null; - } - - var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery - { - AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(), - ResourcePath = getPathForType(typeof(global::Test.TestPage)), - Instance = this, - }); - - if (rlr?.ResourceContent != null) - { - this.InitializeComponentRuntime(); - return; - } + var (result, generated) = RunGenerator(xaml, code); + Assert.False(result.Diagnostics.Any()); - var resourceDictionary = new global::Microsoft.Maui.Controls.ResourceDictionary(); - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(resourceDictionary!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4); - var __root = this; - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2); -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); -#endif -#line 7 "{{testXamlFilePath}}" - __root.Resources = (global::Microsoft.Maui.Controls.ResourceDictionary)resourceDictionary; -#line default - resourceDictionary.AddFactory("NetworkIndicator", () => - { - var style1 = new global::Microsoft.Maui.Controls.Style(typeof(global::Microsoft.Maui.Controls.Image)); - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style1!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 5); -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope1 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif -#if !_MAUIXAML_SG_NAMESCOPE_DISABLE - global::Microsoft.Maui.Controls.Internals.INameScope iNameScope2 = new global::Microsoft.Maui.Controls.Internals.NameScope(); -#endif - var setter = new global::Microsoft.Maui.Controls.Setter {Property = global::Microsoft.Maui.Controls.AbsoluteLayout.LayoutBoundsProperty, Value = (global::Microsoft.Maui.Graphics.Rect)new global::Microsoft.Maui.Controls.BoundsTypeConverter().ConvertFromInvariantString("10,10,20,20")!}; - if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(setter!) == null) - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(setter!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 9, 6); -#line 9 "{{testXamlFilePath}}" - ((global::System.Collections.Generic.ICollection)style1.Setters).Add((global::Microsoft.Maui.Controls.Setter)setter); -#line default - return style1; - }, shared: true); -#line 7 "{{testXamlFilePath}}" - __root.Resources = (global::Microsoft.Maui.Controls.ResourceDictionary)resourceDictionary; -#line default - } -} + // Verify trimmable style constructor is used for Image TargetType + Assert.Contains("new global::Microsoft.Maui.Controls.Style(\"Microsoft.Maui.Controls.Image, Microsoft.Maui.Controls\")", generated, StringComparison.Ordinal); -"""; + // Verify setter with attached property LayoutBounds and content value syntax + Assert.Contains("AbsoluteLayout.LayoutBoundsProperty", generated, StringComparison.Ordinal); + Assert.Contains("10,10,20,20", generated, StringComparison.Ordinal); + Assert.Contains("BoundsTypeConverter", generated, StringComparison.Ordinal); - var (result, generated) = RunGenerator(xaml, code); - Assert.False(result.Diagnostics.Any()); - Assert.Equal(expected, generated, ignoreLineEndingDifferences: true); + // Verify lazy initialization pattern + Assert.Contains("LazyInitialization = (__style, __target) =>", generated, StringComparison.Ordinal); + Assert.Contains("__style.Setters", generated, StringComparison.Ordinal); } } diff --git a/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs b/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs index 33d52ce57c66..0ca3af2174bd 100644 --- a/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/StyleSourceGenTests.cs @@ -184,8 +184,6 @@ private partial void InitializeComponent() this.InitializeComponentRuntime(); return; } - var style = new global::Microsoft.Maui.Controls.Style("Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls"); - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4); var __root = this; global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2); #if !_MAUIXAML_SG_NAMESCOPE_DISABLE @@ -194,6 +192,8 @@ private partial void InitializeComponent() #if !_MAUIXAML_SG_NAMESCOPE_DISABLE global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); #endif + var style = new global::Microsoft.Maui.Controls.Style("Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls"); + global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4); __root.Resources["EmptyStyle"] = style; } } @@ -316,8 +316,6 @@ private partial void InitializeComponent() this.InitializeComponentRuntime(); return; } - var style = new global::Microsoft.Maui.Controls.Style("Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls"); - global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4); var __root = this; global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2); #if !_MAUIXAML_SG_NAMESCOPE_DISABLE @@ -326,6 +324,8 @@ private partial void InitializeComponent() #if !_MAUIXAML_SG_NAMESCOPE_DISABLE global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope); #endif + var style = new global::Microsoft.Maui.Controls.Style("Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls"); + global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(style!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4); style.LazyInitialization = (__style, __target) => { if (__target is not global::Microsoft.Maui.Controls.Label) return; diff --git a/src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs b/src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs index eb4c41883730..ac8faca20e64 100644 --- a/src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs +++ b/src/Controls/tests/Xaml.UnitTests/LazyStyleTestHelper.cs @@ -2,14 +2,22 @@ namespace Microsoft.Maui.Controls.Xaml.UnitTests; /// /// Helper for lazy style tests. Forces initialization of a source-gen lazy style -/// by applying it to a target element. +/// by running its LazyInitialization callback without applying the style. /// static class LazyStyleTestHelper { /// - /// Forces a lazy style to initialize by applying it to the given target. + /// Forces a lazy style to initialize by invoking its LazyInitialization callback. /// Use this when tests need to inspect Setters before the style is naturally applied. + /// Unlike IStyle.Apply, this does not apply setters to the target — it only populates + /// the style's Setters collection. /// internal static void ForceInitialize(this Style style, BindableObject target) - => ((IStyle)style).Apply(target, new SetterSpecificity()); + { + if (style.LazyInitialization is not null) + { + style.LazyInitialization(style, target); + style.LazyInitialization = null; + } + } } From ad4669a101bb918e54fe98a7ab8d792d9773e1be Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 1 Apr 2026 10:44:48 +0200 Subject: [PATCH 13/13] Fix LazyInitialization getter: private -> internal for test access Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Controls/src/Core/Style.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Style.cs b/src/Controls/src/Core/Style.cs index 902ba38a0c98..6888ed2bf435 100644 --- a/src/Controls/src/Core/Style.cs +++ b/src/Controls/src/Core/Style.cs @@ -64,7 +64,7 @@ public Style(string assemblyQualifiedTargetTypeName) /// This property is intended for source generator use only. /// [EditorBrowsable(EditorBrowsableState.Never)] - public Action LazyInitialization { private get; set; } + public Action LazyInitialization { internal get; set; } /// /// Gets or sets whether the style can be applied to types derived from .