From 57877ecb5f2f50a649432dc85b0b92a65d1afd44 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Fri, 5 Dec 2025 18:34:32 +1100 Subject: [PATCH 01/24] Extend BindableProperty source gen to handle partial property initializers --- .../CommonUsageTests.cs | 99 ++++++++++++++++--- .../EdgeCaseTests.cs | 51 ++++++---- .../IntegrationTests.cs | 38 ++++--- .../BindablePropertyModelTests.cs | 63 +++++++++++- ...indablePropertyAttributeSourceGenerator.cs | 56 +++++++++-- .../Models/Records.cs | 2 +- .../Views/RatingView/RatingView.shared.cs | 62 +++++------- 7 files changed, 277 insertions(+), 94 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index d12688ef2f..2aa61de9b3 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -38,7 +38,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// 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, null); - public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } """; @@ -79,7 +80,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (string)"Hello", Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } """; @@ -120,7 +122,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public new 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, null); - public new partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public new partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } """; @@ -161,7 +164,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// 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, null); - public partial string? Text { get => (string? )GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string? Text { get => __initializingText ? field : (string? )GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } """; @@ -205,13 +209,15 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// 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, null); - public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Number", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial int Number { get => (int)GetValue(NumberProperty); set => SetValue(NumberProperty, value); } + bool __initializingNumber = false; + public partial int Number { get => __initializingNumber ? field : (int)GetValue(NumberProperty); set => SetValue(NumberProperty, field = value); } } """; @@ -265,7 +271,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (int)42, (Microsoft.Maui.Controls.BindingMode)1, ValidateValue, OnPropertyChanged, OnPropertyChanging, CoerceValue, CreateDefaultValue); - public partial int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } + bool __initializingValue = false; + public partial int Value { get => __initializingValue ? field : (int)GetValue(ValueProperty); set => SetValue(ValueProperty, field = value); } } """; @@ -306,7 +313,8 @@ internal partial class TestView /// Backing BindableProperty for the property. /// 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, null); - public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } """; @@ -386,7 +394,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Text { get => (string)GetValue(TextProperty); internal set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); internal set => SetValue(TextProperty, field = value); } } """; @@ -428,7 +437,8 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - public partial string Text { get => (string)GetValue(TextProperty); private protected set => SetValue(textPropertyKey, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); private protected set => SetValue(textPropertyKey, field = value); } } """; @@ -469,7 +479,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Text { get => (string)GetValue(TextProperty); protected internal set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); protected internal set => SetValue(TextProperty, field = value); } } """; @@ -511,7 +522,8 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - public partial string Text { get => (string)GetValue(TextProperty); protected set => SetValue(textPropertyKey, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); protected set => SetValue(textPropertyKey, field = value); } } """; @@ -553,7 +565,8 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - public partial string Text { get => (string)GetValue(TextProperty); private set => SetValue(textPropertyKey, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); private set => SetValue(textPropertyKey, field = value); } } """; @@ -595,7 +608,8 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - public partial string Text { get => (string)GetValue(TextProperty); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); } } """; @@ -640,16 +654,69 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty PositionProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Position", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.TimeSpan.Zero, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial System.TimeSpan Position { get => (System.TimeSpan)GetValue(PositionProperty); set => SetValue(PositionProperty, value); } + bool __initializingPosition = false; + public partial System.TimeSpan Position { get => __initializingPosition ? field : (System.TimeSpan)GetValue(PositionProperty); set => SetValue(PositionProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), new global::System.TimeSpan(900000000), Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial System.TimeSpan CustomDuration { get => (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } + bool __initializingCustomDuration = false; + public partial System.TimeSpan CustomDuration { get => __initializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, field = value); } } """; 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 + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + #pragma warning disable + #nullable enable + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), 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) + { + ((TestView)bindable).__initializingText = true; + var defaultValue = ((TestView)bindable).Text; + ((TestView)bindable).__initializingText = false; + return defaultValue; + } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs index 8de524928e..f29b3ff205 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs @@ -41,13 +41,15 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty classProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("@class", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string @class { get => (string)GetValue(classProperty); set => SetValue(classProperty, value); } + bool __initializingclass = false; + public partial string @class { get => __initializingclass ? field : (string)GetValue(classProperty); set => SetValue(classProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty namespaceProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("@namespace", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string @namespace { get => (string)GetValue(namespaceProperty); set => SetValue(namespaceProperty, value); } + bool __initializingnamespace = false; + public partial string @namespace { get => __initializingnamespace ? field : (string)GetValue(namespaceProperty); set => SetValue(namespaceProperty, field = value); } } """; @@ -95,7 +97,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (TestNamespace.Status)1, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial TestNamespace.Status InvoiceStatus { get => (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } + bool __initializingInvoiceStatus = false; + public partial TestNamespace.Status InvoiceStatus { get => __initializingInvoiceStatus ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, field = value); } } """; @@ -143,7 +146,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (TestNamespace.Status)9223372036854775807, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial TestNamespace.Status InvoiceStatus { get => (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } + bool __initializingInvoiceStatus = false; + public partial TestNamespace.Status InvoiceStatus { get => __initializingInvoiceStatus ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, field = value); } } """; @@ -191,19 +195,22 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableIntProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableInt", typeof(int? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial int? NullableInt { get => (int? )GetValue(NullableIntProperty); set => SetValue(NullableIntProperty, value); } + bool __initializingNullableInt = false; + public partial int? NullableInt { get => __initializingNullableInt ? field : (int? )GetValue(NullableIntProperty); set => SetValue(NullableIntProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableDateTimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableDateTime", typeof(System.DateTime? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial System.DateTime? NullableDateTime { get => (System.DateTime? )GetValue(NullableDateTimeProperty); set => SetValue(NullableDateTimeProperty, value); } + bool __initializingNullableDateTime = false; + public partial System.DateTime? NullableDateTime { get => __initializingNullableDateTime ? field : (System.DateTime? )GetValue(NullableDateTimeProperty); set => SetValue(NullableDateTimeProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableBoolProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableBool", typeof(bool? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial bool? NullableBool { get => (bool? )GetValue(NullableBoolProperty); set => SetValue(NullableBoolProperty, value); } + bool __initializingNullableBool = false; + public partial bool? NullableBool { get => __initializingNullableBool ? field : (bool? )GetValue(NullableBoolProperty); set => SetValue(NullableBoolProperty, field = value); } } """; @@ -250,19 +257,22 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty StringArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("StringArray", typeof(string[]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string[] StringArray { get => (string[])GetValue(StringArrayProperty); set => SetValue(StringArrayProperty, value); } + bool __initializingStringArray = false; + public partial string[] StringArray { get => __initializingStringArray ? field : (string[])GetValue(StringArrayProperty); set => SetValue(StringArrayProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty MultiDimensionalArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("MultiDimensionalArray", typeof(int[, ]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial int[, ] MultiDimensionalArray { get => (int[, ])GetValue(MultiDimensionalArrayProperty); set => SetValue(MultiDimensionalArrayProperty, value); } + bool __initializingMultiDimensionalArray = false; + public partial int[, ] MultiDimensionalArray { get => __initializingMultiDimensionalArray ? field : (int[, ])GetValue(MultiDimensionalArrayProperty); set => SetValue(MultiDimensionalArrayProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty JaggedArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("JaggedArray", typeof(byte[][]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial byte[][] JaggedArray { get => (byte[][])GetValue(JaggedArrayProperty); set => SetValue(JaggedArrayProperty, value); } + bool __initializingJaggedArray = false; + public partial byte[][] JaggedArray { get => __initializingJaggedArray ? field : (byte[][])GetValue(JaggedArrayProperty); set => SetValue(JaggedArrayProperty, field = value); } } """; @@ -303,7 +313,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(Very.Long.Namespace.With.Many.Segments.TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } """; @@ -341,7 +352,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } """; @@ -385,13 +397,15 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty Property_With_UnderscoresProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Property_With_Underscores", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Property_With_Underscores { get => (string)GetValue(Property_With_UnderscoresProperty); set => SetValue(Property_With_UnderscoresProperty, value); } + bool __initializingProperty_With_Underscores = false; + public partial string Property_With_Underscores { get => __initializingProperty_With_Underscores ? field : (string)GetValue(Property_With_UnderscoresProperty); set => SetValue(Property_With_UnderscoresProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty Property123WithNumbersProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Property123WithNumbers", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Property123WithNumbers { get => (string)GetValue(Property123WithNumbersProperty); set => SetValue(Property123WithNumbersProperty, value); } + bool __initializingProperty123WithNumbers = false; + public partial string Property123WithNumbers { get => __initializingProperty123WithNumbers ? field : (string)GetValue(Property123WithNumbersProperty); set => SetValue(Property123WithNumbersProperty, field = value); } } """; @@ -438,19 +452,22 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty IsEnabledProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("IsEnabled", typeof(bool), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (bool)true, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial bool IsEnabled { get => (bool)GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, value); } + bool __initializingIsEnabled = false; + public partial bool IsEnabled { get => __initializingIsEnabled ? field : (bool)GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty PiProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Pi", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (double)3.14, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial double Pi { get => (double)GetValue(PiProperty); set => SetValue(PiProperty, value); } + bool __initializingPi = false; + public partial double Pi { get => __initializingPi ? field : (double)GetValue(PiProperty); set => SetValue(PiProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty LetterProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Letter", typeof(char), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (char)'A', Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial char Letter { get => (char)GetValue(LetterProperty); set => SetValue(LetterProperty, value); } + bool __initializingLetter = false; + public partial char Letter { get => __initializingLetter ? field : (char)GetValue(LetterProperty); set => SetValue(LetterProperty, field = value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs index 3df8bd95cc..4e2535e4d6 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs @@ -1,8 +1,3 @@ -using CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Testing; using Xunit; namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.BindablePropertyAttributeSourceGeneratorTests; @@ -52,7 +47,8 @@ public partial class BaseView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty BaseTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("BaseText", typeof(string), typeof(TestNamespace.BaseView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string BaseText { get => (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, value); } + bool __initializingBaseText = false; + public partial string BaseText { get => __initializingBaseText ? field : (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, field = value); } } """; @@ -71,13 +67,15 @@ public partial class DerivedView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty DerivedTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("DerivedText", typeof(string), typeof(TestNamespace.DerivedView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string DerivedText { get => (string)GetValue(DerivedTextProperty); set => SetValue(DerivedTextProperty, value); } + bool __initializingDerivedText = false; + public partial string DerivedText { get => __initializingDerivedText ? field : (string)GetValue(DerivedTextProperty); set => SetValue(DerivedTextProperty, field = value); } /// /// Backing BindableProperty for the property. /// public new static readonly global::Microsoft.Maui.Controls.BindableProperty BaseTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("BaseText", typeof(string), typeof(TestNamespace.DerivedView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public new partial string BaseText { get => (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, value); } + bool __initializingBaseText = false; + public new partial string BaseText { get => __initializingBaseText ? field : (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, field = value); } } """; @@ -121,13 +119,15 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial T? Value { get => (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); } + bool __initializingValue = false; + public partial T? Value { get => __initializingValue ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NameProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Name", typeof(U), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial U? Name { get => (U? )GetValue(NameProperty); set => SetValue(NameProperty, value); } + bool __initializingName = false; + public partial U? Name { get => __initializingName ? field : (U? )GetValue(NameProperty); set => SetValue(NameProperty, field = value); } } """; @@ -175,7 +175,8 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{outerClassName}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + bool __initializingText = false; + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } } } """; @@ -229,19 +230,22 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ModelProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Model", typeof(TestNamespace.CustomModel), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial {{defaultTestNamespace}}.CustomModel Model { get => ({{defaultTestNamespace}}.CustomModel)GetValue(ModelProperty); set => SetValue(ModelProperty, value); } + bool __initializingModel = false; + public partial TestNamespace.CustomModel Model { get => __initializingModel ? field : (TestNamespace.CustomModel)GetValue(ModelProperty); set => SetValue(ModelProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ItemsProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Items", typeof(System.Collections.Generic.List), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial System.Collections.Generic.List Items { get => (System.Collections.Generic.List)GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); } + bool __initializingItems = false; + public partial System.Collections.Generic.List Items { get => __initializingItems ? field : (System.Collections.Generic.List)GetValue(ItemsProperty); set => SetValue(ItemsProperty, field = value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty PropertiesProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Properties", typeof(System.Collections.Generic.Dictionary), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial System.Collections.Generic.Dictionary Properties { get => (System.Collections.Generic.Dictionary)GetValue(PropertiesProperty); set => SetValue(PropertiesProperty, value); } + bool __initializingProperties = false; + public partial System.Collections.Generic.Dictionary Properties { get => __initializingProperties ? field : (System.Collections.Generic.Dictionary)GetValue(PropertiesProperty); set => SetValue(PropertiesProperty, field = value); } } """; @@ -288,7 +292,8 @@ public partial class FirstView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty FirstTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("FirstText", typeof(string), typeof(TestNamespace.FirstView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string FirstText { get => (string)GetValue(FirstTextProperty); set => SetValue(FirstTextProperty, value); } + bool __initializingFirstText = false; + public partial string FirstText { get => __initializingFirstText ? field : (string)GetValue(FirstTextProperty); set => SetValue(FirstTextProperty, field = value); } } """; @@ -307,7 +312,8 @@ public partial class SecondView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty SecondTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("SecondText", typeof(string), typeof(TestNamespace.SecondView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - public partial string SecondText { get => (string)GetValue(SecondTextProperty); set => SetValue(SecondTextProperty, value); } + bool __initializingSecondText = false; + public partial string SecondText { get => __initializingSecondText ? field : (string)GetValue(SecondTextProperty); set => SetValue(SecondTextProperty, field = value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 7738db8a20..321bfcc9e5 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -29,7 +29,8 @@ public void BindablePropertyName_ReturnsCorrectPropertyName() "null", string.Empty, true, // IsReadOnlyBindableProperty - string.Empty // SetterAccessibility + string.Empty, // SetterAccessibility + false ); // Act @@ -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( @@ -71,7 +73,8 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues() defaultValueCreatorMethodName, newKeywordText, true, // IsReadOnlyBindableProperty - string.Empty // SetterAccessibility + string.Empty, // SetterAccessibility + hasInitializer ); // Assert @@ -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); } @@ -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(); @@ -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().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); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 7688ca3ceb..3ac63d26c0 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -184,6 +184,11 @@ static string GenerateSource(SemanticValues value) } GenerateProperty(sb, in info); + + if (info.HasInitializer) + { + GenerateDefaultValueMethod(sb, in info, classNameWithGenerics); + } } sb.Append('}'); @@ -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'); @@ -304,13 +309,22 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; var formattedReturnType = GetFormattedReturnType(info.ReturnType); + 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("\n{\nget => ") + .Append("__initializing") + .Append(info.PropertyName) + .Append(" ? field : ") + .Append("(") .Append(formattedReturnType) .Append(")GetValue(") .Append(info.BindablePropertyName) @@ -321,7 +335,7 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) sb.Append(info.SetterAccessibility) .Append("set => SetValue(") .Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName) - .Append(", value);\n"); + .Append(", field = value);\n"); } // else Do not create a Setter because the property is read-only @@ -333,6 +347,7 @@ static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, var propertyDeclarationSyntax = Unsafe.As(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) { @@ -359,7 +374,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)); } @@ -456,7 +471,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) { @@ -472,7 +487,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)] @@ -529,4 +544,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"); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs index 704e1aea28..925afe0037 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -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"; diff --git a/src/CommunityToolkit.Maui/Views/RatingView/RatingView.shared.cs b/src/CommunityToolkit.Maui/Views/RatingView/RatingView.shared.cs index 8e677c2a18..f17dbc5fa1 100644 --- a/src/CommunityToolkit.Maui/Views/RatingView/RatingView.shared.cs +++ b/src/CommunityToolkit.Maui/Views/RatingView/RatingView.shared.cs @@ -38,21 +38,21 @@ public event EventHandler RatingChanged /// The path data should be provided in a format compatible with the rendering system, such as SVG path /// syntax. If the value is null or empty, no custom shape will be applied. [BindableProperty(PropertyChangedMethodName = nameof(OnCustomShapePathPropertyChanged))] - public partial string CustomShapePath { get; set; } + public partial string? CustomShapePath { get; set; } /// /// Gets or sets the padding applied to the shape's content area. /// - [BindableProperty(PropertyChangedMethodName = nameof(OnShapePaddingPropertyChanged), DefaultValueCreatorMethodName = nameof(CreateDefaultShapePadding))] - public partial Thickness ShapePadding { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnShapePaddingPropertyChanged))] + public partial Thickness ShapePadding { get; set; } = RatingViewDefaults.ShapePadding; /// /// Gets or sets the shape used to display each rating item in the view. /// /// Changing this property updates the visual appearance of the rating items. The available shapes are /// defined by the enumeration. - [BindableProperty(DefaultValue = RatingViewDefaults.Shape, PropertyChangedMethodName = nameof(OnShapePropertyChanged))] - public partial RatingViewShape Shape { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnShapePropertyChanged))] + public partial RatingViewShape Shape { get; set; } = RatingViewDefaults.Shape; /// /// Gets or sets the color used to draw the border of the shape. @@ -61,22 +61,22 @@ public event EventHandler RatingChanged /// This uses a non-nullable . A value will be converted to /// [AllowNull] - [BindableProperty(PropertyChangedMethodName = nameof(OnShapeBorderColorChanged), CoerceValueMethodName = nameof(CoerceColorToTransparent), DefaultValueCreatorMethodName = nameof(CreateDefaultShapeBorderColor))] - public partial Color ShapeBorderColor { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnShapeBorderColorChanged), CoerceValueMethodName = nameof(CoerceColorToTransparent))] + public partial Color ShapeBorderColor { get; set; } = RatingViewDefaults.ShapeBorderColor; /// /// Gets or sets the thickness of the border applied to the shape, in device-independent units. /// /// A value of 0 indicates that no border will be rendered. Negative values are not supported and may /// result in undefined behavior. - [BindableProperty(DefaultValue = RatingViewDefaults.ShapeBorderThickness, PropertyChangedMethodName = nameof(OnShapeBorderThicknessChanged), PropertyChangingMethodName = nameof(OnShapeBorderThicknessChanging))] - public partial double ShapeBorderThickness { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnShapeBorderThicknessChanged), PropertyChangingMethodName = nameof(OnShapeBorderThicknessChanging))] + public partial double ShapeBorderThickness { get; set; } = RatingViewDefaults.ShapeBorderThickness; /// /// Gets or sets the diameter of the shape, in device-independent units. /// - [BindableProperty(DefaultValue = RatingViewDefaults.ItemShapeSize, PropertyChangedMethodName = nameof(OnShapeDiameterSizeChanged))] - public partial double ShapeDiameter { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnShapeDiameterSizeChanged))] + public partial double ShapeDiameter { get; set; } = RatingViewDefaults.ItemShapeSize; /// /// Gets or sets the color used to display empty rating shapes. @@ -85,8 +85,8 @@ public event EventHandler RatingChanged /// This uses a non-nullable . A value will be converted to /// [AllowNull] - [BindableProperty(PropertyChangedMethodName = nameof(OnRatingColorChanged), CoerceValueMethodName = nameof(CoerceColorToTransparent), DefaultValueCreatorMethodName = nameof(CreateDefaultEmptyShapeColor))] - public partial Color EmptyShapeColor { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnRatingColorChanged), CoerceValueMethodName = nameof(CoerceColorToTransparent))] + public partial Color EmptyShapeColor { get; set; } = RatingViewDefaults.EmptyShapeColor; /// /// Gets or sets the color used to fill the rating indicator. @@ -95,22 +95,22 @@ public event EventHandler RatingChanged /// This uses a non-nullable . A value will be converted to /// [AllowNull] - [BindableProperty(PropertyChangedMethodName = nameof(OnRatingColorChanged), CoerceValueMethodName = nameof(CoerceColorToTransparent), DefaultValueCreatorMethodName = nameof(CreateDefaultFillColor))] - public partial Color FillColor { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnRatingColorChanged), CoerceValueMethodName = nameof(CoerceColorToTransparent))] + public partial Color FillColor { get; set; } = RatingViewDefaults.FillColor; /// /// Gets or sets a value indicating whether the control is in a read-only state. /// - [BindableProperty(DefaultValue = RatingViewDefaults.IsReadOnly, PropertyChangedMethodName = nameof(OnIsReadOnlyChanged))] - public partial bool IsReadOnly { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnIsReadOnlyChanged))] + public partial bool IsReadOnly { get; set; } = RatingViewDefaults.IsReadOnly; /// /// Gets or sets the maximum rating value that can be assigned. /// /// The maximum rating determines the upper limit for rating inputs. Changing this value may affect /// validation and display of rating controls. The value must be a positive integer. - [BindableProperty(DefaultValue = RatingViewDefaults.MaximumRating, PropertyChangedMethodName = nameof(OnMaximumRatingChanged), PropertyChangingMethodName = nameof(OnMaximumRatingChanging))] - public partial int MaximumRating { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnMaximumRatingChanged), PropertyChangingMethodName = nameof(OnMaximumRatingChanging))] + public partial int MaximumRating { get; set; } = RatingViewDefaults.MaximumRating; /// /// Gets or sets the fill behavior to apply when the rating view is tapped. @@ -118,28 +118,28 @@ public event EventHandler RatingChanged /// Use this property to control how the rating view visually responds to user interaction. The /// selected fill option determines the appearance of the rating elements when tapped. [Obsolete($"Use {nameof(FillOption)} instead")] - [BindableProperty(DefaultValue = RatingViewDefaults.FillWhenTapped, PropertyChangedMethodName = nameof(OnRatingColorChanged))] - public partial RatingViewFillOption FillWhenTapped { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnRatingColorChanged))] + public partial RatingViewFillOption FillWhenTapped { get; set; } = RatingViewDefaults.FillWhenTapped; /// /// Gets or sets the rating value for the item. /// /// The rating must be a valid value as determined by the associated validation method. Changing the /// rating triggers the property changed callback, which may update related UI or logic. - [BindableProperty(DefaultValue = RatingViewDefaults.Rating, PropertyChangedMethodName = nameof(OnRatingChanged), PropertyChangingMethodName = nameof(OnRatingChanging))] - public partial double Rating { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnRatingChanged), PropertyChangingMethodName = nameof(OnRatingChanging))] + public partial double Rating { get; set; } = RatingViewDefaults.Rating; /// /// Gets or sets the amount of space, in device-independent units, between adjacent items. /// - [BindableProperty(DefaultValue = RatingViewDefaults.Spacing, PropertyChangedMethodName = nameof(OnSpacingChanged))] - public partial double Spacing { get; set; } + [BindableProperty(PropertyChangedMethodName = nameof(OnSpacingChanged))] + public partial double Spacing { get; set; } = RatingViewDefaults.Spacing; /// Gets or sets the element to fill when a is set. /// Use this property to control how the rating view visually responds to user interaction. /// The selected fill option determines the appearance of the rating elements when tapped. - [BindableProperty(DefaultValue = RatingViewDefaults.FillOption, PropertyChangingMethodName = nameof(OnRatingColorChanged))] - public partial RatingViewFillOption FillOption { get; set; } + [BindableProperty(PropertyChangingMethodName = nameof(OnRatingColorChanged))] + public partial RatingViewFillOption FillOption { get; set; } = RatingViewDefaults.FillOption; internal HorizontalStackLayout RatingLayout { get; } = []; @@ -166,14 +166,6 @@ public event EventHandler RatingChanged } }; - static object CreateDefaultShapeBorderColor(BindableObject bindable) => RatingViewDefaults.ShapeBorderColor; - - static object CreateDefaultEmptyShapeColor(BindableObject bindable) => RatingViewDefaults.EmptyShapeColor; - - static object CreateDefaultFillColor(BindableObject bindable) => RatingViewDefaults.FillColor; - - static object CreateDefaultShapePadding(BindableObject bindable) => RatingViewDefaults.ShapePadding; - static object CoerceColorToTransparent(BindableObject bindable, object value) { var colorValue = (Color?)value; From a9085ef9e850cb69509034c461ecea7d4dbb1830 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Thu, 4 Dec 2025 23:59:44 -0800 Subject: [PATCH 02/24] Update CommonUsageTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../CommonUsageTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index 2aa61de9b3..ed49d4f7ce 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -702,15 +702,15 @@ public partial class {{defaultTestClassName}} /// /// Backing BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __createDefaultText); + 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) { - ((TestView)bindable).__initializingText = true; - var defaultValue = ((TestView)bindable).Text; - ((TestView)bindable).__initializingText = false; + (({{defaultTestClassName}})bindable).__initializingText = true; + var defaultValue = (({{defaultTestClassName}})bindable).Text; + (({{defaultTestClassName}})bindable).__initializingText = false; return defaultValue; } } From fba8b2561bfb1177fb15a7a0987be5b917179b86 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Fri, 5 Dec 2025 19:00:22 +1100 Subject: [PATCH 03/24] Only implement new initializer code for when initializers are present --- .../CommonUsageTests.cs | 50 ++++++----------- .../EdgeCaseTests.cs | 51 ++++++------------ .../IntegrationTests.cs | 33 ++++-------- ...indablePropertyAttributeSourceGenerator.cs | 53 +++++++++++++------ 4 files changed, 83 insertions(+), 104 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index 2aa61de9b3..6db589015b 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -38,8 +38,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// 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, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } } """; @@ -80,8 +79,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (string)"Hello", Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } } """; @@ -122,8 +120,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public new 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, null); - bool __initializingText = false; - public new partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public new partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } } """; @@ -164,8 +161,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// 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, null); - bool __initializingText = false; - public partial string? Text { get => __initializingText ? field : (string? )GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string? Text { get => (string? )GetValue(TextProperty); set => SetValue(TextProperty, value); } } """; @@ -209,15 +205,13 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// 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, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Number", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingNumber = false; - public partial int Number { get => __initializingNumber ? field : (int)GetValue(NumberProperty); set => SetValue(NumberProperty, field = value); } + public partial int Number { get => (int)GetValue(NumberProperty); set => SetValue(NumberProperty, value); } } """; @@ -271,8 +265,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (int)42, (Microsoft.Maui.Controls.BindingMode)1, ValidateValue, OnPropertyChanged, OnPropertyChanging, CoerceValue, CreateDefaultValue); - bool __initializingValue = false; - public partial int Value { get => __initializingValue ? field : (int)GetValue(ValueProperty); set => SetValue(ValueProperty, field = value); } + public partial int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } } """; @@ -313,8 +306,7 @@ internal partial class TestView /// Backing BindableProperty for the property. /// 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, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } } """; @@ -394,8 +386,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); internal set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); internal set => SetValue(TextProperty, value); } } """; @@ -437,8 +428,7 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); private protected set => SetValue(textPropertyKey, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); private protected set => SetValue(textPropertyKey, value); } } """; @@ -479,8 +469,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); protected internal set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); protected internal set => SetValue(TextProperty, value); } } """; @@ -522,8 +511,7 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); protected set => SetValue(textPropertyKey, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); protected set => SetValue(textPropertyKey, value); } } """; @@ -565,8 +553,7 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); private set => SetValue(textPropertyKey, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); private set => SetValue(textPropertyKey, value); } } """; @@ -608,8 +595,7 @@ public partial class {{defaultTestClassName}} /// static readonly global::Microsoft.Maui.Controls.BindablePropertyKey textPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = textPropertyKey.BindableProperty; - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); } + public partial string Text { get => (string)GetValue(TextProperty); } } """; @@ -654,21 +640,20 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty PositionProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Position", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.TimeSpan.Zero, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingPosition = false; - public partial System.TimeSpan Position { get => __initializingPosition ? field : (System.TimeSpan)GetValue(PositionProperty); set => SetValue(PositionProperty, field = value); } + public partial System.TimeSpan Position { get => (System.TimeSpan)GetValue(PositionProperty); set => SetValue(PositionProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), new global::System.TimeSpan(900000000), Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingCustomDuration = false; - public partial System.TimeSpan CustomDuration { get => __initializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, field = value); } + public partial System.TimeSpan CustomDuration { get => (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } } """; await VerifySourceGeneratorAsync(source, expectedGenerated); } + [Fact] public async Task GenerateBindableProperty_WithInitializers_GeneratesCorrectCode() { @@ -718,5 +703,4 @@ static object __createDefaultText(Microsoft.Maui.Controls.BindableObject bindabl await VerifySourceGeneratorAsync(source, expectedGenerated); } - } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs index f29b3ff205..8de524928e 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs @@ -41,15 +41,13 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty classProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("@class", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingclass = false; - public partial string @class { get => __initializingclass ? field : (string)GetValue(classProperty); set => SetValue(classProperty, field = value); } + public partial string @class { get => (string)GetValue(classProperty); set => SetValue(classProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty namespaceProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("@namespace", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingnamespace = false; - public partial string @namespace { get => __initializingnamespace ? field : (string)GetValue(namespaceProperty); set => SetValue(namespaceProperty, field = value); } + public partial string @namespace { get => (string)GetValue(namespaceProperty); set => SetValue(namespaceProperty, value); } } """; @@ -97,8 +95,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (TestNamespace.Status)1, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingInvoiceStatus = false; - public partial TestNamespace.Status InvoiceStatus { get => __initializingInvoiceStatus ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, field = value); } + public partial TestNamespace.Status InvoiceStatus { get => (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } } """; @@ -146,8 +143,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (TestNamespace.Status)9223372036854775807, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingInvoiceStatus = false; - public partial TestNamespace.Status InvoiceStatus { get => __initializingInvoiceStatus ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, field = value); } + public partial TestNamespace.Status InvoiceStatus { get => (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } } """; @@ -195,22 +191,19 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableIntProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableInt", typeof(int? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingNullableInt = false; - public partial int? NullableInt { get => __initializingNullableInt ? field : (int? )GetValue(NullableIntProperty); set => SetValue(NullableIntProperty, field = value); } + public partial int? NullableInt { get => (int? )GetValue(NullableIntProperty); set => SetValue(NullableIntProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableDateTimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableDateTime", typeof(System.DateTime? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingNullableDateTime = false; - public partial System.DateTime? NullableDateTime { get => __initializingNullableDateTime ? field : (System.DateTime? )GetValue(NullableDateTimeProperty); set => SetValue(NullableDateTimeProperty, field = value); } + public partial System.DateTime? NullableDateTime { get => (System.DateTime? )GetValue(NullableDateTimeProperty); set => SetValue(NullableDateTimeProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableBoolProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableBool", typeof(bool? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingNullableBool = false; - public partial bool? NullableBool { get => __initializingNullableBool ? field : (bool? )GetValue(NullableBoolProperty); set => SetValue(NullableBoolProperty, field = value); } + public partial bool? NullableBool { get => (bool? )GetValue(NullableBoolProperty); set => SetValue(NullableBoolProperty, value); } } """; @@ -257,22 +250,19 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty StringArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("StringArray", typeof(string[]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingStringArray = false; - public partial string[] StringArray { get => __initializingStringArray ? field : (string[])GetValue(StringArrayProperty); set => SetValue(StringArrayProperty, field = value); } + public partial string[] StringArray { get => (string[])GetValue(StringArrayProperty); set => SetValue(StringArrayProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty MultiDimensionalArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("MultiDimensionalArray", typeof(int[, ]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingMultiDimensionalArray = false; - public partial int[, ] MultiDimensionalArray { get => __initializingMultiDimensionalArray ? field : (int[, ])GetValue(MultiDimensionalArrayProperty); set => SetValue(MultiDimensionalArrayProperty, field = value); } + public partial int[, ] MultiDimensionalArray { get => (int[, ])GetValue(MultiDimensionalArrayProperty); set => SetValue(MultiDimensionalArrayProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty JaggedArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("JaggedArray", typeof(byte[][]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingJaggedArray = false; - public partial byte[][] JaggedArray { get => __initializingJaggedArray ? field : (byte[][])GetValue(JaggedArrayProperty); set => SetValue(JaggedArrayProperty, field = value); } + public partial byte[][] JaggedArray { get => (byte[][])GetValue(JaggedArrayProperty); set => SetValue(JaggedArrayProperty, value); } } """; @@ -313,8 +303,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(Very.Long.Namespace.With.Many.Segments.TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } } """; @@ -352,8 +341,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof(TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } } """; @@ -397,15 +385,13 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty Property_With_UnderscoresProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Property_With_Underscores", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingProperty_With_Underscores = false; - public partial string Property_With_Underscores { get => __initializingProperty_With_Underscores ? field : (string)GetValue(Property_With_UnderscoresProperty); set => SetValue(Property_With_UnderscoresProperty, field = value); } + public partial string Property_With_Underscores { get => (string)GetValue(Property_With_UnderscoresProperty); set => SetValue(Property_With_UnderscoresProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty Property123WithNumbersProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Property123WithNumbers", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingProperty123WithNumbers = false; - public partial string Property123WithNumbers { get => __initializingProperty123WithNumbers ? field : (string)GetValue(Property123WithNumbersProperty); set => SetValue(Property123WithNumbersProperty, field = value); } + public partial string Property123WithNumbers { get => (string)GetValue(Property123WithNumbersProperty); set => SetValue(Property123WithNumbersProperty, value); } } """; @@ -452,22 +438,19 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty IsEnabledProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("IsEnabled", typeof(bool), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (bool)true, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingIsEnabled = false; - public partial bool IsEnabled { get => __initializingIsEnabled ? field : (bool)GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, field = value); } + public partial bool IsEnabled { get => (bool)GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty PiProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Pi", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (double)3.14, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingPi = false; - public partial double Pi { get => __initializingPi ? field : (double)GetValue(PiProperty); set => SetValue(PiProperty, field = value); } + public partial double Pi { get => (double)GetValue(PiProperty); set => SetValue(PiProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty LetterProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Letter", typeof(char), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (char)'A', Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingLetter = false; - public partial char Letter { get => __initializingLetter ? field : (char)GetValue(LetterProperty); set => SetValue(LetterProperty, field = value); } + public partial char Letter { get => (char)GetValue(LetterProperty); set => SetValue(LetterProperty, value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs index 4e2535e4d6..d791152c27 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs @@ -47,8 +47,7 @@ public partial class BaseView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty BaseTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("BaseText", typeof(string), typeof(TestNamespace.BaseView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingBaseText = false; - public partial string BaseText { get => __initializingBaseText ? field : (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, field = value); } + public partial string BaseText { get => (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, value); } } """; @@ -67,15 +66,13 @@ public partial class DerivedView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty DerivedTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("DerivedText", typeof(string), typeof(TestNamespace.DerivedView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingDerivedText = false; - public partial string DerivedText { get => __initializingDerivedText ? field : (string)GetValue(DerivedTextProperty); set => SetValue(DerivedTextProperty, field = value); } + public partial string DerivedText { get => (string)GetValue(DerivedTextProperty); set => SetValue(DerivedTextProperty, value); } /// /// Backing BindableProperty for the property. /// public new static readonly global::Microsoft.Maui.Controls.BindableProperty BaseTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("BaseText", typeof(string), typeof(TestNamespace.DerivedView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingBaseText = false; - public new partial string BaseText { get => __initializingBaseText ? field : (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, field = value); } + public new partial string BaseText { get => (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, value); } } """; @@ -119,15 +116,13 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingValue = false; - public partial T? Value { get => __initializingValue ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, field = value); } + public partial T? Value { get => (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty NameProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Name", typeof(U), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingName = false; - public partial U? Name { get => __initializingName ? field : (U? )GetValue(NameProperty); set => SetValue(NameProperty, field = value); } + public partial U? Name { get => (U? )GetValue(NameProperty); set => SetValue(NameProperty, value); } } """; @@ -175,8 +170,7 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{outerClassName}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingText = false; - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } } } """; @@ -230,22 +224,19 @@ public partial class {{defaultTestClassName}} /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ModelProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Model", typeof(TestNamespace.CustomModel), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingModel = false; - public partial TestNamespace.CustomModel Model { get => __initializingModel ? field : (TestNamespace.CustomModel)GetValue(ModelProperty); set => SetValue(ModelProperty, field = value); } + public partial {{defaultTestNamespace}}.CustomModel Model { get => ({{defaultTestNamespace}}.CustomModel)GetValue(ModelProperty); set => SetValue(ModelProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty ItemsProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Items", typeof(System.Collections.Generic.List), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingItems = false; - public partial System.Collections.Generic.List Items { get => __initializingItems ? field : (System.Collections.Generic.List)GetValue(ItemsProperty); set => SetValue(ItemsProperty, field = value); } + public partial System.Collections.Generic.List Items { get => (System.Collections.Generic.List)GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); } /// /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty PropertiesProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Properties", typeof(System.Collections.Generic.Dictionary), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingProperties = false; - public partial System.Collections.Generic.Dictionary Properties { get => __initializingProperties ? field : (System.Collections.Generic.Dictionary)GetValue(PropertiesProperty); set => SetValue(PropertiesProperty, field = value); } + public partial System.Collections.Generic.Dictionary Properties { get => (System.Collections.Generic.Dictionary)GetValue(PropertiesProperty); set => SetValue(PropertiesProperty, value); } } """; @@ -292,8 +283,7 @@ public partial class FirstView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty FirstTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("FirstText", typeof(string), typeof(TestNamespace.FirstView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingFirstText = false; - public partial string FirstText { get => __initializingFirstText ? field : (string)GetValue(FirstTextProperty); set => SetValue(FirstTextProperty, field = value); } + public partial string FirstText { get => (string)GetValue(FirstTextProperty); set => SetValue(FirstTextProperty, value); } } """; @@ -312,8 +302,7 @@ public partial class SecondView /// Backing BindableProperty for the property. /// public static readonly global::Microsoft.Maui.Controls.BindableProperty SecondTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("SecondText", typeof(string), typeof(TestNamespace.SecondView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); - bool __initializingSecondText = false; - public partial string SecondText { get => __initializingSecondText ? field : (string)GetValue(SecondTextProperty); set => SetValue(SecondTextProperty, field = value); } + public partial string SecondText { get => (string)GetValue(SecondTextProperty); set => SetValue(SecondTextProperty, value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 3ac63d26c0..5c969b3a10 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -309,10 +309,13 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; var formattedReturnType = GetFormattedReturnType(info.ReturnType); - sb.Append("bool ") - .Append("__initializing") - .Append(info.PropertyName) - .Append(" = false;\n"); + if (info.HasInitializer) + { + sb.Append("bool ") + .Append("__initializing") + .Append(info.PropertyName) + .Append(" = false;\n"); + } sb.Append("public ") .Append(info.NewKeywordText) @@ -320,22 +323,42 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) .Append(formattedReturnType) .Append(' ') .Append(sanitizedPropertyName) - .Append("\n{\nget => ") - .Append("__initializing") - .Append(info.PropertyName) - .Append(" ? field : ") - .Append("(") - .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(", field = 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 From fd700dc046516f38692561fd40e7f447fad281ea Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Fri, 5 Dec 2025 19:01:22 +1100 Subject: [PATCH 04/24] Revert IntegrationTests --- .../IntegrationTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs index d791152c27..3df8bd95cc 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs @@ -1,3 +1,8 @@ +using CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; using Xunit; namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.BindablePropertyAttributeSourceGeneratorTests; From b2dc373d20ee33523ba90750c124a37798775f8f Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Fri, 5 Dec 2025 19:04:10 +1100 Subject: [PATCH 05/24] Revert setter accessibility changes --- .../Generators/BindablePropertyAttributeSourceGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 5c969b3a10..d6fa39649f 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -349,7 +349,8 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) { sb.Append(info.SetterAccessibility) .Append("set => SetValue(") - .Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName); + .Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName) + .Append(", value);\n"); if (info.HasInitializer) { From 6ddcd789cc4345c57ec430dc5b11602ba8c21cc4 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Fri, 5 Dec 2025 19:31:32 +1100 Subject: [PATCH 06/24] Corrected setter --- .../Generators/BindablePropertyAttributeSourceGenerator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index d6fa39649f..5c969b3a10 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -349,8 +349,7 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) { 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) { From c4bda269fa61bd779b22e41d5d638baf6ce2e6ad Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Fri, 5 Dec 2025 22:26:44 +1100 Subject: [PATCH 07/24] Refactor GetDefaultValueCreatorMethod. Update XML Doc. Apply to readonly BindableProperty. Extend unit test. --- .../CommonUsageTests.cs | 23 ++++++- ...indablePropertyAttributeSourceGenerator.cs | 60 ++++++++++++++----- 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index 9c2a3caf9e..aabce499e8 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -663,6 +663,7 @@ public async Task GenerateBindableProperty_WithInitializers_GeneratesCorrectCode $$""" using CommunityToolkit.Maui; using Microsoft.Maui.Controls; + using System; namespace {{defaultTestNamespace}}; @@ -670,6 +671,9 @@ public partial class {{defaultTestClassName}} : View { [BindablePropertyAttribute] public partial string Text { get; set; } = "Initial Value"; + + [BindablePropertyAttribute] + public partial TimeSpan CustomDuration { get; set; } = TimeSpan.FromSeconds(30); } """; @@ -689,8 +693,6 @@ public partial class {{defaultTestClassName}} /// 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; @@ -698,6 +700,23 @@ static object __createDefaultText(Microsoft.Maui.Controls.BindableObject bindabl (({{defaultTestClassName}})bindable).__initializingText = false; return defaultValue; } + + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + + /// + /// Backing BindableProperty for the property. + /// + 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(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); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 5c969b3a10..3a86268438 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -183,12 +183,13 @@ static string GenerateSource(SemanticValues value) GenerateBindableProperty(sb, in info); } - GenerateProperty(sb, in info); - if (info.HasInitializer) { + GenerateInitializingProperty(sb, in info); GenerateDefaultValueMethod(sb, in info, classNameWithGenerics); } + + GenerateProperty(sb, in info); } sb.Append('}'); @@ -241,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 @@ -297,7 +298,7 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel .Append(", ") .Append(info.CoerceValueMethodName) .Append(", ") - .Append(info.HasInitializer ? "__createDefault" + info.PropertyName : info.DefaultValueCreatorMethodName) + .Append(GetDefaulteValueCreatorMethod(in info)) .Append(");\n"); sb.Append('\n'); @@ -309,14 +310,6 @@ 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 ") @@ -568,13 +561,52 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) } } + /// + /// Determines the name of the method used to create the default value for the specified bindable property model. + /// + /// The bindable property model for which to retrieve the default value creator method name. + /// 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. + static string GetDefaulteValueCreatorMethod(in BindablePropertyModel info) + { + if (info.HasInitializer) + { + return "__createDefault" + info.PropertyName; + } + + return info.DefaultValueCreatorMethodName; + } + + /// + /// 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. + /// + /// The StringBuilder instance to which the initialization field declaration will be appended. + /// The model containing metadata about the property for which the initialization field is generated. + static void GenerateInitializingProperty(StringBuilder sb, in BindablePropertyModel info) + { + sb.Append("bool ") + .Append("__initializing") + .Append(info.PropertyName) + .Append(" = false;\n"); + } + + /// + /// 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. + /// + /// The StringBuilder instance to which the initialization field declaration will be appended. + /// The model containing metadata for the property that requires a default value creator. + /// The declaring class name including generic type parameters, if any. [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) + sb.Append("static object ") + .Append(GetDefaulteValueCreatorMethod(in info)) .Append("(Microsoft.Maui.Controls.BindableObject bindable)\n") .Append("{\n") .Append("((") From 06dd4580497b4844cbe226d3b3011ab4ebe4e311 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 09:44:05 +1100 Subject: [PATCH 08/24] Include global root namespace for BindableObject --- .../CommonUsageTests.cs | 4 ++-- .../Generators/BindablePropertyAttributeSourceGenerator.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index aabce499e8..08ab7ffbae 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -693,7 +693,7 @@ public partial class {{defaultTestClassName}} /// 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(Microsoft.Maui.Controls.BindableObject bindable) + static object __createDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) { (({{defaultTestClassName}})bindable).__initializingText = true; var defaultValue = (({{defaultTestClassName}})bindable).Text; @@ -708,7 +708,7 @@ static object __createDefaultText(Microsoft.Maui.Controls.BindableObject bindabl /// 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(Microsoft.Maui.Controls.BindableObject bindable) + static object __createDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) { (({{defaultTestClassName}})bindable).__initializingCustomDuration = true; var defaultValue = (({{defaultTestClassName}})bindable).CustomDuration; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 3a86268438..28ee55fc95 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -607,7 +607,7 @@ static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyMode sb.Append("static object ") .Append(GetDefaulteValueCreatorMethod(in info)) - .Append("(Microsoft.Maui.Controls.BindableObject bindable)\n") + .Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n") .Append("{\n") .Append("((") .Append(classNameWithGenerics) From 825adf8a55ed71595a6499feabeff3b8b2e8793e Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 10:00:35 +1100 Subject: [PATCH 09/24] Corrected typo in GetDefautValueCreatorMethodName --- .../BindablePropertyAttributeSourceGenerator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 28ee55fc95..d6b4039145 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -242,7 +242,7 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper .Append(", ") .Append(info.CoerceValueMethodName) .Append(", ") - .Append(GetDefaulteValueCreatorMethod(in info)) + .Append(GetDefaultValueCreatorMethodName(in info)) .Append(");\n"); // Generate public BindableProperty from the key @@ -298,7 +298,7 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel .Append(", ") .Append(info.CoerceValueMethodName) .Append(", ") - .Append(GetDefaulteValueCreatorMethod(in info)) + .Append(GetDefaultValueCreatorMethodName(in info)) .Append(");\n"); sb.Append('\n'); @@ -568,7 +568,7 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) /// 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. - static string GetDefaulteValueCreatorMethod(in BindablePropertyModel info) + static string GetDefaultValueCreatorMethodName(in BindablePropertyModel info) { if (info.HasInitializer) { @@ -606,7 +606,7 @@ static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyMode var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; sb.Append("static object ") - .Append(GetDefaulteValueCreatorMethod(in info)) + .Append(GetDefaultValueCreatorMethodName(in info)) .Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n") .Append("{\n") .Append("((") From 1727770caf12aaaaf37b74cff28906782ea739f2 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 10:23:15 +1100 Subject: [PATCH 10/24] Revert initializer-setter changes (not needed). Refactor getter-initializer implementation. --- .../CommonUsageTests.cs | 4 +-- ...indablePropertyAttributeSourceGenerator.cs | 33 +++++-------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index 08ab7ffbae..de133d28eb 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -701,7 +701,7 @@ static object __createDefaultText(global::Microsoft.Maui.Controls.BindableObject return defaultValue; } - public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, field = value); } + public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// Backing BindableProperty for the property. @@ -716,7 +716,7 @@ static object __createDefaultCustomDuration(global::Microsoft.Maui.Controls.Bind return defaultValue; } - public partial System.TimeSpan CustomDuration { get => __initializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, field = value); } + public partial System.TimeSpan CustomDuration { get => __initializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index d6b4039145..35a1f8c5a3 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -322,36 +322,21 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) { 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"); + .Append(" ? field : "); } + 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); - - if (info.HasInitializer) - { - sb.Append(", field = value);\n"); - } - else - { - sb.Append(", value);\n"); - } + .Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName) + .Append(", value);\n"); } // else Do not create a Setter because the property is read-only From 9d785b617c43ad22e9b6b4af42e103195120d396 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 10:46:15 +1100 Subject: [PATCH 11/24] Remove unneeded test from BindablePropertyModelTests --- .../BindablePropertyModelTests.cs | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 321bfcc9e5..6f625e479b 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -161,56 +161,4 @@ 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().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); - } } \ No newline at end of file From 8c823de6560f139dce0dfd4eb38e846c8499f27f Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 11:09:40 +1100 Subject: [PATCH 12/24] Refctor EffectiveDefaultValueCreatorMethodName property (replacing GetDefaultValueCreatorMethodName method) --- .../BindablePropertyModelTests.cs | 33 +++++++++++++++++++ ...indablePropertyAttributeSourceGenerator.cs | 23 ++----------- .../Models/Records.cs | 1 + 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 6f625e479b..75e6ef4d10 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -91,6 +91,7 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues() Assert.Equal(newKeywordText, model.NewKeywordText); Assert.Equal(hasInitializer, model.HasInitializer); Assert.Equal("TestPropertyProperty", model.BindablePropertyName); + Assert.Equal(defaultValueCreatorMethodName, model.EffectiveDefaultValueCreatorMethodName); } [Fact] @@ -161,4 +162,36 @@ static Compilation CreateCompilation(string source) references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } + + [Fact] + public void BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultValueCreatorMethodName() + { + // Arrange + var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } = \"Initial Value\"; }"); + var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; + var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); + + var model = new BindablePropertyModel( + "TestProperty", + propertySymbol.Type, + typeSymbol, + "null", + "Microsoft.Maui.Controls.BindingMode.OneWay", + "null", + "null", + "null", + "null", + "null", + string.Empty, + true, // IsReadOnlyBindableProperty + string.Empty, // SetterAccessibility + true + ); + + // Act + var effectiveDefaultValueCreatorMethodName = model.EffectiveDefaultValueCreatorMethodName; + + // Assert + Assert.Equal("__createDefaultTestProperty", effectiveDefaultValueCreatorMethodName); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 35a1f8c5a3..af232a2f1f 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -242,7 +242,7 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper .Append(", ") .Append(info.CoerceValueMethodName) .Append(", ") - .Append(GetDefaultValueCreatorMethodName(in info)) + .Append(info.EffectiveDefaultValueCreatorMethodName) .Append(");\n"); // Generate public BindableProperty from the key @@ -298,7 +298,7 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel .Append(", ") .Append(info.CoerceValueMethodName) .Append(", ") - .Append(GetDefaultValueCreatorMethodName(in info)) + .Append(info.EffectiveDefaultValueCreatorMethodName) .Append(");\n"); sb.Append('\n'); @@ -546,23 +546,6 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) } } - /// - /// Determines the name of the method used to create the default value for the specified bindable property model. - /// - /// The bindable property model for which to retrieve the default value creator method name. - /// 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. - static string GetDefaultValueCreatorMethodName(in BindablePropertyModel info) - { - if (info.HasInitializer) - { - return "__createDefault" + info.PropertyName; - } - - return info.DefaultValueCreatorMethodName; - } - /// /// 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. @@ -591,7 +574,7 @@ static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyMode var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; sb.Append("static object ") - .Append(GetDefaultValueCreatorMethodName(in info)) + .Append(info.EffectiveDefaultValueCreatorMethodName) .Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n") .Append("{\n") .Append("((") diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs index 925afe0037..686edea90e 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -7,6 +7,7 @@ record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeS { public string BindablePropertyName => $"{PropertyName}Property"; public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey"; + public string EffectiveDefaultValueCreatorMethodName => HasInitializer ? $"__createDefault{PropertyName}" : DefaultValueCreatorMethodName; } record SemanticValues(ClassInformation ClassInformation, EquatableArray BindableProperties); From ebab9e10df3fafa14b74e2ef19c5dec0c3c8bbb6 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 11:19:11 +1100 Subject: [PATCH 13/24] Update BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultValueCreatorMethodName for clarity --- .../BindablePropertyModelTests.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 75e6ef4d10..6487cd0df1 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -171,8 +171,11 @@ public void BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultV var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); + const string propertyName = "TestProperty"; + const bool hasInitializer = true; + var model = new BindablePropertyModel( - "TestProperty", + propertyName, propertySymbol.Type, typeSymbol, "null", @@ -183,15 +186,15 @@ public void BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultV "null", "null", string.Empty, - true, // IsReadOnlyBindableProperty - string.Empty, // SetterAccessibility - true + true, + string.Empty, + hasInitializer ); // Act var effectiveDefaultValueCreatorMethodName = model.EffectiveDefaultValueCreatorMethodName; // Assert - Assert.Equal("__createDefaultTestProperty", effectiveDefaultValueCreatorMethodName); + Assert.Equal("__createDefault" + propertyName, effectiveDefaultValueCreatorMethodName); } } \ No newline at end of file From c80418cdc811c1de635e4fdcdb8a4f88ca1c824b Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 11:23:51 +1100 Subject: [PATCH 14/24] Corrected unit test name GenerateBindableProperty_WithInitializer_GeneratesCorrectCode --- .../CommonUsageTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index de133d28eb..7d04a5ecbb 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -655,7 +655,7 @@ public partial class {{defaultTestClassName}} [Fact] - public async Task GenerateBindableProperty_WithInitializers_GeneratesCorrectCode() + public async Task GenerateBindableProperty_WithInitializer_GeneratesCorrectCode() { const string source = /* language=C#-test */ From 0aed7ad1f92efd80a4c0d5ffc8acced3db19ec2a Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Sat, 6 Dec 2025 11:37:02 +1100 Subject: [PATCH 15/24] Refactor InitializingPropertyName property --- .../BindablePropertyModelTests.cs | 1 + .../BindablePropertyAttributeSourceGenerator.cs | 14 ++++++-------- .../Models/Records.cs | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 6487cd0df1..3d91fa8084 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -92,6 +92,7 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues() Assert.Equal(hasInitializer, model.HasInitializer); Assert.Equal("TestPropertyProperty", model.BindablePropertyName); Assert.Equal(defaultValueCreatorMethodName, model.EffectiveDefaultValueCreatorMethodName); + Assert.Equal("__initializingTestProperty", model.InitializingPropertyName); } [Fact] diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index af232a2f1f..806031e8da 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -320,8 +320,7 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) if (info.HasInitializer) { - sb.Append("__initializing") - .Append(info.PropertyName) + sb.Append(info.InitializingPropertyName) .Append(" ? field : "); } @@ -555,8 +554,7 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) static void GenerateInitializingProperty(StringBuilder sb, in BindablePropertyModel info) { sb.Append("bool ") - .Append("__initializing") - .Append(info.PropertyName) + .Append(info.InitializingPropertyName) .Append(" = false;\n"); } @@ -579,8 +577,8 @@ static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyMode .Append("{\n") .Append("((") .Append(classNameWithGenerics) - .Append(")bindable).__initializing") - .Append(info.PropertyName) + .Append(")bindable).") + .Append(info.InitializingPropertyName) .Append(" = true;\n") .Append("var defaultValue = ") .Append("((") @@ -590,8 +588,8 @@ static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyMode .Append(";\n") .Append("((") .Append(classNameWithGenerics) - .Append(")bindable).__initializing") - .Append(info.PropertyName) + .Append(")bindable).") + .Append(info.InitializingPropertyName) .Append(" = false;\n") .Append("return defaultValue;\n") .Append("}\n"); diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs index 686edea90e..d85fcb26bc 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -8,6 +8,7 @@ record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeS public string BindablePropertyName => $"{PropertyName}Property"; public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey"; public string EffectiveDefaultValueCreatorMethodName => HasInitializer ? $"__createDefault{PropertyName}" : DefaultValueCreatorMethodName; + public string InitializingPropertyName => $"__initializing{PropertyName}"; } record SemanticValues(ClassInformation ClassInformation, EquatableArray BindableProperties); From 1ba82b48160de4b794937f75c511169ab22ca3c8 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:28:00 -0800 Subject: [PATCH 16/24] Add Edge Case Test `GenerateBindableProperty_WithBothInitializerAndDefault_GeneratedCodeDefaultsToUseDefaultValueCreatorMethod` --- .../EdgeCaseTests.cs | 47 +++++++++++++++++++ ...indablePropertyAttributeSourceGenerator.cs | 14 ++++-- .../Models/Records.cs | 5 +- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs index 8de524928e..2fef4c8764 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs @@ -456,4 +456,51 @@ public partial class {{defaultTestClassName}} await VerifySourceGeneratorAsync(source, expectedGenerated); } + + [Fact] + public async Task GenerateBindableProperty_WithBothInitializerAndDefault_GeneratedCodeDefaultsToUseDefaultValueCreatorMethod() + { + 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(DefaultValueCreatorMethodName = nameof(CreateDefaultText))] + public partial string Text { get; set; } = "Initial Value"; + + static string CreateDefaultText(BindableObject bindable) + { + return "Initial Value"; + } + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + #pragma warning disable + #nullable enable + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + 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); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 806031e8da..ca692124ae 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -183,7 +183,7 @@ static string GenerateSource(SemanticValues value) GenerateBindableProperty(sb, in info); } - if (info.HasInitializer) + if (info.ShouldUsePropertyInitializer) { GenerateInitializingProperty(sb, in info); GenerateDefaultValueMethod(sb, in info, classNameWithGenerics); @@ -320,8 +320,16 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) if (info.HasInitializer) { - sb.Append(info.InitializingPropertyName) - .Append(" ? field : "); + if (info.ShouldUsePropertyInitializer) + { + sb.Append(info.InitializingPropertyName); + } + else + { + sb.Append("false"); + } + + sb.Append(" ? field : "); } sb.Append("(") diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs index d85fcb26bc..a9319222d6 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -5,10 +5,13 @@ 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, bool HasInitializer) { + // When both a DefaultValueCreatorMethodName and an initializer are provided, we implement the DefaultValueCreator method and the ignore the partial Property initializer + public bool ShouldUsePropertyInitializer => HasInitializer && string.IsNullOrEmpty(DefaultValueCreatorMethodName); public string BindablePropertyName => $"{PropertyName}Property"; public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey"; - public string EffectiveDefaultValueCreatorMethodName => HasInitializer ? $"__createDefault{PropertyName}" : DefaultValueCreatorMethodName; + public string EffectiveDefaultValueCreatorMethodName => ShouldUsePropertyInitializer ? $"__createDefault{PropertyName}" : DefaultValueCreatorMethodName; public string InitializingPropertyName => $"__initializing{PropertyName}"; + } record SemanticValues(ClassInformation ClassInformation, EquatableArray BindableProperties); From 07d0087da7834b58d90f4ea9cf071475ce9bfa07 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:56:14 -0800 Subject: [PATCH 17/24] Use `"null"` instead of `string.IsNullOrEmpty()` --- .../Models/Records.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs index a9319222d6..b4e820c5c6 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -6,7 +6,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, bool HasInitializer) { // When both a DefaultValueCreatorMethodName and an initializer are provided, we implement the DefaultValueCreator method and the ignore the partial Property initializer - public bool ShouldUsePropertyInitializer => HasInitializer && string.IsNullOrEmpty(DefaultValueCreatorMethodName); + public bool ShouldUsePropertyInitializer => HasInitializer && DefaultValueCreatorMethodName is "null"; public string BindablePropertyName => $"{PropertyName}Property"; public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey"; public string EffectiveDefaultValueCreatorMethodName => ShouldUsePropertyInitializer ? $"__createDefault{PropertyName}" : DefaultValueCreatorMethodName; From 3a642d08becf324161ba3930734c77ad5d6ac8de Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:24:24 -0800 Subject: [PATCH 18/24] Remove `DefaultValue` --- .../ImageTouch/ImageTouchBehavior.shared.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs index d2e5e2100a..c41225af79 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs @@ -11,42 +11,42 @@ public partial class ImageTouchBehavior : TouchBehavior /// /// Gets or sets the when is . /// - [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.DefaultBackgroundImageSource)] - public partial ImageSource? DefaultImageSource { get; set; } + [BindableProperty] + public partial ImageSource? DefaultImageSource { get; set; } = (ImageSource?)ImageTouchBehaviorDefaults.DefaultBackgroundImageSource; /// /// Gets or sets the when the is /// - [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.HoveredBackgroundImageSource)] - public partial ImageSource? HoveredImageSource { get; set; } + [BindableProperty] + public partial ImageSource? HoveredImageSource { get; set; } = (ImageSource?)ImageTouchBehaviorDefaults.HoveredBackgroundImageSource; /// /// Gets or sets the when the is /// - [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.PressedBackgroundImageSource)] - public partial ImageSource? PressedImageSource { get; set; } + [BindableProperty] + public partial ImageSource? PressedImageSource { get; set; } = (ImageSource?)ImageTouchBehaviorDefaults.PressedBackgroundImageSource; /// /// Gets or sets the when is . /// - [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.DefaultBackgroundImageAspect)] - public partial Aspect DefaultImageAspect { get; set; } + [BindableProperty] + public partial Aspect DefaultImageAspect { get; set; } = ImageTouchBehaviorDefaults.DefaultBackgroundImageAspect; /// /// Gets or sets the when is . /// - [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.HoveredBackgroundImageAspect)] - public partial Aspect HoveredImageAspect { get; set; } + [BindableProperty] + public partial Aspect HoveredImageAspect { get; set; } = ImageTouchBehaviorDefaults.HoveredBackgroundImageAspect; /// /// Gets or sets the when the is /// - [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.PressedBackgroundImageAspect)] - public partial Aspect PressedImageAspect { get; set; } + [BindableProperty] + public partial Aspect PressedImageAspect { get; set; } = ImageTouchBehaviorDefaults.PressedBackgroundImageAspect; /// /// Gets or sets a value indicating whether the image should be set when the animation ends. /// - [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.ShouldSetImageOnAnimationEnd)] - public partial bool ShouldSetImageOnAnimationEnd { get; set; } + [BindableProperty] + public partial bool ShouldSetImageOnAnimationEnd { get; set; } = ImageTouchBehaviorDefaults.ShouldSetImageOnAnimationEnd; } \ No newline at end of file From 77b3fa57780277e4dcd36adc1bac04c05c4733a5 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:01:45 -0800 Subject: [PATCH 19/24] Move out-of-scope variables to `file static class` --- .../CommonUsageTests.cs | 39 +++--- .../BindablePropertyModelTests.cs | 39 +++++- ...indablePropertyAttributeSourceGenerator.cs | 112 +++++++++++------- .../Models/Records.cs | 4 +- 4 files changed, 132 insertions(+), 62 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index 7d04a5ecbb..5ec07004bc 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -691,32 +691,35 @@ public partial class {{defaultTestClassName}} /// /// Backing BindableProperty for the property. /// - 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, value); } + 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); } /// /// Backing BindableProperty for the property. /// - 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) + 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) { - (({{defaultTestClassName}})bindable).__initializingCustomDuration = true; - var defaultValue = (({{defaultTestClassName}})bindable).CustomDuration; - (({{defaultTestClassName}})bindable).__initializingCustomDuration = false; + IsInitializingText = true; + var defaultValue = ((TestView)bindable).Text; + IsInitializingText = false; return defaultValue; } - public partial System.TimeSpan CustomDuration { get => __initializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } + 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; + } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 3d91fa8084..81e9ffe185 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -92,7 +92,7 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues() Assert.Equal(hasInitializer, model.HasInitializer); Assert.Equal("TestPropertyProperty", model.BindablePropertyName); Assert.Equal(defaultValueCreatorMethodName, model.EffectiveDefaultValueCreatorMethodName); - Assert.Equal("__initializingTestProperty", model.InitializingPropertyName); + Assert.Equal("IsInitializingTestProperty", model.InitializingPropertyName); } [Fact] @@ -196,6 +196,41 @@ public void BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultV var effectiveDefaultValueCreatorMethodName = model.EffectiveDefaultValueCreatorMethodName; // Assert - Assert.Equal("__createDefault" + propertyName, effectiveDefaultValueCreatorMethodName); + Assert.Equal("CreateDefault" + propertyName, effectiveDefaultValueCreatorMethodName); + } + + [Fact] + public void BindablePropertyName_WithInitializerAndDefaulValueCreator_ReturnsCorrectEffectiveDefaultValueCreatorMethodName() + { + // Arrange + var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } = \"Initial Value\"; }"); + var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; + var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); + + const string propertyName = "TestProperty"; + const bool hasInitializer = true; + + var model = new BindablePropertyModel( + propertyName, + propertySymbol.Type, + typeSymbol, + "null", + "Microsoft.Maui.Controls.BindingMode.OneWay", + "null", + "null", + "null", + "null", + "CreateTextDefaultValue", + string.Empty, + true, + string.Empty, + hasInitializer + ); + + // Act + var effectiveDefaultValueCreatorMethodName = model.EffectiveDefaultValueCreatorMethodName; + + // Assert + Assert.Equal("CreateTextDefaultValue", effectiveDefaultValueCreatorMethodName); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 31eaad7982..0a756f29e1 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -174,24 +174,42 @@ static string GenerateSource(SemanticValues value) sb.Append(value.ClassInformation.DeclaredAccessibility).Append(" partial class ").Append(classNameWithGenerics).Append("\n{\n\n"); + // Prepare helper builder for file-static class members (static flags + default creators) + var fileStaticClassStringBuilder = new StringBuilder(256); + var helperNames = new HashSet(); + const string fileStaticClassName = "__BindablePropertyInitHelpers"; + + // Build fully-qualified declaring type name for helper method casts (include containing types) + var fullDeclaringType = string.IsNullOrEmpty(value.ClassInformation.ContainingTypes) + ? classNameWithGenerics + : string.Concat(value.ClassInformation.ContainingTypes, ".", classNameWithGenerics); + foreach (var info in value.BindableProperties) { if (info.IsReadOnlyBindableProperty) { - GenerateReadOnlyBindableProperty(sb, in info); + GenerateReadOnlyBindableProperty(sb, in info, fileStaticClassName); } else { - GenerateBindableProperty(sb, in info); + GenerateBindableProperty(sb, in info, fileStaticClassName); } if (info.ShouldUsePropertyInitializer) { - GenerateInitializingProperty(sb, in info); - GenerateDefaultValueMethod(sb, in info, classNameWithGenerics); + // Generate only references within the class; actual static field and creator method + // will be placed inside the file static helper class below. + if (helperNames.Add(info.InitializingPropertyName)) + { + AppendHelperInitializingField(fileStaticClassStringBuilder, in info); + } + if (helperNames.Add(info.EffectiveDefaultValueCreatorMethodName)) + { + AppendHelperDefaultValueMethod(fileStaticClassStringBuilder, in info, fullDeclaringType); + } } - GenerateProperty(sb, in info); + GenerateProperty(sb, in info, fileStaticClassName); } sb.Append('}'); @@ -206,11 +224,19 @@ static string GenerateSource(SemanticValues value) } } + // If we generated any helper members, emit a file static class with them. + if (fileStaticClassStringBuilder.Length > 0) + { + sb.Append("\n\nfile static class ").Append(fileStaticClassName).Append("\n{\n"); + sb.Append(fileStaticClassStringBuilder.ToString()); + sb.Append("}\n"); + } + return sb.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindablePropertyModel info) + static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindablePropertyModel info, in string fileStaticClassName) { // Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType); @@ -243,8 +269,15 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper .Append(info.PropertyChangingMethodName) .Append(", ") .Append(info.CoerceValueMethodName) - .Append(", ") - .Append(info.EffectiveDefaultValueCreatorMethodName) + .Append(", "); + + if (info.ShouldUsePropertyInitializer) + { + sb.Append(fileStaticClassName) + .Append('.'); + } + + sb.Append(info.EffectiveDefaultValueCreatorMethodName) .Append(");\n"); // Generate public BindableProperty from the key @@ -262,7 +295,7 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper } [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel info) + static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel info, in string helperClassName) { // Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType); @@ -299,15 +332,21 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel .Append(info.PropertyChangingMethodName) .Append(", ") .Append(info.CoerceValueMethodName) - .Append(", ") - .Append(info.EffectiveDefaultValueCreatorMethodName) - .Append(");\n"); + .Append(", "); - sb.Append('\n'); + if (info.ShouldUsePropertyInitializer) + { + sb.Append(helperClassName) + .Append('.'); + } + + sb.Append(info.EffectiveDefaultValueCreatorMethodName) + .Append(");\n") + .Append('\n'); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) + static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info, in string fileStaticClassName) { var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; var formattedReturnType = GetFormattedReturnType(info.ReturnType); @@ -324,7 +363,8 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) { if (info.ShouldUsePropertyInitializer) { - sb.Append(info.InitializingPropertyName); + // Now reference the static flag on the file static helper class + sb.Append(fileStaticClassName).Append(".").Append(info.InitializingPropertyName); } else { @@ -556,52 +596,44 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) } /// - /// 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. + /// Appends the initializing flag into the file-static helper class. /// - /// The StringBuilder instance to which the initialization field declaration will be appended. - /// The model containing metadata about the property for which the initialization field is generated. - static void GenerateInitializingProperty(StringBuilder sb, in BindablePropertyModel info) + /// Helper StringBuilder used to collect helper members. + /// Property model. + static void AppendHelperInitializingField(StringBuilder helperSb, in BindablePropertyModel info) { - sb.Append("bool ") + // Make the flag public static so it can be referenced from the generated partial class in the same file. + helperSb.Append("public static bool ") .Append(info.InitializingPropertyName) .Append(" = false;\n"); } /// - /// 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. + /// Appends a default value creator method into the file-static helper class. + /// The method sets the static initializing flag, reads the property's initializer value by casting the bindable + /// to the declaring type, then clears the flag and returns the value. /// - /// The StringBuilder instance to which the initialization field declaration will be appended. - /// The model containing metadata for the property that requires a default value creator. - /// The declaring class name including generic type parameters, if any. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyModel info, string classNameWithGenerics) + /// Helper StringBuilder used to collect helper members. + /// Property model. + /// Declaring type including containing types and generic parameters. + static void AppendHelperDefaultValueMethod(StringBuilder helperSb, in BindablePropertyModel info, string fullDeclaringType) { var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; - sb.Append("static object ") + helperSb.Append("public static object ") .Append(info.EffectiveDefaultValueCreatorMethodName) .Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n") .Append("{\n") - .Append("((") - .Append(classNameWithGenerics) - .Append(")bindable).") .Append(info.InitializingPropertyName) .Append(" = true;\n") - .Append("var defaultValue = ") - .Append("((") - .Append(classNameWithGenerics) + .Append("var defaultValue = ((") + .Append(fullDeclaringType) .Append(")bindable).") .Append(sanitizedPropertyName) .Append(";\n") - .Append("((") - .Append(classNameWithGenerics) - .Append(")bindable).") .Append(info.InitializingPropertyName) .Append(" = false;\n") .Append("return defaultValue;\n") - .Append("}\n"); + .Append("}\n\n"); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs index 952fa2de1d..ddc946229b 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -9,8 +9,8 @@ public record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, public bool ShouldUsePropertyInitializer => HasInitializer && DefaultValueCreatorMethodName is "null"; public string BindablePropertyName => $"{PropertyName}Property"; public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey"; - public string EffectiveDefaultValueCreatorMethodName => ShouldUsePropertyInitializer ? $"__createDefault{PropertyName}" : DefaultValueCreatorMethodName; - public string InitializingPropertyName => $"__initializing{PropertyName}"; + public string EffectiveDefaultValueCreatorMethodName => ShouldUsePropertyInitializer ? $"CreateDefault{PropertyName}" : DefaultValueCreatorMethodName; + public string InitializingPropertyName => $"IsInitializing{PropertyName}"; } From fc82b827efa571bb5c09db30f72b0b75c3ac4a4f Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:01:24 -0800 Subject: [PATCH 20/24] Use raw string literal --- .../BindablePropertyModelTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 81e9ffe185..f0df70603e 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -168,7 +168,10 @@ static Compilation CreateCompilation(string source) public void BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultValueCreatorMethodName() { // Arrange - var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } = \"Initial Value\"; }"); + var compilation = CreateCompilation( + """ + public class TestClass { public string TestProperty { get; set; } = "Initial Value"; }"); + """); var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); @@ -203,7 +206,10 @@ public void BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultV public void BindablePropertyName_WithInitializerAndDefaulValueCreator_ReturnsCorrectEffectiveDefaultValueCreatorMethodName() { // Arrange - var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } = \"Initial Value\"; }"); + var compilation = CreateCompilation( + """ + public class TestClass { public string TestProperty { get; set; } = "Initial Value"; } + """); var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); From 4060d68de5786122a13af14638b9648ca30a2f03 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:19:15 -0800 Subject: [PATCH 21/24] Update variable name --- .../BindablePropertyAttributeSourceGenerator.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 0a756f29e1..cd68704a4c 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -598,12 +598,12 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) /// /// Appends the initializing flag into the file-static helper class. /// - /// Helper StringBuilder used to collect helper members. + /// Helper StringBuilder used to collect helper members. /// Property model. - static void AppendHelperInitializingField(StringBuilder helperSb, in BindablePropertyModel info) + static void AppendHelperInitializingField(StringBuilder fileStaticClassStringBuilder, in BindablePropertyModel info) { // Make the flag public static so it can be referenced from the generated partial class in the same file. - helperSb.Append("public static bool ") + fileStaticClassStringBuilder.Append("public static bool ") .Append(info.InitializingPropertyName) .Append(" = false;\n"); } @@ -613,14 +613,14 @@ static void AppendHelperInitializingField(StringBuilder helperSb, in BindablePro /// The method sets the static initializing flag, reads the property's initializer value by casting the bindable /// to the declaring type, then clears the flag and returns the value. /// - /// Helper StringBuilder used to collect helper members. + /// Helper StringBuilder used to collect helper members. /// Property model. /// Declaring type including containing types and generic parameters. - static void AppendHelperDefaultValueMethod(StringBuilder helperSb, in BindablePropertyModel info, string fullDeclaringType) + static void AppendHelperDefaultValueMethod(StringBuilder fileStaticClassStringBuilder, in BindablePropertyModel info, string fullDeclaringType) { var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; - helperSb.Append("public static object ") + fileStaticClassStringBuilder.Append("public static object ") .Append(info.EffectiveDefaultValueCreatorMethodName) .Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n") .Append("{\n") From a382be9d0994868f98e623e4c9dd107430d89343 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:21:57 -0800 Subject: [PATCH 22/24] Implement `[BindableProperty]` for `AvatarView` --- .../Views/AvatarView.shared.cs | 86 ++++++------------- 1 file changed, 28 insertions(+), 58 deletions(-) diff --git a/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs b/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs index 37ecc433ea..ccec8f309b 100644 --- a/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs +++ b/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs @@ -9,15 +9,6 @@ namespace CommunityToolkit.Maui.Views; /// AvatarView control. public partial class AvatarView : Border, IAvatarView, IBorderElement, IFontElement, ITextElement, IImageElement, ITextAlignmentElement, ILineHeightElement, ICornerElement { - /// The backing store for the bindable property. - public static readonly BindableProperty BorderColorProperty = BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(IAvatarView), defaultValue: AvatarViewDefaults.DefaultBorderColor, propertyChanged: OnBorderColorPropertyChanged); - - /// The backing store for the bindable property. - public static readonly BindableProperty BorderWidthProperty = BindableProperty.Create(nameof(BorderWidth), typeof(double), typeof(IAvatarView), defaultValue: AvatarViewDefaults.DefaultBorderWidth, propertyChanged: OnBorderWidthPropertyChanged); - - /// The backing store for the bindable property. - public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CornerRadius), typeof(CornerRadius), typeof(ICornerElement), defaultValue: AvatarViewDefaults.DefaultCornerRadius, propertyChanged: OnCornerRadiusPropertyChanged); - /// The backing store for the bindable property. public static readonly BindableProperty FontAttributesProperty = FontElement.FontAttributesProperty; @@ -30,15 +21,9 @@ public partial class AvatarView : Border, IAvatarView, IBorderElement, IFontElem /// The backing store for the bindable property. public static readonly BindableProperty FontSizeProperty = FontElement.FontSizeProperty; - /// The backing store for the bindable property. - public static readonly BindableProperty ImageSourceProperty = BindableProperty.Create(nameof(ImageSource), typeof(ImageSource), typeof(IImageElement), default(ImageSource), propertyChanged: OnImageSourcePropertyChanged); - /// The backing store for the bindable property. public static readonly BindableProperty TextColorProperty = TextElement.TextColorProperty; - /// The backing store for the bindable property. - public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(AvatarView), defaultValue: AvatarViewDefaults.DefaultText, propertyChanged: OnTextPropertyChanged); - /// The backing store for the bindable property. public static readonly BindableProperty TextTransformProperty = TextElement.TextTransformProperty; @@ -80,20 +65,6 @@ public AvatarView() /// Gets or sets the control font. public Microsoft.Maui.Font Font { get; set; } = Microsoft.Maui.Font.SystemFontOfSize((double)FontElement.FontSizeProperty.DefaultValue); - /// Gets or sets a value of the control border colour. - public Color BorderColor - { - get => (Color)GetValue(BorderColorProperty); - set => SetValue(BorderColorProperty, value); - } - - /// Gets or sets a value of the control border width. - public double BorderWidth - { - get => (double)GetValue(BorderWidthProperty); - set => SetValue(BorderWidthProperty, value); - } - /// Gets or sets a value of the control text character spacing property. public double CharacterSpacing { @@ -101,13 +72,6 @@ public double CharacterSpacing set => SetValue(TextElement.CharacterSpacingProperty, value); } - /// Gets or sets a value of the control corner radius property. - public CornerRadius CornerRadius - { - get => (CornerRadius)GetValue(CornerRadiusProperty); - set => SetValue(CornerRadiusProperty, value); - } - /// Gets or sets a value of the control font attributes property. public FontAttributes FontAttributes { @@ -137,20 +101,26 @@ public double FontSize set => SetValue(FontElement.FontSizeProperty, value); } + /// Gets or sets a value of the control border colour. + [BindableProperty(PropertyChangedMethodName = nameof(OnBorderColorPropertyChanged))] + public partial Color BorderColor { get; set; } = AvatarViewDefaults.DefaultBorderColor; + + /// Gets or sets a value of the control border width. + [BindableProperty(PropertyChangedMethodName = nameof(OnBorderWidthPropertyChanged))] + public partial double BorderWidth { get; set; } = AvatarViewDefaults.DefaultBorderWidth; + + /// Gets or sets a value of the control corner radius property. + [BindableProperty(PropertyChangedMethodName = nameof(OnCornerRadiusPropertyChanged))] + public partial CornerRadius CornerRadius { get; set; } = AvatarViewDefaults.DefaultCornerRadius; + /// Gets or sets a value of the control image source property. [TypeConverter(typeof(ImageSourceConverter))] - public ImageSource ImageSource - { - get => (ImageSource)GetValue(ImageSourceProperty); - set => SetValue(ImageSourceProperty, value); - } + [BindableProperty(PropertyChangedMethodName = nameof(OnImageSourcePropertyChanged))] + public partial ImageSource ImageSource { get; set; } /// Gets or sets a value of the control text property. - public string Text - { - get => (string)GetValue(TextProperty); - set => SetValue(TextProperty, value); - } + [BindableProperty(PropertyChangedMethodName = nameof(OnTextPropertyChanged))] + public partial string Text { get; set; } = AvatarViewDefaults.DefaultText; /// Gets or sets a value of the control text colour property. public Color TextColor @@ -170,13 +140,13 @@ public TextTransform TextTransform Aspect IImageElement.Aspect => avatarImage.Aspect; - Color IBorderElement.BorderColorDefaultValue => (Color)BorderColorProperty.DefaultValue; + Color IBorderElement.BorderColorDefaultValue => AvatarViewDefaults.DefaultBorderColor; - double IBorderElement.BorderWidthDefaultValue => (double)BorderWidthProperty.DefaultValue; + double IBorderElement.BorderWidthDefaultValue => AvatarViewDefaults.DefaultBorderWidth; int IBorderElement.CornerRadius => (int)GetAverageCorderRadius(CornerRadius); - int IBorderElement.CornerRadiusDefaultValue => (int)GetAverageCorderRadius((CornerRadius)CornerRadiusProperty.DefaultValue); + int IBorderElement.CornerRadiusDefaultValue => (int)GetAverageCorderRadius(AvatarViewDefaults.DefaultCornerRadius); TextAlignment ITextAlignment.HorizontalTextAlignment => ((ITextAlignmentElement)this).HorizontalTextAlignment; @@ -344,15 +314,15 @@ void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e) { // Ensure avatarImage is clipped to the bounds of the AvatarView whenever its Height, Width, CornerRadius and Padding properties change if ((e.PropertyName == HeightProperty.PropertyName - || e.PropertyName == WidthProperty.PropertyName - || e.PropertyName == PaddingProperty.PropertyName - || e.PropertyName == ImageSourceProperty.PropertyName - || e.PropertyName == BorderWidthProperty.PropertyName - || e.PropertyName == CornerRadiusProperty.PropertyName - || e.PropertyName == StrokeThicknessProperty.PropertyName) - && Height >= 0 // The default value of Height (before the view is drawn onto the page) is -1 - && Width >= 0 // The default value of Y (before the view is drawn onto the page) is -1 - && avatarImage.Source is not null) + || e.PropertyName == WidthProperty.PropertyName + || e.PropertyName == PaddingProperty.PropertyName + || e.PropertyName == ImageSourceProperty.PropertyName + || e.PropertyName == BorderWidthProperty.PropertyName + || e.PropertyName == CornerRadiusProperty.PropertyName + || e.PropertyName == StrokeThicknessProperty.PropertyName) + && Height >= 0 // The default value of Height (before the view is drawn onto the page) is -1 + && Width >= 0 // The default value of Y (before the view is drawn onto the page) is -1 + && avatarImage.Source is not null) { Geometry? avatarImageClipGeometry = null; From a7ad7bc5eabd62c269b526072ccc141c294cfba1 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:25:57 -0800 Subject: [PATCH 23/24] Include class name in `file static class` --- .../CommonUsageTests.cs | 10 +++++----- .../BindablePropertyAttributeSourceGenerator.cs | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index 5ec07004bc..593bcabf7f 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -691,17 +691,17 @@ public partial class {{defaultTestClassName}} /// /// Backing BindableProperty for the property. /// - 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); } + 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, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultText); + public partial string Text { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// Backing BindableProperty for the property. /// - 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); } + 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, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultCustomDuration); + public partial System.TimeSpan CustomDuration { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } } - file static class __BindablePropertyInitHelpers + file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { public static bool IsInitializingText = false; public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index cd68704a4c..b38a572090 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -177,12 +177,13 @@ static string GenerateSource(SemanticValues value) // Prepare helper builder for file-static class members (static flags + default creators) var fileStaticClassStringBuilder = new StringBuilder(256); var helperNames = new HashSet(); - const string fileStaticClassName = "__BindablePropertyInitHelpers"; // Build fully-qualified declaring type name for helper method casts (include containing types) var fullDeclaringType = string.IsNullOrEmpty(value.ClassInformation.ContainingTypes) ? classNameWithGenerics : string.Concat(value.ClassInformation.ContainingTypes, ".", classNameWithGenerics); + + var fileStaticClassName = $"__{classNameWithGenerics}BindablePropertyInitHelpers"; foreach (var info in value.BindableProperties) { From ead0fea34e384dd8e1a3065531833ba19224412b Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:40:33 -0800 Subject: [PATCH 24/24] Use `[BindableProperty]` for AvatarView --- .../Defaults/AvatarViewDefaults.shared.cs | 14 ++--- .../AvatarView/AvatarViewInterfaceTests.cs | 10 ++-- .../Views/AvatarView/AvatarViewTests.cs | 41 +++++++++---- .../Views/AvatarView.shared.cs | 60 +++++++++---------- 4 files changed, 72 insertions(+), 53 deletions(-) diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/AvatarViewDefaults.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/AvatarViewDefaults.shared.cs index 9c783bbc5d..ba9ed825be 100644 --- a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/AvatarViewDefaults.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/AvatarViewDefaults.shared.cs @@ -6,23 +6,23 @@ namespace CommunityToolkit.Maui.Core; static class AvatarViewDefaults { /// Default avatar border width. - public const double DefaultBorderWidth = 1; + public const double BorderWidth = 1; /// Default height request. - public const double DefaultHeightRequest = 48; + public const double HeightRequest = 48; /// Default avatar text. - public const string DefaultText = "?"; + public const string Text = "?"; /// Default width request. - public const double DefaultWidthRequest = 48; + public const double WidthRequest = 48; /// default avatar border colour. - public static Color DefaultBorderColor { get; } = Colors.White; + public static Color BorderColor { get; } = Colors.White; /// Default corner radius. - public static CornerRadius DefaultCornerRadius { get; } = new(24, 24, 24, 24); + public static CornerRadius CornerRadius { get; } = new(24, 24, 24, 24); /// Default padding. - public static Thickness DefaultPadding { get; } = new(1); + public static Thickness Padding { get; } = new(1); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewInterfaceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewInterfaceTests.cs index 1223da88ff..777163852b 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewInterfaceTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewInterfaceTests.cs @@ -15,21 +15,21 @@ public AvatarViewInterfaceTests() public void IBorderElementBorderColorDefaultValue() { var avatarView = new Maui.Views.AvatarView(); - ((IBorderElement)avatarView).BorderColorDefaultValue.Should().Be(AvatarViewDefaults.DefaultBorderColor); + ((IBorderElement)avatarView).BorderColorDefaultValue.Should().Be(AvatarViewDefaults.BorderColor); } [Fact] public void IBorderElementBorderWidthDefaultValue() { var avatarView = new Maui.Views.AvatarView(); - ((IBorderElement)avatarView).BorderWidthDefaultValue.Should().Be(AvatarViewDefaults.DefaultBorderWidth); + ((IBorderElement)avatarView).BorderWidthDefaultValue.Should().Be(AvatarViewDefaults.BorderWidth); } [Fact] public void IBorderElementCornerRadius() { var avatarView = new Maui.Views.AvatarView(); - int average = (int)(new[] { AvatarViewDefaults.DefaultCornerRadius.TopLeft, AvatarViewDefaults.DefaultCornerRadius.TopRight, AvatarViewDefaults.DefaultCornerRadius.BottomLeft, AvatarViewDefaults.DefaultCornerRadius.BottomRight }).Average(); + int average = (int)(new[] { AvatarViewDefaults.CornerRadius.TopLeft, AvatarViewDefaults.CornerRadius.TopRight, AvatarViewDefaults.CornerRadius.BottomLeft, AvatarViewDefaults.CornerRadius.BottomRight }).Average(); ((IBorderElement)avatarView).CornerRadius.Should().Be(average); CornerRadius cornerRadius = new(3, 7, 37, 73); average = (int)(new[] { cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomLeft, cornerRadius.BottomRight }).Average(); @@ -41,7 +41,7 @@ public void IBorderElementCornerRadius() public void IBorderElementCornerRadiusDefaultValue() { var avatarView = new Maui.Views.AvatarView(); - int average = (int)(new[] { AvatarViewDefaults.DefaultCornerRadius.TopLeft, AvatarViewDefaults.DefaultCornerRadius.TopRight, AvatarViewDefaults.DefaultCornerRadius.BottomLeft, AvatarViewDefaults.DefaultCornerRadius.BottomRight }).Average(); + int average = (int)(new[] { AvatarViewDefaults.CornerRadius.TopLeft, AvatarViewDefaults.CornerRadius.TopRight, AvatarViewDefaults.CornerRadius.BottomLeft, AvatarViewDefaults.CornerRadius.BottomRight }).Average(); ((IBorderElement)avatarView).CornerRadiusDefaultValue.Should().Be(average); } @@ -235,7 +235,7 @@ public void IsCornerRadiusSet() public void IBorderElementOnBorderColorPropertyChanged() { var avatarView = new Maui.Views.AvatarView(); - avatarView.Stroke.Should().Be((SolidColorBrush)AvatarViewDefaults.DefaultBorderColor); + avatarView.Stroke.Should().Be((SolidColorBrush)AvatarViewDefaults.BorderColor); ((IBorderElement)avatarView).OnBorderColorPropertyChanged(Colors.AliceBlue, Colors.Azure); avatarView.Stroke.Should().Be((SolidColorBrush)Colors.Azure); } diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewTests.cs index bde79255a2..0690d1c576 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/AvatarView/AvatarViewTests.cs @@ -13,6 +13,25 @@ public AvatarViewTests() Assert.IsType(new Maui.Views.AvatarView(), exactMatch: false); } + [Fact] + public void MultipleAvatarViews_BindablePropertyAttributedProperties_WithPartialPropertyInializers_UseCorrectDefaultValue() + { + // Arrange + var avatarView1 = new Maui.Views.AvatarView(); + var avatarView2 = new Maui.Views.AvatarView(); + + // Act Assert + Assert.Equal(AvatarViewDefaults.BorderColor, avatarView1.BorderColor); + Assert.Equal(AvatarViewDefaults.BorderWidth, avatarView1.BorderWidth); + Assert.Equal(AvatarViewDefaults.CornerRadius, avatarView1.CornerRadius); + Assert.Equal(AvatarViewDefaults.Text, avatarView1.Text); + + Assert.Equal(AvatarViewDefaults.BorderColor, avatarView2.BorderColor); + Assert.Equal(AvatarViewDefaults.BorderWidth, avatarView2.BorderWidth); + Assert.Equal(AvatarViewDefaults.CornerRadius, avatarView2.CornerRadius); + Assert.Equal(AvatarViewDefaults.Text, avatarView2.Text); + } + [Fact] public void AvatarViewShouldBeAssignedToIAvatarView() { @@ -164,21 +183,21 @@ public void CornerRadiusSameRadiusToThree() public void DefaultBorderColor() { var avatarView = new Maui.Views.AvatarView(); - avatarView.BorderColor.Should().Be(AvatarViewDefaults.DefaultBorderColor); + avatarView.BorderColor.Should().Be(AvatarViewDefaults.BorderColor); } [Fact] public void DefaultBorderWidth() { var avatarView = new Maui.Views.AvatarView(); - avatarView.BorderWidth.Should().Be(AvatarViewDefaults.DefaultBorderWidth); + avatarView.BorderWidth.Should().Be(AvatarViewDefaults.BorderWidth); } [Fact] public void DefaultCornerRadius() { var avatarView = new Maui.Views.AvatarView(); - avatarView.CornerRadius.Should().Be(new CornerRadius(AvatarViewDefaults.DefaultCornerRadius.TopLeft, AvatarViewDefaults.DefaultCornerRadius.TopRight, AvatarViewDefaults.DefaultCornerRadius.BottomLeft, AvatarViewDefaults.DefaultCornerRadius.BottomRight)); + avatarView.CornerRadius.Should().Be(new CornerRadius(AvatarViewDefaults.CornerRadius.TopLeft, AvatarViewDefaults.CornerRadius.TopRight, AvatarViewDefaults.CornerRadius.BottomLeft, AvatarViewDefaults.CornerRadius.BottomRight)); } [Fact] @@ -195,7 +214,7 @@ public void DefaultHeightRequest() CreateViewHandler(avatarView); Size request = avatarView.Measure(double.PositiveInfinity, double.PositiveInfinity); - request.Height.Should().Be(AvatarViewDefaults.DefaultHeightRequest); + request.Height.Should().Be(AvatarViewDefaults.HeightRequest); } [Fact] @@ -208,7 +227,7 @@ public void DefaultLabelProperties() avatarLabel.Should().NotBeNull(); avatarLabel.HorizontalTextAlignment.Should().Be(TextAlignment.Center); avatarLabel.VerticalTextAlignment.Should().Be(TextAlignment.Center); - avatarLabel.Text.Should().Be(AvatarViewDefaults.DefaultText); + avatarLabel.Text.Should().Be(AvatarViewDefaults.Text); } } @@ -219,11 +238,11 @@ public void DefaultProperties() avatarView.IsEnabled.Should().BeTrue(); avatarView.HorizontalOptions.Should().Be(LayoutOptions.Center); avatarView.VerticalOptions.Should().Be(LayoutOptions.Center); - avatarView.HeightRequest.Should().Be(AvatarViewDefaults.DefaultHeightRequest); - avatarView.WidthRequest.Should().Be(AvatarViewDefaults.DefaultWidthRequest); - avatarView.Padding.Should().Be(AvatarViewDefaults.DefaultPadding); - avatarView.Stroke.Should().Be((SolidColorBrush)AvatarViewDefaults.DefaultBorderColor); - avatarView.StrokeThickness.Should().Be(AvatarViewDefaults.DefaultBorderWidth); + avatarView.HeightRequest.Should().Be(AvatarViewDefaults.HeightRequest); + avatarView.WidthRequest.Should().Be(AvatarViewDefaults.WidthRequest); + avatarView.Padding.Should().Be(AvatarViewDefaults.Padding); + avatarView.Stroke.Should().Be((SolidColorBrush)AvatarViewDefaults.BorderColor); + avatarView.StrokeThickness.Should().Be(AvatarViewDefaults.BorderWidth); avatarView.StrokeShape.Should().BeOfType(); } @@ -234,7 +253,7 @@ public void DefaultWidthRequest() CreateViewHandler(avatarView); Size request = avatarView.Measure(double.PositiveInfinity, double.PositiveInfinity); - request.Width.Should().Be(AvatarViewDefaults.DefaultWidthRequest); + request.Width.Should().Be(AvatarViewDefaults.WidthRequest); } [Fact] diff --git a/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs b/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs index ccec8f309b..4c7a8cd2ed 100644 --- a/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs +++ b/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs @@ -36,7 +36,7 @@ public partial class AvatarView : Border, IAvatarView, IBorderElement, IFontElem { HorizontalTextAlignment = TextAlignment.Center, VerticalTextAlignment = TextAlignment.Center, - Text = AvatarViewDefaults.DefaultText, + Text = AvatarViewDefaults.Text, }; bool wasImageLoading; @@ -48,14 +48,14 @@ public AvatarView() IsEnabled = true; HorizontalOptions = VerticalOptions = LayoutOptions.Center; - HeightRequest = AvatarViewDefaults.DefaultHeightRequest; - WidthRequest = AvatarViewDefaults.DefaultWidthRequest; - Padding = AvatarViewDefaults.DefaultPadding; - Stroke = AvatarViewDefaults.DefaultBorderColor; - StrokeThickness = AvatarViewDefaults.DefaultBorderWidth; + HeightRequest = AvatarViewDefaults.HeightRequest; + WidthRequest = AvatarViewDefaults.WidthRequest; + Padding = AvatarViewDefaults.Padding; + Stroke = AvatarViewDefaults.BorderColor; + StrokeThickness = AvatarViewDefaults.BorderWidth; StrokeShape = new RoundRectangle { - CornerRadius = new CornerRadius(AvatarViewDefaults.DefaultCornerRadius.TopLeft, AvatarViewDefaults.DefaultCornerRadius.TopRight, AvatarViewDefaults.DefaultCornerRadius.BottomLeft, AvatarViewDefaults.DefaultCornerRadius.BottomRight), + CornerRadius = new CornerRadius(AvatarViewDefaults.CornerRadius.TopLeft, AvatarViewDefaults.CornerRadius.TopRight, AvatarViewDefaults.CornerRadius.BottomLeft, AvatarViewDefaults.CornerRadius.BottomRight), }; Content = avatarLabel; avatarImage.SetBinding(WidthRequestProperty, BindingBase.Create(static p => p.WidthRequest, source: this)); @@ -101,52 +101,52 @@ public double FontSize set => SetValue(FontElement.FontSizeProperty, value); } + /// Gets or sets a value of the control text colour property. + public Color TextColor + { + get => (Color)GetValue(TextElement.TextColorProperty); + set => SetValue(TextElement.TextColorProperty, value); + } + + /// + public TextTransform TextTransform + { + get => (TextTransform)GetValue(TextElement.TextTransformProperty); + set => SetValue(TextElement.TextTransformProperty, value); + } + /// Gets or sets a value of the control border colour. [BindableProperty(PropertyChangedMethodName = nameof(OnBorderColorPropertyChanged))] - public partial Color BorderColor { get; set; } = AvatarViewDefaults.DefaultBorderColor; + public partial Color BorderColor { get; set; } = AvatarViewDefaults.BorderColor; /// Gets or sets a value of the control border width. [BindableProperty(PropertyChangedMethodName = nameof(OnBorderWidthPropertyChanged))] - public partial double BorderWidth { get; set; } = AvatarViewDefaults.DefaultBorderWidth; + public partial double BorderWidth { get; set; } = AvatarViewDefaults.BorderWidth; /// Gets or sets a value of the control corner radius property. [BindableProperty(PropertyChangedMethodName = nameof(OnCornerRadiusPropertyChanged))] - public partial CornerRadius CornerRadius { get; set; } = AvatarViewDefaults.DefaultCornerRadius; + public partial CornerRadius CornerRadius { get; set; } = AvatarViewDefaults.CornerRadius; /// Gets or sets a value of the control image source property. [TypeConverter(typeof(ImageSourceConverter))] [BindableProperty(PropertyChangedMethodName = nameof(OnImageSourcePropertyChanged))] - public partial ImageSource ImageSource { get; set; } + public partial ImageSource? ImageSource { get; set; } /// Gets or sets a value of the control text property. [BindableProperty(PropertyChangedMethodName = nameof(OnTextPropertyChanged))] - public partial string Text { get; set; } = AvatarViewDefaults.DefaultText; - - /// Gets or sets a value of the control text colour property. - public Color TextColor - { - get => (Color)GetValue(TextElement.TextColorProperty); - set => SetValue(TextElement.TextColorProperty, value); - } - - /// - public TextTransform TextTransform - { - get => (TextTransform)GetValue(TextElement.TextTransformProperty); - set => SetValue(TextElement.TextTransformProperty, value); - } + public partial string Text { get; set; } = AvatarViewDefaults.Text; Aspect Microsoft.Maui.IImage.Aspect => ((IImageElement)this).Aspect; Aspect IImageElement.Aspect => avatarImage.Aspect; - Color IBorderElement.BorderColorDefaultValue => AvatarViewDefaults.DefaultBorderColor; + Color IBorderElement.BorderColorDefaultValue => AvatarViewDefaults.BorderColor; - double IBorderElement.BorderWidthDefaultValue => AvatarViewDefaults.DefaultBorderWidth; + double IBorderElement.BorderWidthDefaultValue => AvatarViewDefaults.BorderWidth; int IBorderElement.CornerRadius => (int)GetAverageCorderRadius(CornerRadius); - int IBorderElement.CornerRadiusDefaultValue => (int)GetAverageCorderRadius(AvatarViewDefaults.DefaultCornerRadius); + int IBorderElement.CornerRadiusDefaultValue => (int)GetAverageCorderRadius(AvatarViewDefaults.CornerRadius); TextAlignment ITextAlignment.HorizontalTextAlignment => ((ITextAlignmentElement)this).HorizontalTextAlignment; @@ -168,7 +168,7 @@ public TextTransform TextTransform double ILineHeightElement.LineHeight => avatarLabel.LineHeight; - IImageSource IImageSourcePart.Source => ImageSource; + IImageSource? IImageSourcePart.Source => ImageSource; ImageSource IImageElement.Source => avatarImage.Source;