Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
57877ec
Extend BindableProperty source gen to handle partial property initial…
stephenquan Dec 5, 2025
a9085ef
Update CommonUsageTests.cs
TheCodeTraveler Dec 5, 2025
fba8b25
Only implement new initializer code for when initializers are present
stephenquan Dec 5, 2025
fd700dc
Revert IntegrationTests
stephenquan Dec 5, 2025
b2dc373
Revert setter accessibility changes
stephenquan Dec 5, 2025
30fabbd
Merge branch 'feature/bp-sourcegen-init' of github.com:stephenquan/Ma…
stephenquan Dec 5, 2025
6ddcd78
Corrected setter
stephenquan Dec 5, 2025
c4bda26
Refactor GetDefaultValueCreatorMethod. Update XML Doc. Apply to reado…
stephenquan Dec 5, 2025
06dd458
Include global root namespace for BindableObject
stephenquan Dec 5, 2025
825adf8
Corrected typo in GetDefautValueCreatorMethodName
stephenquan Dec 5, 2025
1727770
Revert initializer-setter changes (not needed). Refactor getter-initi…
stephenquan Dec 5, 2025
9d785b6
Remove unneeded test from BindablePropertyModelTests
stephenquan Dec 5, 2025
8c823de
Refctor EffectiveDefaultValueCreatorMethodName property (replacing Ge…
stephenquan Dec 6, 2025
ebab9e1
Update BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDe…
stephenquan Dec 6, 2025
c80418c
Corrected unit test name GenerateBindableProperty_WithInitializer_Gen…
stephenquan Dec 6, 2025
0aed7ad
Refactor InitializingPropertyName property
stephenquan Dec 6, 2025
1ba82b4
Add Edge Case Test `GenerateBindableProperty_WithBothInitializerAndDe…
TheCodeTraveler Dec 8, 2025
50b229b
Merge branch 'main' into feature/bp-sourcegen-init
TheCodeTraveler Dec 8, 2025
1ce2b6a
Merge branch 'main' into pr/2987
TheCodeTraveler Dec 8, 2025
07d0087
Use `"null"` instead of `string.IsNullOrEmpty()`
TheCodeTraveler Dec 8, 2025
45adcdf
Merge branch 'feature/bp-sourcegen-init' of https://github.com/stephe…
TheCodeTraveler Dec 8, 2025
3a642d0
Remove `DefaultValue`
TheCodeTraveler Dec 8, 2025
77b3fa5
Move out-of-scope variables to `file static class`
TheCodeTraveler Dec 8, 2025
fc82b82
Use raw string literal
TheCodeTraveler Dec 8, 2025
4060d68
Update variable name
TheCodeTraveler Dec 9, 2025
a382be9
Implement `[BindableProperty]` for `AvatarView`
TheCodeTraveler Dec 9, 2025
a7ad7bc
Include class name in `file static class`
TheCodeTraveler Dec 9, 2025
ead0fea
Use `[BindableProperty]` for AvatarView
TheCodeTraveler Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -652,4 +652,74 @@ public partial class {{defaultTestClassName}}

await VerifySourceGeneratorAsync(source, expectedGenerated);
}


[Fact]
public async Task GenerateBindableProperty_WithInitializers_GeneratesCorrectCode()
{
const string source =
/* language=C#-test */
//lang=csharp
$$"""
using CommunityToolkit.Maui;
using Microsoft.Maui.Controls;
using System;

namespace {{defaultTestNamespace}};

public partial class {{defaultTestClassName}} : View
{
[BindablePropertyAttribute]
public partial string Text { get; set; } = "Initial Value";

[BindablePropertyAttribute]
public partial TimeSpan CustomDuration { get; set; } = TimeSpan.FromSeconds(30);
}
""";

const string expectedGenerated =
/* language=C#-test */
//lang=csharp
$$"""
// <auto-generated>
// See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
#pragma warning disable
#nullable enable
namespace {{defaultTestNamespace}};
public partial class {{defaultTestClassName}}
{
/// <summary>
/// Backing BindableProperty for the <see cref = "Text"/> property.
/// </summary>
public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __createDefaultText);
bool __initializingText = false;
static object __createDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable)
{
(({{defaultTestClassName}})bindable).__initializingText = true;
var defaultValue = (({{defaultTestClassName}})bindable).Text;
(({{defaultTestClassName}})bindable).__initializingText = false;
return defaultValue;
}

public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); }

/// <summary>
/// Backing BindableProperty for the <see cref = "CustomDuration"/> property.
/// </summary>
public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __createDefaultCustomDuration);
bool __initializingCustomDuration = false;
static object __createDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable)
{
(({{defaultTestClassName}})bindable).__initializingCustomDuration = true;
var defaultValue = (({{defaultTestClassName}})bindable).CustomDuration;
(({{defaultTestClassName}})bindable).__initializingCustomDuration = false;
return defaultValue;
}

public partial System.TimeSpan CustomDuration { get => __initializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, field = value); }
}
""";

await VerifySourceGeneratorAsync(source, expectedGenerated);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public void BindablePropertyName_ReturnsCorrectPropertyName()
"null",
string.Empty,
true, // IsReadOnlyBindableProperty
string.Empty // SetterAccessibility
string.Empty, // SetterAccessibility
false
);

// Act
Expand All @@ -56,6 +57,7 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues()
const string coerceValueMethodName = "CoerceValue";
const string defaultValueCreatorMethodName = "CreateDefaultValue";
const string newKeywordText = "new ";
const bool hasInitializer = false;

// Act
var model = new BindablePropertyModel(
Expand All @@ -71,7 +73,8 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues()
defaultValueCreatorMethodName,
newKeywordText,
true, // IsReadOnlyBindableProperty
string.Empty // SetterAccessibility
string.Empty, // SetterAccessibility
hasInitializer
);

// Assert
Expand All @@ -86,6 +89,7 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues()
Assert.Equal(coerceValueMethodName, model.CoerceValueMethodName);
Assert.Equal(defaultValueCreatorMethodName, model.DefaultValueCreatorMethodName);
Assert.Equal(newKeywordText, model.NewKeywordText);
Assert.Equal(hasInitializer, model.HasInitializer);
Assert.Equal("TestPropertyProperty", model.BindablePropertyName);
}

Expand Down Expand Up @@ -128,7 +132,8 @@ public void SemanticValues_WithClassInfoAndProperties_StoresCorrectValues()
"null",
string.Empty,
true, // IsReadOnlyBindableProperty
string.Empty // SetterAccessibilityText
string.Empty, // SetterAccessibilityText
false
);

var bindableProperties = new[] { bindableProperty }.ToImmutableArray();
Expand Down Expand Up @@ -156,4 +161,56 @@ static Compilation CreateCompilation(string source)
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}

[Fact]
public void BindablePropertyModel_WithInitializer_GeneratesDefaultValueCreator()
{
// Arrange
var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } = \"Hello\"; }");
var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!;
var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType<IPropertySymbol>().First();

const string propertyName = "TestProperty";
const string defaultValue = "\"Hello\"";
const string defaultBindingMode = "Microsoft.Maui.Controls.BindingMode.TwoWay";
const string validateValueMethodName = "ValidateValue";
const string propertyChangedMethodName = "OnPropertyChanged";
const string propertyChangingMethodName = "OnPropertyChanging";
const string coerceValueMethodName = "CoerceValue";
const string defaultValueCreatorMethodName = "__createDefaultTestProperty";
const string newKeywordText = "new ";
const bool hasInitializer = true;

// Act
var model = new BindablePropertyModel(
propertyName,
propertySymbol.Type,
typeSymbol,
defaultValue,
defaultBindingMode,
validateValueMethodName,
propertyChangedMethodName,
propertyChangingMethodName,
coerceValueMethodName,
defaultValueCreatorMethodName,
newKeywordText,
true, // IsReadOnlyBindableProperty
string.Empty, // SetterAccessibility
hasInitializer);

// Assert
Assert.Equal(propertyName, model.PropertyName);
Assert.Equal(propertySymbol.Type, model.ReturnType);
Assert.Equal(typeSymbol, model.DeclaringType);
Assert.Equal(defaultValue, model.DefaultValue);
Assert.Equal(defaultBindingMode, model.DefaultBindingMode);
Assert.Equal(validateValueMethodName, model.ValidateValueMethodName);
Assert.Equal(propertyChangedMethodName, model.PropertyChangedMethodName);
Assert.Equal(propertyChangingMethodName, model.PropertyChangingMethodName);
Assert.Equal(coerceValueMethodName, model.CoerceValueMethodName);
Assert.Equal(defaultValueCreatorMethodName, model.DefaultValueCreatorMethodName);
Assert.Equal(newKeywordText, model.NewKeywordText);
Assert.Equal(hasInitializer, model.HasInitializer);
Assert.Equal("TestPropertyProperty", model.BindablePropertyName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ static string GenerateSource(SemanticValues value)
GenerateBindableProperty(sb, in info);
}

if (info.HasInitializer)
{
GenerateInitializingProperty(sb, in info);
GenerateDefaultValueMethod(sb, in info, classNameWithGenerics);
}

GenerateProperty(sb, in info);
}

Expand Down Expand Up @@ -236,7 +242,7 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper
.Append(", ")
.Append(info.CoerceValueMethodName)
.Append(", ")
.Append(info.DefaultValueCreatorMethodName)
.Append(GetDefaulteValueCreatorMethod(in info))
.Append(");\n");

// Generate public BindableProperty from the key
Expand Down Expand Up @@ -292,7 +298,7 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel
.Append(", ")
.Append(info.CoerceValueMethodName)
.Append(", ")
.Append(info.DefaultValueCreatorMethodName)
.Append(GetDefaulteValueCreatorMethod(in info))
.Append(");\n");

sb.Append('\n');
Expand All @@ -310,18 +316,42 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info)
.Append(formattedReturnType)
.Append(' ')
.Append(sanitizedPropertyName)
.Append("\n{\nget => (")
.Append(formattedReturnType)
.Append(")GetValue(")
.Append(info.BindablePropertyName)
.Append(");\n");
.Append("\n{\nget => ");

if (info.HasInitializer)
{
sb.Append("__initializing")
.Append(info.PropertyName)
.Append(" ? field : ")
.Append("(")
.Append(formattedReturnType)
.Append(")GetValue(")
.Append(info.BindablePropertyName)
.Append(");\n");
}
else
{
sb.Append("(")
.Append(formattedReturnType)
.Append(")GetValue(")
.Append(info.BindablePropertyName)
.Append(");\n");
}

if (info.SetterAccessibility is not null)
{
sb.Append(info.SetterAccessibility)
.Append("set => SetValue(")
.Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName)
.Append(", value);\n");
.Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName);

if (info.HasInitializer)
{
sb.Append(", field = value);\n");
}
else
{
sb.Append(", value);\n");
}
}
// else Do not create a Setter because the property is read-only

Expand All @@ -333,6 +363,7 @@ static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context,
var propertyDeclarationSyntax = Unsafe.As<PropertyDeclarationSyntax>(context.TargetNode);
var semanticModel = context.SemanticModel;
var propertySymbol = (IPropertySymbol?)ModelExtensions.GetDeclaredSymbol(semanticModel, propertyDeclarationSyntax, cancellationToken);
var hasInitializer = propertyDeclarationSyntax.Initializer is not null;

if (propertySymbol is null)
{
Expand All @@ -359,7 +390,7 @@ static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context,
var (isReadOnlyBindableProperty, setterAccessibility) = GetPropertyAccessibility(propertySymbol, propertyDeclarationSyntax);

var attributeData = context.Attributes[0];
bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility);
bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility, hasInitializer);

return new(propertyInfo, ImmutableArray.Create(bindablePropertyModels));
}
Expand Down Expand Up @@ -456,7 +487,7 @@ static string GetGenericTypeParameters(INamedTypeSymbol typeSymbol)
return sb.ToString();
}

static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility)
static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility, in bool hasInitializer)
{
if (attributeData.AttributeClass is null)
{
Expand All @@ -472,7 +503,7 @@ static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attrib
var validateValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));
var newKeywordText = doesContainNewKeyword ? "new " : string.Empty;

return new BindablePropertyModel(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility);
return new BindablePropertyModel(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility, hasInitializer);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down Expand Up @@ -529,4 +560,72 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol)
return typeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
}
}

/// <summary>
/// Determines the name of the method used to create the default value for the specified bindable property model.
/// </summary>
/// <param name="info">The bindable property model for which to retrieve the default value creator method name.</param>
/// <returns>A string containing the name of the method that creates the default value for the property.
/// If the property has an initializer, the returned name is of the generated default value method;
/// otherwise, the default value creator method name from the model is returned.</returns>
static string GetDefaulteValueCreatorMethod(in BindablePropertyModel info)
{
if (info.HasInitializer)
{
return "__createDefault" + info.PropertyName;
}

return info.DefaultValueCreatorMethodName;
}

/// <summary>
/// Generates the boolean initialization flag used by bindable properties with initializers to indicate that the getter
/// should return the backing field while the generated default value method is executing.
/// </summary>
/// <param name="sb">The StringBuilder instance to which the initialization field declaration will be appended.</param>
/// <param name="info">The model containing metadata about the property for which the initialization field is generated.</param>
static void GenerateInitializingProperty(StringBuilder sb, in BindablePropertyModel info)
{
sb.Append("bool ")
.Append("__initializing")
.Append(info.PropertyName)
.Append(" = false;\n");
}

/// <summary>
/// Generates the default value creator static method used by BindableProperty instances with initializers.
/// This method temporarily switches the property's getter to return its backing field while the default value is being computed,
/// ensuring that the initializer-provided value is captured and returned as the BindableProperty's default.
/// </summary>
/// <param name="sb">The StringBuilder instance to which the initialization field declaration will be appended.</param>
/// <param name="info">The model containing metadata for the property that requires a default value creator.</param>
/// <param name="classNameWithGenerics">The declaring class name including generic type parameters, if any.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyModel info, string classNameWithGenerics)
{
var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;

sb.Append("static object ")
.Append(GetDefaulteValueCreatorMethod(in info))
.Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n")
.Append("{\n")
.Append("((")
.Append(classNameWithGenerics)
.Append(")bindable).__initializing")
.Append(info.PropertyName)
.Append(" = true;\n")
.Append("var defaultValue = ")
.Append("((")
.Append(classNameWithGenerics)
.Append(")bindable).")
.Append(sanitizedPropertyName)
.Append(";\n")
.Append("((")
.Append(classNameWithGenerics)
.Append(")bindable).__initializing")
.Append(info.PropertyName)
.Append(" = false;\n")
.Append("return defaultValue;\n")
.Append("}\n");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace CommunityToolkit.Maui.SourceGenerators.Internal.Models;

record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultValue, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText, bool IsReadOnlyBindableProperty, string? SetterAccessibility)
record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultValue, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText, bool IsReadOnlyBindableProperty, string? SetterAccessibility, bool HasInitializer)
{
public string BindablePropertyName => $"{PropertyName}Property";
public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey";
Expand Down
Loading
Loading