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.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs index d12688ef2f..593bcabf7f 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -652,4 +652,77 @@ public partial class {{defaultTestClassName}} await VerifySourceGeneratorAsync(source, expectedGenerated); } + + + [Fact] + public async Task GenerateBindableProperty_WithInitializer_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + using System; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindablePropertyAttribute] + public partial string Text { get; set; } = "Initial Value"; + + [BindablePropertyAttribute] + public partial TimeSpan CustomDuration { get; set; } = TimeSpan.FromSeconds(30); + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // 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, __{{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, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultCustomDuration); + public partial System.TimeSpan CustomDuration { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } + } + + file static class __{{defaultTestClassName}}BindablePropertyInitHelpers + { + public static bool IsInitializingText = false; + public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) + { + IsInitializingText = true; + var defaultValue = ((TestView)bindable).Text; + IsInitializingText = false; + return defaultValue; + } + + public static bool IsInitializingCustomDuration = false; + public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) + { + IsInitializingCustomDuration = true; + var defaultValue = ((TestView)bindable).CustomDuration; + IsInitializingCustomDuration = false; + return defaultValue; + } + } + """; + + 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..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.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs index 7738db8a20..f0df70603e 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,7 +89,10 @@ 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); + Assert.Equal(defaultValueCreatorMethodName, model.EffectiveDefaultValueCreatorMethodName); + Assert.Equal("IsInitializingTestProperty", model.InitializingPropertyName); } [Fact] @@ -128,7 +134,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 +163,80 @@ 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(); + + 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", + "null", + string.Empty, + true, + string.Empty, + hasInitializer + ); + + // Act + var effectiveDefaultValueCreatorMethodName = model.EffectiveDefaultValueCreatorMethodName; + + // Assert + 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 c4409da734..b38a572090 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -174,18 +174,43 @@ 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(); + + // 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) { if (info.IsReadOnlyBindableProperty) { - GenerateReadOnlyBindableProperty(sb, in info); + GenerateReadOnlyBindableProperty(sb, in info, fileStaticClassName); } else { - GenerateBindableProperty(sb, in info); + GenerateBindableProperty(sb, in info, fileStaticClassName); } - GenerateProperty(sb, in info); + if (info.ShouldUsePropertyInitializer) + { + // 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, fileStaticClassName); } sb.Append('}'); @@ -200,11 +225,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); @@ -237,8 +270,15 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper .Append(info.PropertyChangingMethodName) .Append(", ") .Append(info.CoerceValueMethodName) - .Append(", ") - .Append(info.DefaultValueCreatorMethodName) + .Append(", "); + + if (info.ShouldUsePropertyInitializer) + { + sb.Append(fileStaticClassName) + .Append('.'); + } + + sb.Append(info.EffectiveDefaultValueCreatorMethodName) .Append(");\n"); // Generate public BindableProperty from the key @@ -256,7 +296,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); @@ -293,15 +333,21 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel .Append(info.PropertyChangingMethodName) .Append(", ") .Append(info.CoerceValueMethodName) - .Append(", ") - .Append(info.DefaultValueCreatorMethodName) - .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); @@ -312,7 +358,24 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info) .Append(formattedReturnType) .Append(' ') .Append(sanitizedPropertyName) - .Append("\n{\nget => (") + .Append("\n{\nget => "); + + if (info.HasInitializer) + { + if (info.ShouldUsePropertyInitializer) + { + // Now reference the static flag on the file static helper class + sb.Append(fileStaticClassName).Append(".").Append(info.InitializingPropertyName); + } + else + { + sb.Append("false"); + } + + sb.Append(" ? field : "); + } + + sb.Append("(") .Append(formattedReturnType) .Append(")GetValue(") .Append(info.BindablePropertyName) @@ -335,6 +398,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) { @@ -361,7 +425,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)); } @@ -458,7 +522,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) { @@ -474,7 +538,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)] @@ -531,4 +595,46 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) return typeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); } } + + /// + /// Appends the initializing flag into the file-static helper class. + /// + /// Helper StringBuilder used to collect helper members. + /// Property model. + 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. + fileStaticClassStringBuilder.Append("public static bool ") + .Append(info.InitializingPropertyName) + .Append(" = false;\n"); + } + + /// + /// 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. + /// + /// Helper StringBuilder used to collect helper members. + /// Property model. + /// Declaring type including containing types and generic parameters. + static void AppendHelperDefaultValueMethod(StringBuilder fileStaticClassStringBuilder, in BindablePropertyModel info, string fullDeclaringType) + { + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; + + fileStaticClassStringBuilder.Append("public static object ") + .Append(info.EffectiveDefaultValueCreatorMethodName) + .Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n") + .Append("{\n") + .Append(info.InitializingPropertyName) + .Append(" = true;\n") + .Append("var defaultValue = ((") + .Append(fullDeclaringType) + .Append(")bindable).") + .Append(sanitizedPropertyName) + .Append(";\n") + .Append(info.InitializingPropertyName) + .Append(" = false;\n") + .Append("return defaultValue;\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 e2ead63c39..ddc946229b 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -3,10 +3,15 @@ namespace CommunityToolkit.Maui.SourceGenerators.Internal.Models; -public 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) +public 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 && 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 => $"IsInitializing{PropertyName}"; + } public record SemanticValues(ClassInformation ClassInformation, EquatableArray BindableProperties); 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/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 diff --git a/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs b/src/CommunityToolkit.Maui/Views/AvatarView.shared.cs index 37ecc433ea..4c7a8cd2ed 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; @@ -51,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; @@ -63,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)); @@ -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,21 +101,6 @@ public double FontSize set => SetValue(FontElement.FontSizeProperty, value); } - /// 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); - } - - /// Gets or sets a value of the control text property. - public string Text - { - get => (string)GetValue(TextProperty); - set => SetValue(TextProperty, value); - } - /// Gets or sets a value of the control text colour property. public Color TextColor { @@ -166,17 +115,38 @@ public TextTransform TextTransform 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.BorderColor; + + /// Gets or sets a value of the control border width. + [BindableProperty(PropertyChangedMethodName = nameof(OnBorderWidthPropertyChanged))] + 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.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; } + + /// Gets or sets a value of the control text property. + [BindableProperty(PropertyChangedMethodName = nameof(OnTextPropertyChanged))] + public partial string Text { get; set; } = AvatarViewDefaults.Text; + Aspect Microsoft.Maui.IImage.Aspect => ((IImageElement)this).Aspect; Aspect IImageElement.Aspect => avatarImage.Aspect; - Color IBorderElement.BorderColorDefaultValue => (Color)BorderColorProperty.DefaultValue; + Color IBorderElement.BorderColorDefaultValue => AvatarViewDefaults.BorderColor; - double IBorderElement.BorderWidthDefaultValue => (double)BorderWidthProperty.DefaultValue; + double IBorderElement.BorderWidthDefaultValue => AvatarViewDefaults.BorderWidth; int IBorderElement.CornerRadius => (int)GetAverageCorderRadius(CornerRadius); - int IBorderElement.CornerRadiusDefaultValue => (int)GetAverageCorderRadius((CornerRadius)CornerRadiusProperty.DefaultValue); + int IBorderElement.CornerRadiusDefaultValue => (int)GetAverageCorderRadius(AvatarViewDefaults.CornerRadius); TextAlignment ITextAlignment.HorizontalTextAlignment => ((ITextAlignmentElement)this).HorizontalTextAlignment; @@ -198,7 +168,7 @@ public TextTransform TextTransform double ILineHeightElement.LineHeight => avatarLabel.LineHeight; - IImageSource IImageSourcePart.Source => ImageSource; + IImageSource? IImageSourcePart.Source => ImageSource; ImageSource IImageElement.Source => avatarImage.Source; @@ -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; 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;