Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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,55 @@ 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;

namespace {{defaultTestNamespace}};

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

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;
public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); }

static object __createDefaultText(Microsoft.Maui.Controls.BindableObject bindable)
{
(({{defaultTestClassName}})bindable).__initializingText = true;
var defaultValue = (({{defaultTestClassName}})bindable).Text;
(({{defaultTestClassName}})bindable).__initializingText = false;
return defaultValue;
}
}
""";

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 @@ -184,6 +184,11 @@ static string GenerateSource(SemanticValues value)
}

GenerateProperty(sb, in info);

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

sb.Append('}');
Expand Down Expand Up @@ -292,7 +297,7 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel
.Append(", ")
.Append(info.CoerceValueMethodName)
.Append(", ")
.Append(info.DefaultValueCreatorMethodName)
.Append(info.HasInitializer ? "__createDefault" + info.PropertyName : info.DefaultValueCreatorMethodName)
.Append(");\n");

sb.Append('\n');
Expand All @@ -304,24 +309,56 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info)
var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;
var formattedReturnType = GetFormattedReturnType(info.ReturnType);

if (info.HasInitializer)
{
sb.Append("bool ")
.Append("__initializing")
.Append(info.PropertyName)
.Append(" = false;\n");
}

sb.Append("public ")
.Append(info.NewKeywordText)
.Append("partial ")
.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 +370,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 +397,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 +494,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 +510,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 +567,33 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol)
return typeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
}
}

[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 __createDefault")
.Append(info.PropertyName)
.Append("(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