diff --git a/src/Controls/src/Build.Tasks/BuildException.cs b/src/Controls/src/Build.Tasks/BuildException.cs
index ecdf63e2d033..dd6429bc8db9 100644
--- a/src/Controls/src/Build.Tasks/BuildException.cs
+++ b/src/Controls/src/Build.Tasks/BuildException.cs
@@ -81,6 +81,7 @@ class BuildExceptionCode
public static BuildExceptionCode NamescopeDuplicate = new BuildExceptionCode("XC", 0064, nameof(NamescopeDuplicate), "");
public static BuildExceptionCode ContentPropertyAttributeMissing = new BuildExceptionCode("XC", 0065, nameof(ContentPropertyAttributeMissing), "");
public static BuildExceptionCode InvalidXaml = new BuildExceptionCode("XC", 0066, nameof(InvalidXaml), "");
+ public static BuildExceptionCode DuplicatePropertyAssignment = new BuildExceptionCode("XC", 0067, nameof(DuplicatePropertyAssignment), ""); //warning
//Extensions
diff --git a/src/Controls/src/Build.Tasks/ErrorMessages.resx b/src/Controls/src/Build.Tasks/ErrorMessages.resx
index 3eeabf1a9911..cf57f0d50975 100644
--- a/src/Controls/src/Build.Tasks/ErrorMessages.resx
+++ b/src/Controls/src/Build.Tasks/ErrorMessages.resx
@@ -270,8 +270,12 @@
A key is required in {StaticResource}.
-
+
x:Shared is only supported with SourceGen inflator. Remove the attribute or switch to SourceGen.
+
+ Property '{0}' is being set multiple times. Only the last value will be used.
+ 0 is property name (e.g. "Border.Content" or "Label.Text")
+
\ No newline at end of file
diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs
index 7ff49a6a9b92..14954818e9c4 100644
--- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs
+++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs
@@ -43,6 +43,31 @@ public bool IsResourceDictionary(ElementNode node)
ModuleDefinition Module { get; } = context.Body.Method.Module;
+ // Track properties that have been set to detect duplicates
+ readonly Dictionary> setProperties = new Dictionary>();
+
+ void CheckForDuplicateProperty(ElementNode parentNode, XmlName propertyName, IXmlLineInfo lineInfo)
+ {
+ if (!setProperties.TryGetValue(parentNode, out var props))
+ {
+ props = new HashSet();
+ setProperties[parentNode] = props;
+ }
+
+ if (!props.Add(propertyName))
+ {
+ // Property is being set multiple times
+ Context.LoggingHelper.LogWarningOrError(
+ DuplicatePropertyAssignment,
+ Context.XamlFilePath,
+ lineInfo.LineNumber,
+ lineInfo.LinePosition,
+ 0,
+ 0,
+ $"{parentNode.XmlType.Name}.{propertyName.LocalName}");
+ }
+ }
+
public void Visit(ValueNode node, INode parentNode)
{
//TODO support Label text as element
@@ -69,6 +94,12 @@ public void Visit(ValueNode node, INode parentNode)
return;
if (propertyName.Equals(XmlName.mcIgnorable))
return;
+
+ // TODO: Add duplicate implicit content property checking here to match SourceGen behavior
+ // (SourceGen Visit(ValueNode) emits MAUIX2015 when a ValueNode assigns the same implicit
+ // content property that was already assigned by an ElementNode or another ValueNode).
+ // Without this check, text produces MAUIX2015 under SourceGen
+ // but no warning under XamlC.
Context.IL.Append(SetPropertyValue(Context.Variables[(ElementNode)parentNode], propertyName, node, Context, node));
}
@@ -140,8 +171,32 @@ public void Visit(ElementNode node, INode parentNode)
var name = new XmlName(node.NamespaceURI, contentProperty);
if (skips.Contains(name))
return;
- if (parentNode is ElementNode node2 && node2.SkipProperties.Contains(propertyName))
+ if (parentNode is ElementNode node2 && node2.SkipProperties.Contains(name))
return;
+
+ // Resolve the content property type to determine if it is a collection.
+ // Use CanGet (CLR property resolution) rather than GetBindablePropertyReference+GetValue/Get
+ // to avoid side effects: GetBindablePropertyReference can emit ObsoleteProperty diagnostics
+ // and GetValue/Get generate IL instructions; both would be duplicated by SetPropertyValue.
+ // CanGet covers all content properties because every BP-backed property has a CLR wrapper.
+ var propLocalName = name.LocalName;
+ bool canResolveProperty = CanGet(parentVar, propLocalName, Context, out TypeReference contentPropType);
+
+ // Skip duplicate check when the property cannot be resolved, is System.Object (unresolved
+ // generics would produce false positives), or is a collection type. Matches SourceGen behavior.
+ if (canResolveProperty && contentPropType != null)
+ {
+ bool isObject = contentPropType.FullName == "System.Object";
+ bool isCollection = !isObject
+ && contentPropType.ImplementsInterface(Context.Cache, Module.ImportReference(Context.Cache, ("mscorlib", "System.Collections", "IEnumerable")))
+ && contentPropType.GetMethods(Context.Cache, md => md.Name == "Add" && md.Parameters.Count == 1, Module).Any();
+
+ // Use parent namespace for duplicate tracking to match how explicit property assignments are keyed
+ var contentPropertyName = new XmlName(((ElementNode)parentNode).NamespaceURI, contentProperty);
+ if (!isCollection && !isObject)
+ CheckForDuplicateProperty((ElementNode)parentNode, contentPropertyName, node);
+ }
+
Context.IL.Append(SetPropertyValue(Context.Variables[(ElementNode)parentNode], name, node, Context, node));
}
// Collection element, implicit content, or implicit collection element.
@@ -539,7 +594,7 @@ static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableD
// continue compilation
}
- if ( dataTypeNode is ElementNode enode
+ if (dataTypeNode is ElementNode enode
&& enode.XmlType.NamespaceUri == XamlParser.X2009Uri
&& enode.XmlType.Name == nameof(Xaml.NullExtension))
{
diff --git a/src/Controls/src/SourceGen/AnalyzerReleases.Unshipped.md b/src/Controls/src/SourceGen/AnalyzerReleases.Unshipped.md
index 7618199f24cc..561c857d919a 100644
--- a/src/Controls/src/SourceGen/AnalyzerReleases.Unshipped.md
+++ b/src/Controls/src/SourceGen/AnalyzerReleases.Unshipped.md
@@ -30,3 +30,4 @@ MAUIX2010 | XamlParsing | Info | ExpressionNotSettable
MAUIX2011 | XamlParsing | Warning | AmbiguousMemberWithStaticType
MAUIX2012 | XamlParsing | Error | CSharpExpressionsRequirePreviewFeatures
MAUIX2013 | XamlParsing | Error | AsyncLambdaNotSupported
+MAUIX2015 | XamlInflation | Warning | DuplicatePropertyAssignment
diff --git a/src/Controls/src/SourceGen/BindingContextDataType.cs b/src/Controls/src/SourceGen/BindingContextDataType.cs
new file mode 100644
index 000000000000..43047e1ad408
--- /dev/null
+++ b/src/Controls/src/SourceGen/BindingContextDataType.cs
@@ -0,0 +1,36 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Microsoft.Maui.Controls.SourceGen;
+
+///
+/// Distinguishes between different states of x:DataType resolution on a node.
+///
+enum BindingContextDataTypeKind
+{
+ /// x:DataType resolved to a known type.
+ Resolved,
+
+ /// x:DataType="{x:Null}" — explicit opt-out of compiled bindings for this scope.
+ ExplicitNull,
+
+ /// x:DataType is present but couldn't be resolved (diagnostic already reported).
+ Unresolved,
+}
+
+///
+/// Represents the resolved x:DataType for a node in the XAML tree.
+/// Pre-computed by and stored in
+/// .
+///
+/// The resolution state.
+/// The resolved type symbol. Non-null only when is .
+/// The node where x:DataType was declared (for diagnostics). Null when inherited from a parent context.
+/// True if the data type was inherited from an ancestor, false if declared on this node.
+/// True if the data type was inherited across a DataTemplate boundary (likely a bug).
+record BindingContextDataType(
+ BindingContextDataTypeKind Kind,
+ ITypeSymbol? Symbol,
+ INode? Origin,
+ bool IsInherited,
+ bool CrossedTemplateBoundary);
diff --git a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs
index 6e80cb3d237a..abb896df9caf 100644
--- a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs
+++ b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs
@@ -29,15 +29,17 @@ public CompiledBindingMarkup(ElementNode node, string path, ILocalValue bindingE
private Location GetLocation(INode node, string? context = null)
=> LocationHelpers.LocationCreate(_context.ProjectItem.RelativePath!, (IXmlLineInfo)node, context ?? "Binding");
- public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, out string? newBindingExpression)
+ public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, out string? newBindingExpression, out Diagnostic? propertyNotFoundDiagnostic)
{
newBindingExpression = null;
+ propertyNotFoundDiagnostic = null;
if (!TryParsePath(
sourceType,
out ITypeSymbol? propertyType,
out SetterOptions? setterOptions,
- out EquatableArray? parsedPath)
+ out EquatableArray? parsedPath,
+ out propertyNotFoundDiagnostic)
|| propertyType is null
|| setterOptions is null
|| !parsedPath.HasValue)
@@ -261,11 +263,13 @@ bool TryParsePath(
ITypeSymbol sourceType,
out ITypeSymbol? propertyType,
out SetterOptions? setterOptions,
- out EquatableArray? bindingPath)
+ out EquatableArray? bindingPath,
+ out Diagnostic? propertyNotFoundDiagnostic)
{
propertyType = sourceType;
setterOptions = null;
bindingPath = default;
+ propertyNotFoundDiagnostic = null;
var isNullable = false;
var path = _path.Trim('.', ' ');
@@ -310,10 +314,9 @@ bool TryParsePath(
if (!previousPartType.TryGetProperty(p, _context, out var property, out var currentPropertyType, out var isAssumedWritable)
|| currentPropertyType is null)
{
- // Report as WARNING (not Error) because the property might be generated by another source generator
- // that runs after the MAUI XAML source generator. The binding will fall back to reflection at runtime.
- // This allows scenarios like CommunityToolkit.Mvvm's [ObservableProperty] to work correctly.
- _context.ReportDiagnostic(Diagnostic.Create(Descriptors.BindingPropertyNotFound, GetLocation(_node), p, previousPartType.ToFQDisplayString()));
+ // Don't emit directly — let the caller decide whether to report this.
+ // For x:Reference bindings, the caller silently falls back to runtime.
+ propertyNotFoundDiagnostic = Diagnostic.Create(Descriptors.BindingPropertyNotFound, GetLocation(_node), p, previousPartType.ToFQDisplayString());
return false;
}
diff --git a/src/Controls/src/SourceGen/Descriptors.cs b/src/Controls/src/SourceGen/Descriptors.cs
index fed3bdb7ba1d..a338f9c64d2d 100644
--- a/src/Controls/src/SourceGen/Descriptors.cs
+++ b/src/Controls/src/SourceGen/Descriptors.cs
@@ -325,6 +325,14 @@ public static class Descriptors
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
+ public static DiagnosticDescriptor DuplicatePropertyAssignment = new DiagnosticDescriptor(
+ id: "MAUIX2015",
+ title: new LocalizableResourceString(nameof(MauiGResources.DuplicatePropertyAssignmentTitle), MauiGResources.ResourceManager, typeof(MauiGResources)),
+ messageFormat: new LocalizableResourceString(nameof(MauiGResources.DuplicatePropertyAssignmentMessage), MauiGResources.ResourceManager, typeof(MauiGResources)),
+ category: "XamlInflation",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
// public static BuildExceptionCode TypeResolution = new BuildExceptionCode("XC", 0000, nameof(TypeResolution), "");
// public static BuildExceptionCode PropertyResolution = new BuildExceptionCode("XC", 0001, nameof(PropertyResolution), "");
// public static BuildExceptionCode MissingEventHandler = new BuildExceptionCode("XC", 0002, nameof(MissingEventHandler), "");
diff --git a/src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs b/src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs
index db94769a8e97..3df90e8f0da5 100644
--- a/src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs
+++ b/src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs
@@ -130,7 +130,8 @@ PrePost newblock() =>
return;
}
-""");
+"""
+);
}
Visit(root, sgcontext);
@@ -176,6 +177,7 @@ static void Visit(RootNode rootnode, SourceGenContext visitorContext, bool useDe
rootnode.Accept(new CreateValuesVisitor(visitorContext), null);
rootnode.Accept(new SetNamescopesAndRegisterNamesVisitor(visitorContext), null); //set namescopes for {x:Reference} and FindByName
rootnode.Accept(new SetFieldsForXNamesVisitor(visitorContext), null);
+ rootnode.Accept(new PropagateDataTypeVisitor(visitorContext), null);
// rootnode.Accept(new FillResourceDictionariesVisitor(visitorContext), null);
rootnode.Accept(new SetResourcesVisitor(visitorContext), null);
rootnode.Accept(new SetPropertiesVisitor(visitorContext, true), null);
diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs
index 9c0602411cff..ac5d242ce854 100644
--- a/src/Controls/src/SourceGen/KnownMarkups.cs
+++ b/src/Controls/src/SourceGen/KnownMarkups.cs
@@ -343,25 +343,45 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde
returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!;
ITypeSymbol? dataTypeSymbol = null;
- // When Source is explicitly set (RelativeSource or x:Reference), x:DataType does not describe
- // the actual source — skip compilation and fall back to runtime binding.
- bool hasExplicitSource = HasExplicitBindingSource(markupNode);
+ // When Source is RelativeSource, the type is determined at runtime — skip compilation.
+ // When Source is x:Reference, resolve the referenced element's type and compile against it.
+ // Otherwise, use x:DataType from the current scope.
+ bool hasRelativeSource = HasRelativeSourceBinding(markupNode);
context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable);
- if ( !hasExplicitSource
+ if ( !hasRelativeSource
&& extVariable is not null)
{
- TryGetXDataType(markupNode, context, out dataTypeSymbol);
+ ITypeSymbol? xRefSourceType = TryResolveXReferenceSourceType(markupNode, context);
+ dataTypeSymbol = xRefSourceType;
+ if (dataTypeSymbol is null && TryGetBindingContextDataType(markupNode, context, out var bcDataType))
+ {
+ dataTypeSymbol = bcDataType.Symbol;
+
+ if (bcDataType.CrossedTemplateBoundary)
+ {
+ var location = LocationHelpers.LocationCreate(context.ProjectItem.RelativePath!, (IXmlLineInfo)markupNode, "x:DataType");
+ context.ReportDiagnostic(Diagnostic.Create(Descriptors.BindingWithXDataTypeFromOuterScope, location));
+ }
+ }
if (dataTypeSymbol is not null)
{
var compiledBindingMarkup = new CompiledBindingMarkup(markupNode, GetBindingPath(markupNode), extVariable, context);
- if (compiledBindingMarkup.TryCompileBinding(dataTypeSymbol, isTemplateBinding, out string? newBindingExpression) && newBindingExpression is not null)
+ if (compiledBindingMarkup.TryCompileBinding(dataTypeSymbol, isTemplateBinding, out string? newBindingExpression, out Diagnostic? propertyNotFoundDiagnostic) && newBindingExpression is not null)
{
value = newBindingExpression;
return true;
}
+
+ // Emit property-not-found diagnostic only for x:DataType-sourced bindings.
+ // For x:Reference bindings, silently fall back to runtime — these bindings
+ // were never compiled before, so emitting a new warning would be a regression.
+ if (propertyNotFoundDiagnostic is not null && xRefSourceType is null)
+ {
+ context.ReportDiagnostic(propertyNotFoundDiagnostic);
+ }
}
}
@@ -468,161 +488,66 @@ static string GetBindingPath(ElementNode node)
}
}
- static bool TryGetXDataType(ElementNode node, SourceGenContext context, out ITypeSymbol? dataTypeSymbol)
+ static ElementNode? GetParent(ElementNode node)
{
- dataTypeSymbol = null;
-
- if (!TryFindXDataTypeNode(node, context, out INode? dataTypeNode, out bool xDataTypeIsInOuterScope)
- || dataTypeNode is null)
+ return node switch
{
- return false;
- }
-
- var location = LocationHelpers.LocationCreate(context.ProjectItem.RelativePath!, (IXmlLineInfo)node, "x:DataType");
+ { Parent: ListNode { Parent: ElementNode parentNode } } => parentNode,
+ { Parent: ElementNode parentNode } => parentNode,
+ _ => null,
+ };
+ }
- if (xDataTypeIsInOuterScope)
- {
- context.ReportDiagnostic(Diagnostic.Create(Descriptors.BindingWithXDataTypeFromOuterScope, location));
- // continue compilation - this is a warning
- }
+ ///
+ /// Looks up the pre-computed BindingContextDataType for a binding markup node.
+ /// Handles the BindingContext={Binding} skip: when the binding target is the
+ /// BindingContext property itself, the node's own x:DataType describes the RESULT,
+ /// not the source — so we look up the grandparent's type instead.
+ ///
+ static bool TryGetBindingContextDataType(ElementNode markupNode, SourceGenContext context, out BindingContextDataType result)
+ {
+ result = default!;
- if (dataTypeNode.RepresentsType(XamlParser.X2009Uri, "NullExtension"))
+ if (IsBindingContextBinding(markupNode))
{
- // TODO report warning
- // context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, "Binding with x:DataType NullExtension"));
- // context.LoggingHelper.LogWarningOrError(BuildExceptionCode.BindingWithNullDataType, context.XamlFilePath, node.LineNumber, node.LinePosition, 0, 0, null);
- return false;
- }
+ // The binding is BindingContext="{Binding ...}". The parent element's x:DataType
+ // describes what BindingContext will become after the binding resolves — not the
+ // source to resolve against. We need the grandparent's data type.
+ var parent = GetParent(markupNode);
+ if (parent is null)
+ return false;
- // TypeExtension would already provide the type value, so we can just grab it from the context
- if (dataTypeNode.RepresentsType(XamlParser.X2009Uri, "TypeExtension"))
- {
- // it is possible that the dataTypeNode belongs to the parent context
- // this is the case for example in this scenario:
- //
- //
- //
- //
- //
- SourceGenContext? ctx = context;
- while (ctx is not null)
+ var grandparent = GetParent(parent);
+ if (grandparent is not null
+ && TryGetBindingContextDataTypeFromContext(grandparent, context, out result))
{
- if (ctx.Types.TryGetValue(dataTypeNode, out dataTypeSymbol))
- {
- return true;
- }
-
- ctx = ctx.ParentContext;
+ return true;
}
- context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, "Binding with x:DataType TypeExtension which cannot be resolved"));
- return false;
- }
-
- string? dataTypeName = (dataTypeNode as ValueNode)?.Value as string;
- if (dataTypeName is null)
- {
- // TODO
- context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, $"dataTypeNode is not a value node, got {dataTypeNode} instead"));
- // throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null);
- // throw new Exception($"dataTypeNode {dataTypeNode} is not a value node"); // TODO
- return false;
- }
-
- XmlType? dataType = null;
- try
- {
- dataType = TypeArgumentsParser.ParseSingle(dataTypeName, node.NamespaceResolver, dataTypeNode as IXmlLineInfo);
- }
- catch (XamlParseException)
- {
- var prefix = dataTypeName.Contains(":") ? dataTypeName.Substring(0, dataTypeName.IndexOf(":", StringComparison.Ordinal)) : "";
- // throw new BuildException(XmlnsUndeclared, dataTypeNode as IXmlLineInfo, null, prefix);
- throw new Exception($"XmlnsUndeclared {prefix}"); // TODO
- }
-
- if (dataType is null)
- {
- // TODO
- // throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null);
- // throw new Exception($"cannot parse {dataTypeName}"); // TODO
- context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, "Cannot parse x:DataType value"));
- return false;
- }
-
- if (!dataType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out INamedTypeSymbol? symbol) && symbol is not null)
- {
- // TODO report the right diagnostic
- context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, location, "Cannot resolve x:DataType type"));
return false;
}
- dataTypeSymbol = symbol;
- return true;
+ return TryGetBindingContextDataTypeFromContext(markupNode, context, out result);
}
- static bool TryFindXDataTypeNode(ElementNode elementNode, SourceGenContext context, out INode? dataTypeNode, out bool isInOuterScope)
+ static bool TryGetBindingContextDataTypeFromContext(ElementNode node, SourceGenContext context, out BindingContextDataType result)
{
- isInOuterScope = false;
- dataTypeNode = null;
-
- // Special handling for BindingContext={Binding ...}
- // The order of checks is:
- // - x:DataType on the binding itself
- // - SKIP looking for x:DataType on the parent
- // - continue looking for x:DataType on the parent's parent...
- ElementNode? skipNode = null;
- if (IsBindingContextBinding(elementNode))
- {
- skipNode = GetParent(elementNode);
- }
-
- ElementNode? node = elementNode;
- while (node is not null)
+ // Walk up the context chain (handles template child contexts)
+ SourceGenContext? ctx = context;
+ while (ctx is not null)
{
- if (node != skipNode && node.Properties.TryGetValue(XmlName.xDataType, out dataTypeNode))
+ if (ctx.BindingContextDataTypes.TryGetValue(node, out result)
+ && result.Kind == BindingContextDataTypeKind.Resolved)
{
return true;
}
-
- if (DoesNotInheritDataType(node, context))
- {
- return false;
- }
-
- // When the binding is inside of a DataTemplate and the x:DataType is in the parent scope,
- // it is usually a bug.
- if (node.RepresentsType(XamlParser.MauiUri, "DataTemplate"))
- {
- isInOuterScope = true;
- }
-
- node = GetParent(node);
+ ctx = ctx.ParentContext;
}
+ result = default!;
return false;
}
- static bool DoesNotInheritDataType(ElementNode node, SourceGenContext context)
- {
- return GetParent(node) is ElementNode parentNode
- && parentNode.XmlType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out INamedTypeSymbol? parentTypeSymbol)
- && parentTypeSymbol is not null
- && node.TryGetPropertyName(parentNode, out XmlName propertyName)
- && parentTypeSymbol.GetAllProperties(propertyName.LocalName, context).FirstOrDefault() is IPropertySymbol propertySymbol
- && propertySymbol.GetAttributes().Any(a => a.AttributeClass?.ToFQDisplayString() == "global::Microsoft.Maui.Controls.Xaml.DoesNotInheritDataTypeAttribute");
- }
-
- static ElementNode? GetParent(ElementNode node)
- {
- return node switch
- {
- { Parent: ListNode { Parent: ElementNode parentNode } } => parentNode,
- { Parent: ElementNode parentNode } => parentNode,
- _ => null,
- };
- }
-
static bool IsBindingContextBinding(ElementNode node)
{
// looking for BindingContext="{Binding ...}"
@@ -632,29 +557,71 @@ static bool IsBindingContextBinding(ElementNode node)
&& propertyName.LocalName == "BindingContext";
}
- // Checks if the binding has a Source property set to RelativeSource or x:Reference.
- // When Source is explicitly set, x:DataType does not describe the actual binding source,
+ // Checks if the binding has a Source property set to RelativeSource.
+ // When a binding uses RelativeSource, the source type is determined at runtime,
// so we should NOT compile the binding using x:DataType.
- static bool HasExplicitBindingSource(ElementNode bindingNode)
+ static bool HasRelativeSourceBinding(ElementNode bindingNode)
{
- // Check if Source property exists
if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode? sourceNode)
&& !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode))
{
return false;
}
- // Check if the Source is a RelativeSourceExtension or ReferenceExtension
if (sourceNode is ElementNode sourceElementNode)
{
return sourceElementNode.XmlType.Name is "RelativeSourceExtension"
- or "RelativeSource"
- or "ReferenceExtension"
- or "Reference";
+ or "RelativeSource";
}
return false;
}
+
+ // When Source={x:Reference Name} is set on a binding, resolves the referenced element's type
+ // by walking namescopes (same logic as ProvideValueForReferenceExtension).
+ // Returns null if Source is not an x:Reference or the name cannot be resolved.
+ static ITypeSymbol? TryResolveXReferenceSourceType(ElementNode bindingNode, SourceGenContext context)
+ {
+ if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode? sourceNode)
+ && !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode))
+ return null;
+
+ if (sourceNode is not ElementNode refNode)
+ return null;
+
+ if (refNode.XmlType.Name is not "ReferenceExtension" and not "Reference")
+ return null;
+
+ // Extract the Name from the x:Reference markup
+ if (!refNode.Properties.TryGetValue(new XmlName("", "Name"), out INode? refNameNode)
+ && !refNode.Properties.TryGetValue(new XmlName(null, "Name"), out refNameNode))
+ {
+ refNameNode = refNode.CollectionItems.Count > 0 ? refNode.CollectionItems[0] : null;
+ }
+
+ if (refNameNode is not ValueNode vn || vn.Value is not string name)
+ return null;
+
+ // Walk namescopes to find the referenced element's type
+ ElementNode? node = bindingNode;
+ var currentContext = context;
+ while (currentContext is not null && node is not null)
+ {
+ while (currentContext is not null && !currentContext.Scopes.ContainsKey(node))
+ currentContext = currentContext.ParentContext;
+ if (currentContext is null)
+ break;
+ var namescope = currentContext.Scopes[node];
+ if (namescope.namesInScope != null && namescope.namesInScope.ContainsKey(name))
+ return namescope.namesInScope[name].Type;
+ INode n = node;
+ while (n.Parent is ListNode ln)
+ n = ln.Parent;
+ node = n.Parent as ElementNode;
+ }
+
+ return null;
+ }
}
internal static bool ProvideValueForDataTemplateExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value)
diff --git a/src/Controls/src/SourceGen/MauiGResources.resx b/src/Controls/src/SourceGen/MauiGResources.resx
index d1bb6e523e47..da1831797582 100644
--- a/src/Controls/src/SourceGen/MauiGResources.resx
+++ b/src/Controls/src/SourceGen/MauiGResources.resx
@@ -240,4 +240,11 @@
Event handler '{0}' with correct signature not found in type '{1}'.
0 is handler name, 1 is type name
+
+ Property set multiple times
+
+
+ Property '{0}' is being set multiple times. Only the last value will be used.
+ 0 is a property name (e.g., "Border.Content", "Label.Text")
+
diff --git a/src/Controls/src/SourceGen/SetPropertyHelpers.cs b/src/Controls/src/SourceGen/SetPropertyHelpers.cs
index e7e370a0deff..de739a376d87 100644
--- a/src/Controls/src/SourceGen/SetPropertyHelpers.cs
+++ b/src/Controls/src/SourceGen/SetPropertyHelpers.cs
@@ -233,6 +233,16 @@ public static void AddLazyResourceToResourceDictionary(IndentedTextWriter writer
ParentContext = context
};
+ // Seed the lambda context with the parent's BindingContextDataType.
+ // The CrossedTemplateBoundary flag from the main context already carries the
+ // correct value. When the node declares its own x:DataType, the visitor will
+ // override this seed.
+ if (context.BindingContextDataTypes.TryGetValue(node, out var nodeDataType)
+ && nodeDataType.Kind == BindingContextDataTypeKind.Resolved)
+ {
+ lambdaContext.BindingContextDataTypes[node] = nodeDataType;
+ }
+
// First pass: Create all values (node and its descendants) using CreateValuesVisitor
// This mirrors the normal flow: CreateValuesVisitor walks the entire tree first
node.Accept(new CreateValuesVisitor(lambdaContext), null);
@@ -240,6 +250,9 @@ public static void AddLazyResourceToResourceDictionary(IndentedTextWriter writer
// Second pass: Set namescopes and register names in the namescope
node.Accept(new SetNamescopesAndRegisterNamesVisitor(lambdaContext), null);
+ // Propagate x:DataType through the tree
+ node.Accept(new PropagateDataTypeVisitor(lambdaContext), null);
+
// Third pass: Set resources in ResourceDictionary
node.Accept(new SetResourcesVisitor(lambdaContext), null);
@@ -534,7 +547,7 @@ static void SetValue(IndentedTextWriter writer, ILocalValue parentVar, IFieldSym
}
}
- static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext context, out ITypeSymbol? propertyType, out IPropertySymbol? propertySymbol)
+ internal static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext context, out ITypeSymbol? propertyType, out IPropertySymbol? propertySymbol)
{
propertyType = null;
if ((propertySymbol = parentVar.Type.GetAllProperties(localName, context).FirstOrDefault()) == null)
@@ -546,7 +559,7 @@ static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext con
return true;
}
- static bool CanGetValue(ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, bool attached, SourceGenContext context, out ITypeSymbol? propertyType)
+ internal static bool CanGetValue(ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, bool attached, SourceGenContext context, out ITypeSymbol? propertyType)
{
propertyType = null;
if (bpFieldSymbol == null)
diff --git a/src/Controls/src/SourceGen/SourceGenContext.cs b/src/Controls/src/SourceGen/SourceGenContext.cs
index 58ed48d8112d..69cf0b13e65a 100644
--- a/src/Controls/src/SourceGen/SourceGenContext.cs
+++ b/src/Controls/src/SourceGen/SourceGenContext.cs
@@ -38,10 +38,14 @@ public void ReportDiagnostic(Diagnostic diagnostic)
var noWarn = ProjectItem?.NoWarn;
if (!string.IsNullOrEmpty(noWarn))
{
- var suppressedIds = noWarn!.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
+ var suppressedIds = noWarn!.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var id in suppressedIds)
{
- if (diagnostic.Id.Equals(id.Trim(), StringComparison.OrdinalIgnoreCase))
+ var code = id.Trim();
+ // Match full ID (e.g., "MAUIX2015") or bare numeric suffix (e.g., "2015")
+ if (code.Equals(diagnostic.Id, StringComparison.OrdinalIgnoreCase) ||
+ (diagnostic.Id.StartsWith("MAUIX", StringComparison.OrdinalIgnoreCase) &&
+ code == diagnostic.Id.Substring("MAUIX".Length)))
{
return; // Suppress this diagnostic
}
@@ -54,6 +58,7 @@ public void ReportDiagnostic(Diagnostic diagnostic)
public SourceGenContext? ParentContext { get; set; }
public ITypeSymbol? BaseType { get; } = baseType;
public IDictionary Types { get; } = new Dictionary();
+ public IDictionary BindingContextDataTypes { get; } = new Dictionary();
public IDictionary> KeysInRD { get; } = new Dictionary>();
public IDictionary<(ILocalValue, IFieldSymbol?, IPropertySymbol?), ILocalValue> VariablesProperties { get; } = new Dictionary<(ILocalValue, IFieldSymbol?, IPropertySymbol?), ILocalValue>();
public IList LocalMethods { get; } = new List();
diff --git a/src/Controls/src/SourceGen/Visitors/PropagateDataTypeVisitor.cs b/src/Controls/src/SourceGen/Visitors/PropagateDataTypeVisitor.cs
new file mode 100644
index 000000000000..db7752418f38
--- /dev/null
+++ b/src/Controls/src/SourceGen/Visitors/PropagateDataTypeVisitor.cs
@@ -0,0 +1,155 @@
+using System.Linq;
+using System.Xml;
+using Microsoft.CodeAnalysis;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Microsoft.Maui.Controls.SourceGen;
+
+///
+/// Top-down visitor that resolves x:DataType for every ElementNode in the tree and stores
+/// the result in .
+///
+/// This pre-computes type information so binding compilation can do an O(1) lookup instead
+/// of walking up the tree for every binding.
+///
+class PropagateDataTypeVisitor(SourceGenContext context) : IXamlNodeVisitor
+{
+ public TreeVisitingMode VisitingMode => TreeVisitingMode.TopDown;
+ public bool StopOnDataTemplate => true;
+ public bool StopOnResourceDictionary => false;
+ public bool VisitNodeOnDataTemplate => true;
+ public bool SkipChildren(INode node, INode parentNode) => false;
+ public bool IsResourceDictionary(ElementNode node) => node.IsResourceDictionary(context);
+
+ public void Visit(ValueNode node, INode parentNode) { }
+ public void Visit(MarkupNode node, INode parentNode) { }
+ public void Visit(ListNode node, INode parentNode) { }
+ public void Visit(RootNode node, INode parentNode) => Visit((ElementNode)node, parentNode);
+
+ public void Visit(ElementNode node, INode parentNode)
+ {
+ // 1. Check if this node declares x:DataType directly
+ if (node.Properties.TryGetValue(XmlName.xDataType, out INode? dataTypeNode))
+ {
+ var resolved = ResolveDataTypeNode(dataTypeNode, node);
+ context.BindingContextDataTypes[node] = resolved;
+ return;
+ }
+
+ // 2. Try to inherit from parent
+ var parentElement = GetParentElement(node);
+ if (parentElement is null)
+ return;
+
+ if (!context.BindingContextDataTypes.TryGetValue(parentElement, out var parentDataType))
+ return;
+
+ // ExplicitNull and Unresolved block inheritance
+ if (parentDataType.Kind != BindingContextDataTypeKind.Resolved)
+ return;
+
+ // DoesNotInheritDataType blocks inheritance
+ if (DoesNotInheritDataType(node))
+ return;
+
+ // Check if we're crossing a DataTemplate boundary
+ bool crossedBoundary = parentDataType.CrossedTemplateBoundary
+ || node.RepresentsType(XamlParser.MauiUri, "DataTemplate");
+
+ context.BindingContextDataTypes[node] = parentDataType with
+ {
+ IsInherited = true,
+ CrossedTemplateBoundary = crossedBoundary,
+ };
+ }
+
+ ///
+ /// Resolves an x:DataType property value node into a .
+ /// Handles {x:Null}, {x:Type}, and string type names.
+ ///
+ private BindingContextDataType ResolveDataTypeNode(INode dataTypeNode, ElementNode ownerNode)
+ {
+ // x:DataType="{x:Null}" — explicit opt-out
+ if (dataTypeNode.RepresentsType(XamlParser.X2009Uri, "NullExtension"))
+ {
+ return new BindingContextDataType(
+ BindingContextDataTypeKind.ExplicitNull,
+ Symbol: null,
+ Origin: ownerNode,
+ IsInherited: false,
+ CrossedTemplateBoundary: false);
+ }
+
+ // x:DataType="{x:Type vm:MyViewModel}" — lookup from context.Types
+ if (dataTypeNode.RepresentsType(XamlParser.X2009Uri, "TypeExtension"))
+ {
+ SourceGenContext? ctx = context;
+ while (ctx is not null)
+ {
+ if (ctx.Types.TryGetValue(dataTypeNode, out var symbol))
+ {
+ return new BindingContextDataType(
+ BindingContextDataTypeKind.Resolved,
+ Symbol: symbol,
+ Origin: ownerNode,
+ IsInherited: false,
+ CrossedTemplateBoundary: false);
+ }
+ ctx = ctx.ParentContext;
+ }
+
+ return Unresolved(ownerNode);
+ }
+
+ // x:DataType="vm:MyViewModel" — string type name
+ if (dataTypeNode is ValueNode { Value: string dataTypeName })
+ {
+ try
+ {
+ var dataType = TypeArgumentsParser.ParseSingle(
+ dataTypeName, ownerNode.NamespaceResolver, dataTypeNode as IXmlLineInfo);
+
+ if (dataType is not null
+ && dataType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out INamedTypeSymbol? resolved)
+ && resolved is not null)
+ {
+ return new BindingContextDataType(
+ BindingContextDataTypeKind.Resolved,
+ Symbol: resolved,
+ Origin: ownerNode,
+ IsInherited: false,
+ CrossedTemplateBoundary: false);
+ }
+ }
+ catch (XamlParseException)
+ {
+ // Fall through to Unresolved
+ }
+ }
+
+ return Unresolved(ownerNode);
+ }
+
+ private static BindingContextDataType Unresolved(ElementNode ownerNode)
+ => new(BindingContextDataTypeKind.Unresolved, Symbol: null, Origin: ownerNode, IsInherited: false, CrossedTemplateBoundary: false);
+
+ private bool DoesNotInheritDataType(ElementNode node)
+ {
+ return GetParentElement(node) is ElementNode parentNode
+ && parentNode.XmlType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out INamedTypeSymbol? parentTypeSymbol)
+ && parentTypeSymbol is not null
+ && node.TryGetPropertyName(parentNode, out XmlName propertyName)
+ && parentTypeSymbol.GetAllProperties(propertyName.LocalName, context).FirstOrDefault() is IPropertySymbol propertySymbol
+ && propertySymbol.GetAttributes().Any(a => a.AttributeClass?.ToFQDisplayString() == "global::Microsoft.Maui.Controls.Xaml.DoesNotInheritDataTypeAttribute");
+ }
+
+ private static ElementNode? GetParentElement(ElementNode node)
+ {
+ var parent = node.Parent;
+ if (parent is ListNode listNode)
+ return listNode.Parent as ElementNode;
+ if (parent is ElementNode elementNode)
+ return elementNode;
+ return null;
+ }
+}
diff --git a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs
index 1291cb5f05e4..84b8a392f1d0 100644
--- a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs
+++ b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs
@@ -1,3 +1,4 @@
+using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
@@ -35,11 +36,59 @@ class SetPropertiesVisitor(SourceGenContext context, bool stopOnResourceDictiona
public bool SkipChildren(INode node, INode parentNode) => node is ElementNode en && en.IsLazyResource(parentNode, Context);
public bool IsResourceDictionary(ElementNode node) => node.IsResourceDictionary(Context);
+ // Track properties that have been set to detect duplicates
+ readonly Dictionary> setProperties = new Dictionary>();
+
+ void CheckForDuplicateProperty(ElementNode parentNode, XmlName propertyName, IXmlLineInfo lineInfo)
+ {
+ if (!setProperties.TryGetValue(parentNode, out var props))
+ {
+ props = new HashSet();
+ setProperties[parentNode] = props;
+ }
+
+ if (!props.Add(propertyName))
+ {
+ var propertyDisplayName = $"{parentNode.XmlType.Name}.{propertyName.LocalName}";
+ var location = LocationCreate(Context.ProjectItem.RelativePath!, lineInfo, propertyDisplayName);
+ context.ReportDiagnostic(Diagnostic.Create(Descriptors.DuplicatePropertyAssignment, location, propertyDisplayName));
+ }
+ }
+
+ ///
+ /// Resolves the type of an implicit content property and, when the property is a non-collection
+ /// non-Object type, calls to emit a diagnostic if it has
+ /// already been assigned.
+ ///
+ /// XmlName used for the GetBindableProperty lookup (namespace may differ from ).
+ /// XmlName used as the deduplication key (always uses the parent element's namespace).
+ void CheckImplicitContentForDuplicate(
+ ILocalValue parentVar,
+ ElementNode parentNode,
+ XmlName lookupName,
+ XmlName trackingName,
+ IXmlLineInfo lineInfo)
+ {
+ bool attached = false;
+ var localName = lookupName.LocalName;
+ var bpFieldSymbol = parentVar.Type.GetBindableProperty(lookupName.NamespaceURI, ref localName, out attached, context, lineInfo);
+ ITypeSymbol? propertyType = null;
+
+ bool hasProperty = (bpFieldSymbol != null && SetPropertyHelpers.CanGetValue(parentVar, bpFieldSymbol, attached, context, out propertyType))
+ || SetPropertyHelpers.CanGet(parentVar, localName, context, out propertyType, out _);
+
+ bool isObject = propertyType != null && propertyType.SpecialType == SpecialType.System_Object;
+ if (hasProperty && propertyType != null && !isObject && !propertyType.CanAdd(context))
+ CheckForDuplicateProperty(parentNode, trackingName, lineInfo);
+ }
+
public void Visit(ValueNode node, INode parentNode)
{
//TODO support Label text as element
// if it's implicit content property, get the content property name
+ bool isImplicitContentProperty = false;
+ ILocalValue? parentVar = null;
if (!node.TryGetPropertyName(parentNode, out XmlName propertyName))
{
if (!parentNode.IsCollectionItem(node))
@@ -47,9 +96,12 @@ public void Visit(ValueNode node, INode parentNode)
string? contentProperty;
if (!Context.Variables.ContainsKey((ElementNode)parentNode))
return;
- var parentVariable = Context.Variables[(ElementNode)parentNode];
- if ((contentProperty = parentVariable.Type.GetContentPropertyName(context)) != null)
+ parentVar = Context.Variables[(ElementNode)parentNode];
+ if ((contentProperty = parentVar.Type.GetContentPropertyName(context)) != null)
+ {
propertyName = new XmlName(((ElementNode)parentNode).NamespaceURI, contentProperty);
+ isImplicitContentProperty = true;
+ }
else
return;
}
@@ -63,13 +115,12 @@ public void Visit(ValueNode node, INode parentNode)
if (propertyName.Equals(XmlName.mcIgnorable))
return;
- // Check that parent has a variable before accessing
- var parentElement = (ElementNode)parentNode;
- if (!Context.Variables.TryGetValue(parentElement, out var parentVar))
- {
- var parentKey = parentElement.Properties.TryGetValue(XmlName.xKey, out var keyNode) && keyNode is ValueNode vn ? vn.Value?.ToString() : "(none)";
- throw new System.InvalidOperationException($"SetPropertiesVisitor.Visit(ValueNode): Parent '{parentElement.XmlType.Name}' x:Key='{parentKey}' not in Variables for propertyName='{propertyName}'");
- }
+ parentVar ??= Context.Variables.TryGetValue((ElementNode)parentNode, out var pv) ? pv : null;
+ // Parent element not yet in Variables — can occur for markup extensions or nodes
+ // processed before their parent is fully initialized. Silently skip; the assignment
+ // will be handled through another visitor pass.
+ if (parentVar == null)
+ return;
// Try to set runtime name (for x:Name with RuntimeNameProperty attribute)
if (TrySetRuntimeName(propertyName, parentVar, node))
@@ -79,6 +130,10 @@ public void Visit(ValueNode node, INode parentNode)
if (propertyName == XmlName.xName)
return;
+ // Check for duplicate property assignment for implicit content properties
+ if (isImplicitContentProperty)
+ CheckImplicitContentForDuplicate(parentVar, (ElementNode)parentNode, propertyName, propertyName, node);
+
SetPropertyHelpers.SetPropertyValue(Writer, parentVar, propertyName, node, Context);
}
@@ -182,9 +237,22 @@ public void Visit(ElementNode node, INode parentNode)
ParentContext = context,
};
+ // Seed the template context with the parent's BindingContextDataType for the template
+ // root node. This preserves the CrossedTemplateBoundary flag so that bindings inside
+ // the template that inherit from outside get the correct warning.
+ if (context.BindingContextDataTypes.TryGetValue(parentNode, out var parentDataType)
+ && parentDataType.Kind == BindingContextDataTypeKind.Resolved)
+ {
+ templateContext.BindingContextDataTypes[parentNode] = parentDataType with
+ {
+ CrossedTemplateBoundary = true,
+ };
+ }
+
//inflate the template
node.Accept(new CreateValuesVisitor(templateContext), null);
node.Accept(new SetNamescopesAndRegisterNamesVisitor(templateContext), null);
+ node.Accept(new PropagateDataTypeVisitor(templateContext), null);
// node.Accept(new SetFieldVisitor(templateContext), null);
node.Accept(new SetResourcesVisitor(templateContext), null);
node.Accept(new SetPropertiesVisitor(templateContext, stopOnResourceDictionary: true), null);
@@ -207,6 +275,7 @@ public void Visit(ElementNode node, INode parentNode)
var key1 = ((ElementNode)parentNode).Properties.TryGetValue(XmlName.xKey, out var kn1) && kn1 is ValueNode vn1 ? vn1.Value?.ToString() : "(none)";
throw new System.InvalidOperationException($"SetPropertiesVisitor.Visit(ElementNode) propertyName != Empty: Parent '{((ElementNode)parentNode).XmlType.Name}' x:Key='{key1}' not in Variables");
}
+
SetPropertyHelpers.SetPropertyValue(Writer, pVar1, propertyName, node, Context);
}
else if (parentNode.IsCollectionItem(node) && parentNode is ElementNode)
@@ -225,8 +294,14 @@ public void Visit(ElementNode node, INode parentNode)
var name = new XmlName(node.NamespaceURI, contentProperty);
if (skips.Contains(name))
return;
- if (parentNode is ElementNode node1 && node1.SkipProperties.Contains(propertyName))
+ if (parentNode is ElementNode node1 && node1.SkipProperties.Contains(name))
return;
+
+ // Only check for duplicate property assignment if the property is not a collection
+ // Use parent namespace for duplicate tracking to match how explicit property assignments are keyed
+ var contentPropertyName = new XmlName(((ElementNode)parentNode).NamespaceURI, contentProperty);
+ CheckImplicitContentForDuplicate(parentVar, (ElementNode)parentNode, name, contentPropertyName, node);
+
SetPropertyHelpers.SetPropertyValue(Writer, parentVar, name, node, Context);
}
else if (parentVar.Type.CanAdd(context))
diff --git a/src/Controls/src/SourceGen/xlf/MauiGResources.Designer.cs b/src/Controls/src/SourceGen/xlf/MauiGResources.Designer.cs
index a6ceac0485d0..2d51af07e9c0 100644
--- a/src/Controls/src/SourceGen/xlf/MauiGResources.Designer.cs
+++ b/src/Controls/src/SourceGen/xlf/MauiGResources.Designer.cs
@@ -267,9 +267,10 @@ internal static string DuplicateTypeError
// Namescope diagnostics
internal static string NamescopeError => ResourceManager.GetString("NamescopeError", resourceCulture);
internal static string NamescopeDuplicate => ResourceManager.GetString("NamescopeDuplicate", resourceCulture);
-
// Event handler diagnostics
internal static string MissingEventHandlerTitle => ResourceManager.GetString("MissingEventHandlerTitle", resourceCulture);
internal static string MissingEventHandler => ResourceManager.GetString("MissingEventHandler", resourceCulture);
+ internal static string DuplicatePropertyAssignmentTitle => ResourceManager.GetString("DuplicatePropertyAssignmentTitle", resourceCulture);
+ internal static string DuplicatePropertyAssignmentMessage => ResourceManager.GetString("DuplicatePropertyAssignmentMessage", resourceCulture);
}
}
diff --git a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
index 61471531093e..cd520fe8e676 100644
--- a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
+++ b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs
@@ -298,11 +298,62 @@ public class ItemModel
.AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
- // x:Reference bindings skip compilation entirely — no MAUIG2045 should be emitted
- // for properties on the DataTemplate's x:DataType (ItemModel).
+ // x:Reference resolves to ContentPage; BindingContext is 'object', so SelectItemCommand
+ // can't be resolved statically. MAUIG2045 is suppressed for x:Reference bindings.
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045" && d.GetMessage().Contains("ItemModel", StringComparison.Ordinal));
}
+ [Fact]
+ public void BindingWithXReferenceToNonRootElement_ResolvesCorrectType()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+""";
+
+ var csharp =
+"""
+namespace Test;
+
+public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }
+
+public class ViewModel
+{
+ public System.Collections.Generic.List Items { get; set; }
+}
+
+public class ItemModel
+{
+ public string Name { get; set; }
+}
+""";
+
+ var compilation = CreateMauiCompilation()
+ .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
+ var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
+
+ // Path=Text resolves against Label (the x:Reference target), not ItemModel (x:DataType).
+ // Label.Text exists, so there should be no MAUIG2045 at all.
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
+ }
+
[Fact]
public void BindingIndexerTypeUnsupported_ReportsCorrectDiagnostic()
{
diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/MultipleChildrenWarningTests.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/MultipleChildrenWarningTests.cs
new file mode 100644
index 000000000000..f7553c4e585c
--- /dev/null
+++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/MultipleChildrenWarningTests.cs
@@ -0,0 +1,193 @@
+using System;
+using System.IO;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Xunit;
+
+namespace Microsoft.Maui.Controls.SourceGen.UnitTests;
+
+public class MultipleChildrenWarningTests : SourceGenXamlInitializeComponentTestBase
+{
+ [Fact]
+ public void BorderWithMultipleChildren_EmitsWarning()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+"""
+;
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Test;
+
+[XamlProcessing(XamlInflator.SourceGen)]
+partial class TestPage : ContentPage
+{
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+}
+"""
+;
+
+ var (result, generated) = RunGenerator(xaml, code);
+
+ // Verify that a warning diagnostic was emitted
+ var warnings = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning).ToArray();
+ Assert.True(warnings.Length > 0, "Expected at least one warning diagnostic");
+
+ var multipleChildrenWarning = warnings.FirstOrDefault(d => d.Id == "MAUIX2015");
+ Assert.NotNull(multipleChildrenWarning);
+ Assert.Contains("Border.Content", multipleChildrenWarning.GetMessage(), StringComparison.Ordinal);
+ Assert.Contains("multiple times", multipleChildrenWarning.GetMessage(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void ContentPageWithMultipleChildren_EmitsWarning()
+ {
+ var xaml =
+"""
+
+
+
+
+
+"""
+;
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Test;
+
+[XamlProcessing(XamlInflator.SourceGen)]
+partial class TestPage : ContentPage
+{
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+}
+"""
+;
+
+ var (result, generated) = RunGenerator(xaml, code);
+
+ // Verify that a warning diagnostic was emitted
+ var warnings = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Warning).ToArray();
+ Assert.True(warnings.Length > 0, "Expected at least one warning diagnostic");
+
+ var multipleChildrenWarning = warnings.FirstOrDefault(d => d.Id == "MAUIX2015");
+ Assert.NotNull(multipleChildrenWarning);
+ Assert.Contains("ContentPage.Content", multipleChildrenWarning.GetMessage(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void BorderWithSingleChild_NoWarning()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+"""
+;
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Test;
+
+[XamlProcessing(XamlInflator.SourceGen)]
+partial class TestPage : ContentPage
+{
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+}
+"""
+;
+
+ var (result, generated) = RunGenerator(xaml, code);
+
+ // Verify that NO warning diagnostic was emitted for single child
+ var multipleChildrenWarning = result.Diagnostics.FirstOrDefault(d => d.Id == "MAUIX2015");
+ Assert.Null(multipleChildrenWarning);
+ }
+
+ [Fact]
+ public void StackLayoutWithMultipleChildren_NoWarning()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+
+"""
+;
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Test;
+
+[XamlProcessing(XamlInflator.SourceGen)]
+partial class TestPage : ContentPage
+{
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+}
+"""
+;
+
+ var (result, generated) = RunGenerator(xaml, code);
+
+ // VerticalStackLayout is a collection type, so multiple children are valid
+ var multipleChildrenWarning = result.Diagnostics.FirstOrDefault(d => d.Id == "MAUIX2015");
+ Assert.Null(multipleChildrenWarning);
+ }
+}
diff --git a/src/Controls/tests/SourceGen.UnitTests/XReferenceBindingTests.cs b/src/Controls/tests/SourceGen.UnitTests/XReferenceBindingTests.cs
new file mode 100644
index 000000000000..e7571d4ca466
--- /dev/null
+++ b/src/Controls/tests/SourceGen.UnitTests/XReferenceBindingTests.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.Maui.Controls.SourceGen;
+using Xunit;
+
+using static Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver;
+
+namespace Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen;
+
+public class XReferenceBindingTests : SourceGenTestsBase
+{
+ private record AdditionalXamlFile(string Path, string Content, string? RelativePath = null, string? TargetPath = null, string? ManifestResourceName = null, string? TargetFramework = null, string? NoWarn = null)
+ : AdditionalFile(Text: SourceGeneratorDriver.ToAdditionalText(Path, Content), Kind: "Xaml", RelativePath: RelativePath ?? Path, TargetPath: TargetPath, ManifestResourceName: ManifestResourceName, TargetFramework: TargetFramework, NoWarn: NoWarn);
+
+ [Fact]
+ public void XReferenceDirectProperty_DoesNotFallBackSilently()
+ {
+ // Binding to a direct property on a referenced element (e.g., Slider.Value)
+ // should attempt compilation. If the property exists on the element type,
+ // no MAUIG2045 diagnostic should be emitted.
+ var xaml =
+"""
+
+
+
+
+
+
+
+""";
+
+ var csharp =
+"""
+namespace Test;
+
+public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }
+""";
+
+ var compilation = CreateMauiCompilation()
+ .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
+ var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
+
+ // Value exists on Slider — should not report property not found
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
+ }
+
+ [Fact]
+ public void XReferenceInDataTemplate_DoesNotReportFalsePositive()
+ {
+ // x:Reference bindings inside DataTemplates should NOT produce MAUIG2045
+ // for the DataTemplate's x:DataType
+ var xaml =
+"""
+
+
+
+
+
+
+
+
+
+
+""";
+
+ var csharp =
+"""
+namespace Test;
+
+public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }
+
+public class ViewModel
+{
+ public System.Collections.Generic.List Items { get; set; }
+ public Microsoft.Maui.Controls.Command SelectItemCommand { get; set; }
+}
+
+public class ItemModel
+{
+ public string Name { get; set; }
+}
+""";
+
+ var compilation = CreateMauiCompilation()
+ .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
+ var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
+
+ // Should NOT report property not found on ItemModel for the x:Reference binding
+ Assert.DoesNotContain(result.Diagnostics,
+ d => d.Id == "MAUIG2045" && d.GetMessage().Contains("ItemModel", StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public void XReferenceNonExistentProperty_FallsBackToRuntime()
+ {
+ // A binding to a non-existent property on a referenced element falls back to
+ // runtime binding without reporting MAUIG2045 — x:Reference bindings suppress
+ // property-not-found diagnostics since the binding may work via reflection.
+ var xaml =
+"""
+
+
+
+
+
+
+
+""";
+
+ var csharp =
+"""
+namespace Test;
+
+public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }
+""";
+
+ var compilation = CreateMauiCompilation()
+ .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
+ var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
+
+ // x:Reference bindings suppress MAUIG2045 — silently fall back to runtime
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
+ }
+
+ [Fact]
+ public void XDataTypeExplicitNull_BlocksInheritance()
+ {
+ // x:DataType="{x:Null}" should block inheritance and prevent compiled bindings
+ var xaml =
+"""
+
+
+
+
+
+
+""";
+
+ var csharp =
+"""
+namespace Test;
+
+public partial class TestPage : Microsoft.Maui.Controls.ContentPage { }
+
+public class ViewModel
+{
+ public string Name { get; set; }
+}
+""";
+
+ var compilation = CreateMauiCompilation()
+ .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp));
+ var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false);
+
+ // x:DataType="{x:Null}" opts out of compilation — no MAUIG2045 should be reported
+ // because the binding is not compiled at all (falls back to runtime)
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045");
+ }
+}
diff --git a/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj b/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj
index c83f7f509fb4..82f200260b14 100644
--- a/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj
+++ b/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj
@@ -6,7 +6,7 @@
Microsoft.Maui.Controls.Xaml.UnitTests
4
$(NoWarn);0672;0219;0414;CS0436;CS0618;CA2252
- $(WarningsNotAsErrors);XC0618;XC0022;XC0023;XC0025;XC0045;MAUIG2045;MAUID1000;MAUIX2005
+ $(WarningsNotAsErrors);XC0618;XC0022;XC0023;XC0025;XC0045;XC0067;MAUIG2045;MAUID1000;MAUIX2005;MAUIX2015
false
true
true
@@ -76,6 +76,13 @@
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs
index 266e592b4a55..87065fa97f15 100644
--- a/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs
@@ -18,12 +18,15 @@ public class Tests : IDisposable
[Theory]
[XamlInflatorData]
- internal void BindingsWithSourceAndInvalidPathAreNotCompiled(XamlInflator inflator)
+ internal void BindingsWithXReferenceSourceResolveAgainstReferencedType(XamlInflator inflator)
{
+ // Source={x:Reference page} points to ContentPage, which has a Content property.
+ // The binding path "Content" resolves against ContentPage, so the source generator
+ // compiles it instead of falling back to runtime Binding.
var view = new Gh3606(inflator);
var binding = view.Label.GetContext(Label.TextProperty).Bindings.GetValue();
- Assert.IsType(binding);
+ Assert.IsAssignableFrom(binding);
}
}
}
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Gh5095.rt.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Gh5095.rt.xaml.cs
index 3502c18b32e6..5ce5c883775a 100644
--- a/src/Controls/tests/Xaml.UnitTests/Issues/Gh5095.rt.xaml.cs
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Gh5095.rt.xaml.cs
@@ -1,3 +1,4 @@
+using Microsoft.CodeAnalysis;
using Microsoft.Maui.Controls.Build.Tasks;
using Xunit;
@@ -35,8 +36,8 @@ public partial class Gh5095 : ContentPage
""")
.RunMauiSourceGenerator(typeof(Gh5095));
- //FIXME check the diagnostic code
- Assert.Single(result.Diagnostics);
+ Assert.Contains(result.Diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+ Assert.Contains(result.Diagnostics, d => d.Id == "MAUIX2015");
}
}
}
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui3059.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui3059.xaml
new file mode 100644
index 000000000000..f78d29372379
--- /dev/null
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui3059.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui3059.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui3059.xaml.cs
new file mode 100644
index 000000000000..da3a7b3319da
--- /dev/null
+++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui3059.xaml.cs
@@ -0,0 +1,39 @@
+using System;
+using Microsoft.Maui.Controls.Core.UnitTests;
+using Microsoft.Maui.Dispatching;
+using Microsoft.Maui.UnitTests;
+using Xunit;
+
+namespace Microsoft.Maui.Controls.Xaml.UnitTests;
+
+public partial class Maui3059 : ContentPage
+{
+ public Maui3059()
+ {
+ InitializeComponent();
+ }
+
+ [Collection("Issue")]
+ public class Tests : IDisposable
+ {
+ public Tests() => DispatcherProvider.SetCurrent(new DispatcherProviderStub());
+ public void Dispose() => DispatcherProvider.SetCurrent(null);
+
+ [Theory]
+ [XamlInflatorData]
+ internal void BorderWithMultipleChildren_OnlyLastChildIsUsed(XamlInflator inflator)
+ {
+ var page = new Maui3059(inflator);
+
+ Assert.NotNull(page.Content);
+ Assert.IsType(page.Content);
+
+ var border = (Microsoft.Maui.Controls.Border)page.Content;
+ Assert.NotNull(border.Content);
+ Assert.IsType