-
Notifications
You must be signed in to change notification settings - Fork 468
Extend BindableProperty source gen to handle partial property initializers #2987
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extend BindableProperty source gen to handle partial property initializers #2987
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR enhances the BindableProperty source generator to recognize and respect C# property initializers, eliminating the need for attribute-based default values and separate default-value creator methods. Properties with initializers now have their defaults captured safely via generated guard flags and creator methods, while RatingView demonstrates the cleaner pattern by replacing attribute defaults with inline initializers.
Key Changes
- Source generator now detects property initializers and generates guard flags (
__initializing<Property>) and default-value creator methods (__createDefault<Property>) - Generated properties synchronize the backing field with BindableProperty values in both getter and setter
RatingViewmodernized to use property initializers (= RatingViewDefaults.X) instead of attribute parameters, removing boilerplate default-creator methods
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
RatingView.shared.cs |
Migrates 13 properties from attribute defaults to initializers; removes 4 helper methods; makes CustomShapePath nullable |
Records.cs |
Adds HasInitializer boolean parameter to BindablePropertyModel record |
BindablePropertyAttributeSourceGenerator.cs |
Implements initializer detection, guard-flag generation, and conditional default-creator wiring |
BindablePropertyModelTests.cs |
Adds test case for models with initializers and updates existing tests with new parameter |
IntegrationTests.cs |
Updates all expected code to include guard flags and field synchronization |
EdgeCaseTests.cs |
Updates expected output for keywords, enums, nullables, arrays, namespaces, and literals |
CommonUsageTests.cs |
Updates expectations for basic usage, inheritance, callbacks, accessibility, and adds new initializer test |
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
...erators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
|
@TheCodeTraveler, looks like I tried to update the CommonUsageTests.cs at the same time as you. I tried to push a last |
|
This is awesome!! This the perfect use case for the We can use the For example: file bool __initializingText = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
|
@TheCodeTraveler I tried the |
…nly BindableProperty. Extend unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
...oolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs
Show resolved
Hide resolved
...erators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs
Outdated
Show resolved
Hide resolved
...erators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs
Outdated
Show resolved
Hide resolved
…tDefaultValueCreatorMethodName method)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
…faultValueCreatorMethodName for clarity
…eratesCorrectCode
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Stephen!
FYI - I made two updates to the source generator. Let me know what you think of the implementation!
1. Handle the scenario when both a property initializer and a DefaultValueCreatorMethodName are provided
In this scenario, we will ignore the partial property initializer, instead using DefaultValueCreatorMethod. This mimic's how .NET MAUI works today when creating a BindableProperty: .NET initializes the property when the class is initialized using the provided partial property initializer, then .NET MAUI overrides it, setting the BindableProperty default value using DefaultValueCreator
BindableProperty With Both Initializer and DefaultValueCreatorMethod
public partial class TestView : View
{
[BindableProperty(DefaultValueCreatorMethodName = nameof(CreateDefaultText))]
public partial string Text { get; set; } = "Initial Value";
static string CreateDefaultText(BindableObject bindable)
{
return "Initial Value";
}
}Generated Code
// <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);
public partial string Text { get => false ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
}2. Move Out-of-Scope Generated Types to file static class
I apologize for misunderstanding the file keyword earlier! I didn't realize the keyword could only be used on a class.
I updated the source generator to write bool __initializing* and object CreateDefault* in a file static class so that they are not accessible or discoverable by developers and that no developer accidentally references or changes the values to these.
BindableProperty Using Initializer
// <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, __BindablePropertyInitHelpers.CreateDefaultText);
public partial string Text { get => __BindablePropertyInitHelpers.IsInitializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, 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, __BindablePropertyInitHelpers.CreateDefaultCustomDuration);
public partial System.TimeSpan CustomDuration { get => __BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); }
}Generated Code
public partial class TestName
{
/// <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, __BindablePropertyInitHelpers.CreateDefaultText);
public partial string Text { get => __BindablePropertyInitHelpers.IsInitializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, 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, __BindablePropertyInitHelpers.CreateDefaultCustomDuration);
public partial System.TimeSpan CustomDuration { get => __BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); }
}
file static class __BindablePropertyInitHelpers
{
public static bool IsInitializingText = false;
public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable)
{
IsInitializingText = true;
var defaultValue = ((TestView)bindable).Text;
IsInitializingText = false;
return defaultValue;
}
public static bool IsInitializingCustomDuration = false;
public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable)
{
IsInitializingCustomDuration = true;
var defaultValue = ((TestView)bindable).CustomDuration;
IsInitializingCustomDuration = false;
return defaultValue;
}
}
bijington
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a couple of thoughts from me
| public partial class {{defaultTestClassName}} : View | ||
| { | ||
| [BindablePropertyAttribute(DefaultValueCreatorMethodName = nameof(CreateDefaultText))] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we report a warning here? It feels like it could be nice to warn the developer that their initial value will be ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup! Before we promote [BindableProperty] to stable, we'll create a slew of Analyzers, similar to the CommunityToolkit.Mvvm Analyzers to help make using these dummy-proof. It'll include things this, generating a compiler error when missing the partial modifier, generating a compiler error when the provided method signatures are incorrect, etc.
My plan is to start working on the initializers in the new year after we've publish the first preview release of [BindableProperty].
src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs
Outdated
Show resolved
Hide resolved
src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs
Outdated
Show resolved
Hide resolved
ne0rrmatrix
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. I hope we can merge this soon. Just one question I do have. Is there any tests we can run or create that will validate that the generators will or will not generate invalid code that is not caught by the compiler? Has anyone even tested the idea of deliberately writing code that tests this out? Maybe irrelevant but I just thought this might poke an idea from someone about any future issues?
fc82b82
|
@TheCodeTraveler, thanks for the improvements. It makes perfect sense. It makes the implementation way better! |
|
A quick question about One suggestion would be to mangle the class name to include the owning type, for example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
Good question! Since you had already implemented partial property initializers in That said, it still feels kinda icky to have multiple classes in multiple generated files use the same name. Plus, having unique class names will make analyzing stack traces and debugging easier for future us. So, I still moved forward with your suggestion, updating the Good call! Thanks for the suggestion 🙌 |
Description of Change
This PR enhances the BindableProperty source generator to be initializer-aware and modernizes RatingView to use property initializers instead of attribute-provided defaults. It also aligns unit tests and expected codegen outputs with the new behavior.
Linked Issues
PR Checklist
approved(bug) orChampioned(feature/proposal)mainat time of PRAdditional information
Key Outcomes
✅ Initializer-Aware Codegen
When a partial property has an initializer (e.g.,
public partial string Text { get; set; } = "Initial Value";), the generator:bool __initializing<Property>to allow access to the partial property initializer__createDefault<Property>(BindableObject)that toggles the guard, reads the initializer-backed value, and returns it safely.BindablePropertygetter to return the partial property initializer when requested, otherwise it returns the standard BindableProperty valueget => __initializing<Property> ? field : (T)GetValue(PropertyProperty);__createDefault<Property>intoBindableProperty.Create(...)when an initializer exists; falls back to attributeDefaultValueCreatorMethodNameotherwise.✅ Cleaner Control Code in
RatingViewDefaults now come from property initializers (via
RatingViewDefaults) rather than attributes:CreateDefaultFillColor,CreateDefaultShapePadding).CustomShapePathclarified as nullable (string?).✅ Test & Model Alignment
BindablePropertyModelextended withHasInitializerso the generator reliably selects the initializer path.Benefits