diff --git a/src/Controls/src/SourceGen/ITypeSymbolExtensions.Maui.cs b/src/Controls/src/SourceGen/ITypeSymbolExtensions.Maui.cs
index 0b09b9bab3ff..0e7a1a655fd4 100644
--- a/src/Controls/src/SourceGen/ITypeSymbolExtensions.Maui.cs
+++ b/src/Controls/src/SourceGen/ITypeSymbolExtensions.Maui.cs
@@ -27,6 +27,119 @@ static partial class ITypeSymbolExtensions
return bpParentType.GetAllFields(name, context).FirstOrDefault(fi => fi.IsStatic && fi.Type.Equals(context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindableProperty"), SymbolEqualityComparer.Default));
}
+ ///
+ /// Checks if there's a property or field with a similar name that has a BindablePropertyAttribute.
+ /// This is a heuristic to support bindable properties generated by other source generators.
+ /// Returns the explicit property name if specified in the attribute, otherwise null.
+ ///
+ public static bool HasBindablePropertyHeuristic(this ITypeSymbol type, string propertyName, SourceGenContext context, out string? explicitPropertyName)
+ {
+ explicitPropertyName = null;
+
+ // Check if any property name variant has the BindablePropertyAttribute or AutoPropertyAttribute
+ foreach (var name in DerivePotentialPropertyNames(propertyName))
+ {
+ var property = type.GetAllProperties(name, context).FirstOrDefault();
+ if (property != null && HasBindablePropertyOrAutoPropertyAttribute(property, out var propName))
+ {
+ explicitPropertyName = propName;
+ return true;
+ }
+ }
+
+ // Check if any field name variant has the BindablePropertyAttribute or AutoPropertyAttribute
+ foreach (var name in DerivePotentialFieldNames(propertyName))
+ {
+ var field = type.GetAllFields(name, context).FirstOrDefault();
+ if (field != null && HasBindablePropertyOrAutoPropertyAttribute(field, out var propName))
+ {
+ explicitPropertyName = propName;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Derives potential property name variations from a XAML property name.
+ ///
+ private static IEnumerable DerivePotentialPropertyNames(string propertyName)
+ {
+ if (string.IsNullOrEmpty(propertyName))
+ yield break;
+
+ // Exact match (e.g., "Balance")
+ yield return propertyName;
+ }
+
+ ///
+ /// Derives potential field name variations from a XAML property name.
+ ///
+ private static IEnumerable DerivePotentialFieldNames(string propertyName)
+ {
+ if (string.IsNullOrEmpty(propertyName))
+ yield break;
+
+ // Underscore prefix with lowercase first letter (e.g., "_balance" for "Balance")
+ yield return $"_{char.ToLowerInvariant(propertyName[0])}{propertyName.Substring(1)}";
+
+ // Lowercase first letter (e.g., "balance" for "Balance")
+ yield return $"{char.ToLowerInvariant(propertyName[0])}{propertyName.Substring(1)}";
+
+ // Uppercase first letter (e.g., "Balance" for "Balance" - uncommon but supported)
+ yield return propertyName;
+ }
+
+ ///
+ /// List of recognized attribute full names that indicate a bindable property will be generated.
+ /// Easy to extend by adding new attribute names to this list.
+ ///
+ private static readonly string[] RecognizedBindablePropertyAttributes = new[]
+ {
+ "CommunityToolkit.Maui.BindablePropertyAttribute",
+ "SQuan.Helpers.Maui.Mvvm.BindablePropertyAttribute",
+ "Maui.BindableProperty.Generator.Core.AutoBindableAttribute"
+ };
+
+ ///
+ /// Checks if a symbol has a recognized bindable property attribute.
+ /// If the attribute has a PropertyName parameter, returns it in the out parameter.
+ ///
+ private static bool HasBindablePropertyOrAutoPropertyAttribute(ISymbol symbol, out string? explicitPropertyName)
+ {
+ explicitPropertyName = null;
+
+ foreach (var attr in symbol.GetAttributes())
+ {
+ var attrClass = attr.AttributeClass;
+ if (attrClass == null)
+ continue;
+
+ var fullTypeName = attrClass.ToString();
+
+ // Check if the attribute is in our list of recognized attributes
+ if (RecognizedBindablePropertyAttributes.Contains(fullTypeName))
+ {
+ // Try to get the PropertyName named parameter
+ foreach (var namedArg in attr.NamedArguments)
+ {
+ if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string propName)
+ {
+ explicitPropertyName = propName;
+ return true;
+ }
+ }
+
+ // Attribute found but no explicit PropertyName
+ explicitPropertyName = null;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
static bool GetNameAndTypeRef(ref ITypeSymbol elementType, string namespaceURI, ref string localname,
SourceGenContext context, IXmlLineInfo? lineInfo)
{
diff --git a/src/Controls/src/SourceGen/SetPropertyHelpers.cs b/src/Controls/src/SourceGen/SetPropertyHelpers.cs
index a2c893990b53..0805bd2d48c0 100644
--- a/src/Controls/src/SourceGen/SetPropertyHelpers.cs
+++ b/src/Controls/src/SourceGen/SetPropertyHelpers.cs
@@ -45,16 +45,16 @@ public static void SetPropertyValue(IndentedTextWriter writer, ILocalValue paren
}
//If it's a BP and the value is BindingBase, SetBinding
- if (!asCollectionItem && CanSetBinding(bpFieldSymbol, valueNode, context))
+ if (!asCollectionItem && CanSetBinding(bpFieldSymbol, valueNode, parentVar.Type, localName, context, out var explicitPropertyNameForBinding))
{
- SetBinding(writer, parentVar, bpFieldSymbol!, valueNode, context, getNodeValue);
+ SetBinding(writer, parentVar, bpFieldSymbol, localName, explicitPropertyNameForBinding, valueNode, context, getNodeValue);
return;
}
//If it's a BP, SetValue
- if (!asCollectionItem && CanSetValue(bpFieldSymbol, valueNode, context, getNodeValue))
+ if (!asCollectionItem && CanSetValue(bpFieldSymbol, valueNode, parentVar.Type, localName, context, getNodeValue, out var explicitPropertyNameForValue))
{
- SetValue(writer, parentVar, bpFieldSymbol!, valueNode, context, getNodeValue);
+ SetValue(writer, parentVar, bpFieldSymbol, localName, explicitPropertyNameForValue, valueNode, context, getNodeValue);
return;
}
@@ -232,73 +232,106 @@ static void ConnectEvent(IndentedTextWriter writer, ILocalValue parentVar, strin
}
}
- static bool CanSetValue(IFieldSymbol? bpFieldSymbol, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
+ static bool CanSetValue(IFieldSymbol? bpFieldSymbol, INode node, ITypeSymbol parentType, string localName, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue, out string? explicitPropertyName)
{
- if (bpFieldSymbol == null)
- return false;
- if (node is ValueNode vn && vn.CanConvertTo(bpFieldSymbol, context))
- return true;
- if (node is not ElementNode en)
- return false;
+ explicitPropertyName = null;
+
+ if (bpFieldSymbol != null)
+ {
+ // Normal BP case - apply existing logic
+ if (node is ValueNode vn && vn.CanConvertTo(bpFieldSymbol, context))
+ return true;
+ if (node is not ElementNode en)
+ return false;
- var localVar = getNodeValue(en, context.Compilation.ObjectType);
+ var localVar = getNodeValue(en, context.Compilation.ObjectType);
- // If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here.
- // Worst case scenario ? InvalidCastException at runtime
- if (localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
- return true;
+ // If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here.
+ // Worst case scenario ? InvalidCastException at runtime
+ if (localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
+ return true;
- var bpTypeAndConverter = bpFieldSymbol.GetBPTypeAndConverter(context);
- if (context.Compilation.HasImplicitConversion(localVar.Type, bpTypeAndConverter?.type))
- return true;
+ var bpTypeAndConverter = bpFieldSymbol.GetBPTypeAndConverter(context);
+ if (context.Compilation.HasImplicitConversion(localVar.Type, bpTypeAndConverter?.type))
+ return true;
- if (HasDoubleImplicitConversion(localVar.Type, bpTypeAndConverter?.type, context, out _))
- return true;
+ if (HasDoubleImplicitConversion(localVar.Type, bpTypeAndConverter?.type, context, out _))
+ return true;
- if (HasExplicitConversion(localVar.Type, bpTypeAndConverter?.type, context))
- return true;
+ if (HasExplicitConversion(localVar.Type, bpTypeAndConverter?.type, context))
+ return true;
- if (localVar.Type.InheritsFrom(bpTypeAndConverter?.type!, context))
- return true;
+ if (localVar.Type.InheritsFrom(bpTypeAndConverter?.type!, context))
+ return true;
- if (bpFieldSymbol.Type.IsInterface() && localVar.Type.Implements(bpTypeAndConverter?.type!))
- return true;
+ if (bpFieldSymbol.Type.IsInterface() && localVar.Type.Implements(bpTypeAndConverter?.type!))
+ return true;
+ return false;
+ }
+
+ // Heuristic: If BP is null but the type has a property/field with a BindablePropertyAttribute,
+ // assume the BP will be generated by another source generator
+ // Only apply this for non-BindingBase nodes (CanSetBinding handles BindingBase)
+ if (!string.IsNullOrEmpty(localName) && !IsBindingBaseNode(node, context))
+ {
+ return parentType.HasBindablePropertyHeuristic(localName, context, out explicitPropertyName);
+ }
+
return false;
}
- static void SetValue(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol bpFieldSymbol, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
+ static void SetValue(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, string localName, string? explicitPropertyName, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
{
- var pType = bpFieldSymbol.GetBPTypeAndConverter(context)?.type;
+ // Determine bindable property name: use BP field symbol if available, otherwise use heuristic
+ var bpName = bpFieldSymbol != null
+ ? bpFieldSymbol.ToFQDisplayString()
+ : $"{parentVar.Type.ToFQDisplayString()}.{explicitPropertyName ?? $"{localName}Property"}";
+
+ var pType = bpFieldSymbol?.GetBPTypeAndConverter(context)?.type;
+ var property = bpFieldSymbol == null ? parentVar.Type.GetAllProperties(localName, context).FirstOrDefault() : null;
+
if (node is ValueNode valueNode)
{
using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock())
{
- var valueString = valueNode.ConvertTo(bpFieldSymbol, writer,context, parentVar);
- writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpFieldSymbol.ToFQDisplayString()}, {valueString});");
+ var valueString = bpFieldSymbol != null
+ ? valueNode.ConvertTo(bpFieldSymbol, writer, context, parentVar)
+ : (property != null ? valueNode.ConvertTo(property, writer, context, parentVar) : getNodeValue(node, context.Compilation.ObjectType).ValueAccessor);
+ writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpName}, {valueString});");
}
}
else if (node is ElementNode elementNode)
+ {
using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock())
{
var localVar = getNodeValue(elementNode, context.Compilation.ObjectType);
- string cast = string.Empty;
+ var cast = string.Empty;
- if (HasDoubleImplicitConversion(localVar.Type, pType, context, out var conv))
+ if (bpFieldSymbol != null)
{
- cast = "(" + conv!.ReturnType.ToFQDisplayString() + ")";
- }
- else if (pType != null && !context.Compilation.HasImplicitConversion(localVar.Type, pType) && HasExplicitConversion(localVar.Type, pType, context))
- {
- // Only add cast if the source type is not object (object can be cast to anything at runtime)
- if (!localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
+ // BP case: check for double implicit conversion first
+ if (HasDoubleImplicitConversion(localVar.Type, pType, context, out var conv))
{
- cast = $"({pType.ToFQDisplayString()})";
+ cast = "(" + conv!.ReturnType.ToFQDisplayString() + ")";
}
+ else if (pType != null && !context.Compilation.HasImplicitConversion(localVar.Type, pType) && HasExplicitConversion(localVar.Type, pType, context))
+ {
+ // Only add cast if the source type is not object (object can be cast to anything at runtime)
+ if (!localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
+ {
+ cast = $"({pType.ToFQDisplayString()})";
+ }
+ }
+ }
+ else if (property != null && !context.Compilation.HasImplicitConversion(localVar.Type, property.Type))
+ {
+ cast = $"({property.Type.ToFQDisplayString()})";
}
- writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpFieldSymbol.ToFQDisplayString()}, {cast}{localVar.ValueAccessor});");
+ writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpName}, {cast}{localVar.ValueAccessor});");
}
+ }
}
static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext context, out ITypeSymbol? propertyType, out IPropertySymbol? propertySymbol)
@@ -426,31 +459,42 @@ static void Set(IndentedTextWriter writer, ILocalValue parentVar, string localNa
}
}
- static bool CanSetBinding(IFieldSymbol? bpFieldSymbol, INode node, SourceGenContext context)
+ static bool CanSetBinding(IFieldSymbol? bpFieldSymbol, INode node, ITypeSymbol parentType, string localName, SourceGenContext context, out string? explicitPropertyName)
{
-
- if (bpFieldSymbol == null)
- return false;
- if (node is not ElementNode en)
- return false;
- if (!context.Variables.TryGetValue(en, out var localVariable))
+ explicitPropertyName = null;
+
+ // Check if it's a BindingBase node
+ if (!IsBindingBaseNode(node, context))
return false;
-
- var bindingBaseSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!;
-
- if (localVariable.Type.InheritsFrom(bindingBaseSymbol, context))
- return true;
-
- if (context.Compilation.HasImplicitConversion(localVariable.Type, bindingBaseSymbol))
+
+ // If we have a BP field symbol, we can set binding
+ if (bpFieldSymbol != null)
return true;
-
+
+ // Heuristic: If BP is null but the type has a property/field with a BindablePropertyAttribute,
+ // assume the BP will be generated by another source generator
+ if (!string.IsNullOrEmpty(localName))
+ return parentType.HasBindablePropertyHeuristic(localName, context, out explicitPropertyName);
+
return false;
}
- static void SetBinding(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol bpFieldSymbol, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
+ static void SetBinding(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, string localName, string? explicitPropertyName, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
{
var localVariable = getNodeValue((ElementNode)node, context.Compilation.ObjectType);
- writer.WriteLine($"{parentVar.ValueAccessor}.SetBinding({bpFieldSymbol.ToFQDisplayString()}, {localVariable.ValueAccessor});");
+
+ if (bpFieldSymbol != null)
+ {
+ // Normal case: we have the BP field symbol
+ writer.WriteLine($"{parentVar.ValueAccessor}.SetBinding({bpFieldSymbol.ToFQDisplayString()}, {localVariable.ValueAccessor});");
+ }
+ else
+ {
+ // Heuristic case: generate SetBinding call using the expected BindableProperty name
+ // Use explicit property name if provided by attribute, otherwise use the default {localName}Property format
+ var bpName = explicitPropertyName ?? $"{localName}Property";
+ writer.WriteLine($"{parentVar.ValueAccessor}.SetBinding({parentVar.Type.ToFQDisplayString()}.{bpName}, {localVariable.ValueAccessor});");
+ }
}
static bool CanAdd(ILocalValue parentVar, string localName, IFieldSymbol? bpFieldSymbol, bool attached, INode valueNode, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
@@ -534,4 +578,23 @@ static void Add(IndentedTextWriter writer, ILocalValue parentVar, XmlName proper
using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)valueNode, context.ProjectItem) : PrePost.NoBlock())
writer.WriteLine($"{parentObj}.Add(({itemType.ToFQDisplayString()}){cast}{getNodeValue(valueNode, context.Compilation.ObjectType).ValueAccessor});");
}
+
+ static bool IsBindingBaseNode(INode node, SourceGenContext context)
+ {
+ if (node is not ElementNode en)
+ return false;
+ if (!context.Variables.TryGetValue(en, out var localVariable))
+ return false;
+
+ var bindingBaseSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!;
+
+ if (localVariable.Type.InheritsFrom(bindingBaseSymbol, context))
+ return true;
+
+ if (context.Compilation.HasImplicitConversion(localVariable.Type, bindingBaseSymbol))
+ return true;
+
+ return false;
+ }
+
}
diff --git a/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/BindablePropertyHeuristic.cs b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/BindablePropertyHeuristic.cs
new file mode 100644
index 000000000000..93def153fd92
--- /dev/null
+++ b/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/BindablePropertyHeuristic.cs
@@ -0,0 +1,386 @@
+using System;
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Maui.Controls.SourceGen.UnitTests;
+
+public class BindablePropertyHeuristic : SourceGenXamlInitializeComponentTestBase
+{
+ [Theory]
+ [InlineData("CommunityToolkit.Maui", "BindablePropertyAttribute", "", "BalanceProperty")]
+ [InlineData("CommunityToolkit.Maui", "BindablePropertyAttribute", "CustomBalance", "CustomBalance")]
+ [InlineData("Maui.BindableProperty.Generator.Core", "AutoBindableAttribute", "", "BalanceProperty")]
+ [InlineData("Maui.BindableProperty.Generator.Core", "AutoBindableAttribute", "CustomBalance", "CustomBalance")]
+ public void BindablePropertyHeuristic_WithPropertyAndBinding_ShouldGenerateSetBinding(
+ string attributeNamespace,
+ string attributeName,
+ string? explicitPropertyName,
+ string expectedPropertyFieldName)
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+""";
+
+ // Build the attribute usage with optional PropertyName parameter
+ var hasExplicitPropertyName = !string.IsNullOrEmpty(explicitPropertyName);
+ var attributeUsage = hasExplicitPropertyName ? $"[{attributeNamespace}.{attributeName.Replace("Attribute", "", StringComparison.Ordinal)}(PropertyName = \"{explicitPropertyName}\")]" : $"[{attributeNamespace}.{attributeName.Replace("Attribute", "", StringComparison.Ordinal)}]";
+
+ var code =
+$$"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace {{attributeNamespace}}
+{
+ // Simulates an attribute from a third-party library
+ public class {{attributeName}} : Attribute
+ {
+ public string? PropertyName { get; set; }
+ }
+}
+
+namespace Test
+{
+ [XamlProcessing(XamlInflator.SourceGen)]
+ public partial class TestPage : ContentPage
+ {
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+ }
+
+ public class BalanceView : Label
+ {
+ {{attributeUsage}}
+ public double Balance { get; set; }
+ }
+}
+""";
+
+ // assertNoCompilationErrors: false because BalanceProperty is expected to be generated by another source generator
+ var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: false);
+
+ // Should not have MAUIX2002 error
+ var mauix2002Diagnostics = result.Diagnostics.Where(d => d.Id == "MAUIX2002").ToList();
+ Assert.Empty(mauix2002Diagnostics);
+
+ // Should have generated code with the correct SetBinding call
+ Assert.NotNull(generated);
+ Assert.Contains($".SetBinding(global::Test.BalanceView.{expectedPropertyFieldName},", generated, StringComparison.Ordinal);
+ }
+
+ [Theory]
+ [InlineData("CommunityToolkit.Maui", "BindablePropertyAttribute", "_balance", "", "BalanceProperty")]
+ [InlineData("CommunityToolkit.Maui", "BindablePropertyAttribute", "_balance", "CustomBalance", "CustomBalance")]
+ [InlineData("CommunityToolkit.Maui", "BindablePropertyAttribute", "balance", "", "BalanceProperty")]
+ [InlineData("Maui.BindableProperty.Generator.Core", "AutoBindableAttribute", "_balance", "", "BalanceProperty")]
+ [InlineData("Maui.BindableProperty.Generator.Core", "AutoBindableAttribute", "_balance", "CustomBalance", "CustomBalance")]
+ [InlineData("Maui.BindableProperty.Generator.Core", "AutoBindableAttribute", "balance", "", "BalanceProperty")]
+ public void BindablePropertyHeuristic_WithFieldAndBinding_ShouldGenerateSetBinding(
+ string attributeNamespace,
+ string attributeName,
+ string fieldName,
+ string? explicitPropertyName,
+ string expectedPropertyFieldName)
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+""";
+
+ // Build the attribute usage with optional PropertyName parameter
+ var hasExplicitPropertyName = !string.IsNullOrEmpty(explicitPropertyName);
+ var attributeUsage = hasExplicitPropertyName ? $"[{attributeNamespace}.{attributeName.Replace("Attribute", "", StringComparison.Ordinal)}(PropertyName = \"{explicitPropertyName}\")]" : $"[{attributeNamespace}.{attributeName.Replace("Attribute", "", StringComparison.Ordinal)}]";
+
+ var code =
+$$"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace {{attributeNamespace}}
+{
+ // Simulates an attribute from a third-party library
+ public class {{attributeName}} : Attribute
+ {
+ public string? PropertyName { get; set; }
+ }
+}
+
+namespace Test
+{
+ [XamlProcessing(XamlInflator.SourceGen)]
+ public partial class TestPage : ContentPage
+ {
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+ }
+
+ public class BalanceView : Label
+ {
+ {{attributeUsage}}
+ private double {{fieldName}};
+ }
+}
+""";
+
+ // assertNoCompilationErrors: false because BalanceProperty is expected to be generated by another source generator
+ var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: false);
+
+ // Should not have MAUIX2002 error
+ var mauix2002Diagnostics = result.Diagnostics.Where(d => d.Id == "MAUIX2002").ToList();
+ Assert.Empty(mauix2002Diagnostics);
+
+ // Should have generated code with the correct SetBinding call
+ Assert.NotNull(generated);
+ Assert.Contains($".SetBinding(global::Test.BalanceView.{expectedPropertyFieldName},", generated, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void PropertyWithoutAttribute_ShouldProduceError()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+""";
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Test
+{
+ [XamlProcessing(XamlInflator.SourceGen)]
+ public partial class TestPage : ContentPage
+ {
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+ }
+
+ public class BalanceView : Label
+ {
+ // Property without BindablePropertyAttribute - should still fail
+ public double Balance { get; set; }
+ }
+}
+""";
+
+ var (result, generated) = RunGenerator(xaml, code);
+
+ // Should have MAUIX2002 error because there's no BindableProperty and no attribute
+ var mauix2002Diagnostics = result.Diagnostics.Where(d => d.Id == "MAUIX2002").ToList();
+ Assert.NotEmpty(mauix2002Diagnostics);
+ }
+
+ [Fact]
+ public void PropertyWithAttribute_ButDifferentName_ShouldProduceError()
+ {
+ var xaml =
+"""
+
+
+
+
+
+
+
+""";
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace CommunityToolkit.Maui
+{
+ // Simulates an attribute from a third-party library
+ public class BindablePropertyAttribute : Attribute
+ {
+ }
+}
+
+namespace Test
+{
+ [XamlProcessing(XamlInflator.SourceGen)]
+ public partial class TestPage : ContentPage
+ {
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+ }
+
+ public class BalanceView : Label
+ {
+ // Property with completely different name - should still fail
+ [CommunityToolkit.Maui.BindableProperty]
+ public double Amount { get; set; }
+ }
+}
+""";
+
+ var (result, generated) = RunGenerator(xaml, code);
+
+ // Should have MAUIX2002 error because the name doesn't match
+ var mauix2002Diagnostics = result.Diagnostics.Where(d => d.Id == "MAUIX2002").ToList();
+ Assert.NotEmpty(mauix2002Diagnostics);
+ }
+
+ [Fact]
+ public void PropertyWithAttribute_LiteralValue_ShouldUseSetValue()
+ {
+ var xaml =
+"""
+
+
+
+
+""";
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace CommunityToolkit.Maui
+{
+ // Simulates an attribute from a third-party library
+ public class BindablePropertyAttribute : Attribute
+ {
+ public string? PropertyName { get; set; }
+ }
+}
+
+namespace Test
+{
+ [XamlProcessing(XamlInflator.SourceGen)]
+ public partial class TestPage : ContentPage
+ {
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+ }
+
+ public class BalanceView : Label
+ {
+ // Has attribute - literal assignments should use SetValue with the generated BP
+ [CommunityToolkit.Maui.BindableProperty]
+ public double Balance { get; set; }
+ }
+}
+""";
+
+ // assertNoCompilationErrors: false because BalanceProperty is expected to be generated by another source generator
+ var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: false);
+
+ // Should NOT have MAUIX2002 error
+ var mauix2002Diagnostics = result.Diagnostics.Where(d => d.Id == "MAUIX2002").ToList();
+ Assert.Empty(mauix2002Diagnostics);
+
+ // Should use SetValue with the generated BP, not property setter
+ Assert.NotNull(generated);
+ Assert.Contains(".SetValue(global::Test.BalanceView.BalanceProperty,", generated, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void PropertyWithoutAttribute_LiteralValue_ShouldUsePropertySetter()
+ {
+ var xaml =
+"""
+
+
+
+
+""";
+
+ var code =
+"""
+using System;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Test
+{
+ [XamlProcessing(XamlInflator.SourceGen)]
+ public partial class TestPage : ContentPage
+ {
+ public TestPage()
+ {
+ InitializeComponent();
+ }
+ }
+
+ public class BalanceView : Label
+ {
+ // No attribute - should fall back to property setter
+ public double Balance { get; set; }
+ }
+}
+""";
+
+ var (result, generated) = RunGenerator(xaml, code);
+
+ // Should NOT have MAUIX2002 error
+ var mauix2002Diagnostics = result.Diagnostics.Where(d => d.Id == "MAUIX2002").ToList();
+ Assert.Empty(mauix2002Diagnostics);
+
+ // Should use property setter since there's no attribute
+ Assert.NotNull(generated);
+ Assert.Contains(".Balance = ", generated, StringComparison.Ordinal);
+ }
+}