diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/BaseAnimationDefaults.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/BaseAnimationDefaults.cs new file mode 100644 index 0000000000..461c21b487 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/BaseAnimationDefaults.cs @@ -0,0 +1,7 @@ +namespace CommunityToolkit.Maui.Core; + +static class BaseAnimationDefaults +{ + public const uint Length = 250u; + public static Easing Easing { get; } = Easing.Linear; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/FadeAnimationDefaults.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/FadeAnimationDefaults.cs new file mode 100644 index 0000000000..b67dbe5dac --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/FadeAnimationDefaults.cs @@ -0,0 +1,7 @@ +namespace CommunityToolkit.Maui.Core; + +static class FadeAnimationDefaults +{ + public const uint Length = 300u; + public const double Opacity = 0.3; +} \ 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 6edfee7d07..ef0240e7e5 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs @@ -610,7 +610,7 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static bool IsInitializingText = false; + public static volatile bool IsInitializingText = false; public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingText = true; @@ -619,7 +619,7 @@ public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableO return defaultValue; } - public static bool IsInitializingCustomDuration = false; + public static volatile bool IsInitializingCustomDuration = false; public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingCustomDuration = true; @@ -632,4 +632,45 @@ public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls await VerifySourceGeneratorAsync(source, expectedGenerated); } + + [Fact] + public async Task GenerateBindableProperty_GenericClassExample_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; } + } + """; + + 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, null); + public partial string Text { get => (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/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs index 5883995f53..bf2203034b 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs @@ -100,7 +100,7 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static bool IsInitializingInvoiceStatus = false; + public static volatile bool IsInitializingInvoiceStatus = false; public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingInvoiceStatus = true; @@ -160,7 +160,7 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static bool IsInitializingInvoiceStatus = false; + public static volatile bool IsInitializingInvoiceStatus = false; public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingInvoiceStatus = true; @@ -516,7 +516,7 @@ public partial class {{defaultTestClassName}} file static class __{{defaultTestClassName}}BindablePropertyInitHelpers { - public static bool IsInitializingIsEnabled = false; + public static volatile bool IsInitializingIsEnabled = false; public static object CreateDefaultIsEnabled(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingIsEnabled = true; @@ -525,7 +525,7 @@ public static object CreateDefaultIsEnabled(global::Microsoft.Maui.Controls.Bind return defaultValue; } - public static bool IsInitializingPi = false; + public static volatile bool IsInitializingPi = false; public static object CreateDefaultPi(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingPi = true; @@ -534,7 +534,7 @@ public static object CreateDefaultPi(global::Microsoft.Maui.Controls.BindableObj return defaultValue; } - public static bool IsInitializingLetter = false; + public static volatile bool IsInitializingLetter = false; public static object CreateDefaultLetter(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingLetter = true; @@ -543,7 +543,7 @@ public static object CreateDefaultLetter(global::Microsoft.Maui.Controls.Bindabl return defaultValue; } - public static bool IsInitializingTimeSpent = false; + public static volatile bool IsInitializingTimeSpent = false; public static object CreateDefaultTimeSpent(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingTimeSpent = true; @@ -552,7 +552,7 @@ public static object CreateDefaultTimeSpent(global::Microsoft.Maui.Controls.Bind return defaultValue; } - public static bool IsInitializingDoubleEpsilon = false; + public static volatile bool IsInitializingDoubleEpsilon = false; public static object CreateDefaultDoubleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingDoubleEpsilon = true; @@ -561,7 +561,7 @@ public static object CreateDefaultDoubleEpsilon(global::Microsoft.Maui.Controls. return defaultValue; } - public static bool IsInitializingSingleEpsilon = false; + public static volatile bool IsInitializingSingleEpsilon = false; public static object CreateDefaultSingleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingSingleEpsilon = true; @@ -570,7 +570,7 @@ public static object CreateDefaultSingleEpsilon(global::Microsoft.Maui.Controls. return defaultValue; } - public static bool IsInitializingCurrentTime = false; + public static volatile bool IsInitializingCurrentTime = false; public static object CreateDefaultCurrentTime(global::Microsoft.Maui.Controls.BindableObject bindable) { IsInitializingCurrentTime = true; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs index 3df8bd95cc..629707eb6a 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs @@ -134,6 +134,60 @@ public partial class {{defaultTestClassName}} await VerifySourceGeneratorAsync(source, expectedGenerated); } + [Fact] + public async Task GenerateBindableProperty_GenericClass_WithInitializer_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View where T : class + { + [BindableProperty] + public partial T? Value { get; set; } = default; + } + """; + + 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 ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultValue); + public partial T? Value { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingValue ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + private static class __{{defaultTestClassName}}BindablePropertyInitHelpers + { + public static volatile bool IsInitializingValue = false; + public static object CreateDefaultValue(global::Microsoft.Maui.Controls.BindableObject bindable) + { + IsInitializingValue = true; + var defaultValue = (({{defaultTestClassName}})bindable).Value; + IsInitializingValue = false; + return defaultValue; + } + } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + [Fact] public async Task GenerateBindableProperty_NestedClass_GeneratesCorrectCode() { diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs index 85fc81820d..c03c378828 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -182,17 +182,17 @@ static string GenerateSource(SemanticValues value) ? classNameWithGenerics : string.Concat(value.ClassInformation.ContainingTypes, ".", classNameWithGenerics); - var fileStaticClassName = $"__{classNameWithGenerics}BindablePropertyInitHelpers"; + var bindablePropertyInitHelpersClassName = $"__{value.ClassInformation.ClassName}BindablePropertyInitHelpers"; foreach (var info in value.BindableProperties) { if (info.IsReadOnlyBindableProperty) { - GenerateReadOnlyBindableProperty(sb, in info, fileStaticClassName); + GenerateReadOnlyBindableProperty(sb, in info, bindablePropertyInitHelpersClassName); } else { - GenerateBindableProperty(sb, in info, fileStaticClassName); + GenerateBindableProperty(sb, in info, bindablePropertyInitHelpersClassName); } if (info.ShouldUsePropertyInitializer) @@ -209,7 +209,20 @@ static string GenerateSource(SemanticValues value) } } - GenerateProperty(sb, in info, fileStaticClassName); + GenerateProperty(sb, in info, bindablePropertyInitHelpersClassName); + } + + // If we generated any helper members and the declaring class is generic, + // emit the helper class nested inside the generated partial class so + // generic type parameters are in scope for casts used by the helper. + if (fileStaticClassStringBuilder.Length > 0 && !string.IsNullOrEmpty(value.ClassInformation.GenericTypeParameters)) + { + sb.Append("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + sb.Append("\n"); + sb.Append("private static class ").Append(bindablePropertyInitHelpersClassName).Append("\n{"); + sb.Append("\n"); + sb.Append(fileStaticClassStringBuilder.ToString()); + sb.Append("}\n\n"); } sb.Append('}'); @@ -224,10 +237,12 @@ static string GenerateSource(SemanticValues value) } } - // If we generated any helper members, emit a file static class with them. - if (fileStaticClassStringBuilder.Length > 0) + // If we generated any helper members and the declaring class is not generic, + // emit a file static class with them. Generic types have their helpers emitted + // nested inside the class above to ensure type parameter scope. + if (fileStaticClassStringBuilder.Length > 0 && string.IsNullOrEmpty(value.ClassInformation.GenericTypeParameters)) { - sb.Append("\n\nfile static class ").Append(fileStaticClassName).Append("\n{\n"); + sb.Append("\n\nfile static class ").Append(bindablePropertyInitHelpersClassName).Append("\n{\n"); sb.Append(fileStaticClassStringBuilder.ToString()); sb.Append("}\n"); } @@ -598,7 +613,7 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol) 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 ") + fileStaticClassStringBuilder.Append("public static volatile bool ") .Append(info.InitializingPropertyName) .Append(" = false;\n"); } diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/BaseAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/BaseAnimationTests.cs new file mode 100644 index 0000000000..07603c6ecc --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Animations/BaseAnimationTests.cs @@ -0,0 +1,38 @@ +using CommunityToolkit.Maui.Animations; +using CommunityToolkit.Maui.Core; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Animations; + +public class BaseAnimationTests +{ + [Fact] + public void BaseAnimationT_EnsureDefaults() + { + // Arrange + BaseAnimation animation = new MockBaseAnimation(); + + // Act // Assert + Assert.Equal(BaseAnimationDefaults.Easing, animation.Easing); + Assert.Equal(BaseAnimationDefaults.Length, animation.Length); + } + + [Fact] + public void BaseAnimation_EnsureDefaults() + { + // Arrange + BaseAnimation animation = new MockBaseAnimation(); + + // Act // Assert + Assert.Equal(BaseAnimationDefaults.Easing, animation.Easing); + Assert.Equal(BaseAnimationDefaults.Length, animation.Length); + } + + class MockBaseAnimation : BaseAnimation + { + public override Task Animate(VisualElement view, CancellationToken token = default) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs index be4a3abc33..1702c2244a 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs @@ -1,4 +1,5 @@ using CommunityToolkit.Maui.Animations; +using CommunityToolkit.Maui.Core; using CommunityToolkit.Maui.UnitTests.Mocks; using FluentAssertions; using Xunit; @@ -67,4 +68,16 @@ public async Task AnimateShouldReturnToOriginalOpacity() label.Opacity.Should().Be(0.9); } + + [Fact] + public void EnsureDefaults() + { + // Arrange + var animation = new FadeAnimation(); + + // Act // Assert + Assert.Equal(BaseAnimationDefaults.Easing, animation.Easing); + Assert.Equal(FadeAnimationDefaults.Length, animation.Length); + Assert.Equal(FadeAnimationDefaults.Opacity, animation.Opacity); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs index c5cee89798..487dc0616d 100644 --- a/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs +++ b/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs @@ -1,4 +1,6 @@ -namespace CommunityToolkit.Maui.Animations; +using CommunityToolkit.Maui.Core; + +namespace CommunityToolkit.Maui.Animations; /// /// Abstract class for animation types to inherit. /// @@ -7,40 +9,21 @@ /// Initialize BaseAnimation /// /// The default time, in milliseconds, over which to animate the transition -public abstract class BaseAnimation(uint defaultLength = 250u) : BindableObject where TAnimatable : IAnimatable +public abstract partial class BaseAnimation(uint defaultLength = BaseAnimationDefaults.Length) : BindableObject where TAnimatable : IAnimatable { readonly uint defaultLength = defaultLength; /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty LengthProperty = - BindableProperty.Create(nameof(Length), typeof(uint), typeof(BaseAnimation), 250u, - BindingMode.OneWay, defaultValueCreator: bindable => ((BaseAnimation)bindable).defaultLength); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty EasingProperty = - BindableProperty.Create(nameof(Easing), typeof(Easing), typeof(BaseAnimation), Easing.Linear, BindingMode.OneWay); - - /// - /// The time, in milliseconds, over which to animate the transition. + /// Gets or sets the time, in milliseconds, over which to animate the transition. /// - public uint Length - { - get => (uint)GetValue(LengthProperty); - set => SetValue(LengthProperty, value); - } + [BindableProperty(DefaultBindingMode = BindingMode.OneWay, DefaultValueCreatorMethodName = nameof(CreateLengthDefaultValue))] + public partial uint Length { get; set; } /// - /// The easing function to use for the animation + /// Gets or sets the easing function to use for the animation /// - public Easing Easing - { - get => (Easing)GetValue(EasingProperty); - set => SetValue(EasingProperty, value); - } + [BindableProperty(DefaultBindingMode = BindingMode.OneWay)] + public partial Easing Easing { get; set; } = BaseAnimationDefaults.Easing; /// /// Performs the animation on the View @@ -48,6 +31,8 @@ public Easing Easing /// The view to perform the animation on. /// . public abstract Task Animate(TAnimatable view, CancellationToken token = default); + + static object CreateLengthDefaultValue(BindableObject bindable) => ((BaseAnimation)bindable).defaultLength; } /// @@ -55,6 +40,6 @@ public Easing Easing /// Initialize BaseAnimation /// /// The default time, in milliseconds, over which to animate the transition -public abstract class BaseAnimation(uint defaultLength = 250u) : BaseAnimation(defaultLength) +public abstract class BaseAnimation(uint defaultLength = BaseAnimationDefaults.Length) : BaseAnimation(defaultLength) { } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs index f6420378a3..617167fe34 100644 --- a/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs +++ b/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs @@ -1,30 +1,18 @@ -namespace CommunityToolkit.Maui.Animations; +using CommunityToolkit.Maui.Core; + +namespace CommunityToolkit.Maui.Animations; /// /// Animation that will fade the supplied view to the specified /// and then back to its original . /// -public partial class FadeAnimation() : BaseAnimation(300) +public partial class FadeAnimation() : BaseAnimation(FadeAnimationDefaults.Length) { /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty OpacityProperty = - BindableProperty.Create( - nameof(Opacity), - typeof(double), - typeof(FadeAnimation), - 0.3, - BindingMode.TwoWay); - - /// - /// Gets or sets the opacity to fade to before returning to the elements current Opacity. + /// Gets or sets the opacity to fade to before returning to the elements current . /// - public double Opacity - { - get => (double)GetValue(OpacityProperty); - set => SetValue(OpacityProperty, value); - } + [BindableProperty(DefaultBindingMode = BindingMode.TwoWay)] + public partial double Opacity { get; set; } = FadeAnimationDefaults.Opacity; /// public override async Task Animate(VisualElement view, CancellationToken token = default)