Skip to content

Commit 8bd0af3

Browse files
Refactor Animations to use [BindableProperty] partial properties (#3013)
* Refactor to use [BindableProperty] partial properties Refactored BaseAnimation and FadeAnimation to define bindable properties using the [BindableProperty] attribute and partial properties. Removed manual BindableProperty fields and wrappers, specifying default values and value creators via attributes and static methods. Updated property summaries for clarity and marked BaseAnimation as partial. This simplifies and modernizes the property implementation. * Increase Maui Controls Version * Use Class Name without generics for `fileStaticClassName ` * Add BaseAnimationDefaults * Fix Internal Source Generator to work with Generic classes with Initializers and add test for it * Add `EditorBrowsableState.Never`, Rename `bindablePropertyInitHelpersClassName` * Use `DefaultValueCreatorMethodName` as required for `defaultLength` constructor parameter * Create `MockBaseAnimation` to test default values * Add `FadeAnimationDefaults` * Add `EnsureDefaults` Test * Add `volatile` to InitializingPropertyName * Add `volatile` and `[EditorBrowsable]` to unit tests --------- Co-authored-by: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com>
1 parent a9c3d76 commit 8bd0af3

File tree

10 files changed

+214
-66
lines changed

10 files changed

+214
-66
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace CommunityToolkit.Maui.Core;
2+
3+
static class BaseAnimationDefaults
4+
{
5+
public const uint Length = 250u;
6+
public static Easing Easing { get; } = Easing.Linear;
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace CommunityToolkit.Maui.Core;
2+
3+
static class FadeAnimationDefaults
4+
{
5+
public const uint Length = 300u;
6+
public const double Opacity = 0.3;
7+
}

src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/CommonUsageTests.cs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ public partial class {{defaultTestClassName}}
610610
611611
file static class __{{defaultTestClassName}}BindablePropertyInitHelpers
612612
{
613-
public static bool IsInitializingText = false;
613+
public static volatile bool IsInitializingText = false;
614614
public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable)
615615
{
616616
IsInitializingText = true;
@@ -619,7 +619,7 @@ public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableO
619619
return defaultValue;
620620
}
621621
622-
public static bool IsInitializingCustomDuration = false;
622+
public static volatile bool IsInitializingCustomDuration = false;
623623
public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable)
624624
{
625625
IsInitializingCustomDuration = true;
@@ -632,4 +632,45 @@ public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls
632632

633633
await VerifySourceGeneratorAsync(source, expectedGenerated);
634634
}
635+
636+
[Fact]
637+
public async Task GenerateBindableProperty_GenericClassExample_GeneratesCorrectCode()
638+
{
639+
const string source =
640+
/* language=C#-test */
641+
//lang=csharp
642+
$$"""
643+
using CommunityToolkit.Maui;
644+
using Microsoft.Maui.Controls;
645+
646+
namespace {{defaultTestNamespace}};
647+
648+
public partial class {{defaultTestClassName}}<T> : View
649+
{
650+
[BindablePropertyAttribute]
651+
public partial string Text { get; set; }
652+
}
653+
""";
654+
655+
const string expectedGenerated =
656+
/* language=C#-test */
657+
//lang=csharp
658+
$$"""
659+
// <auto-generated>
660+
// See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
661+
#pragma warning disable
662+
#nullable enable
663+
namespace {{defaultTestNamespace}};
664+
public partial class {{defaultTestClassName}}<T>
665+
{
666+
/// <summary>
667+
/// Backing BindableProperty for the <see cref = "Text"/> property.
668+
/// </summary>
669+
public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}<T>), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null);
670+
public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
671+
}
672+
""";
673+
674+
await VerifySourceGeneratorAsync(source, expectedGenerated);
675+
}
635676
}

src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public partial class {{defaultTestClassName}}
100100
101101
file static class __{{defaultTestClassName}}BindablePropertyInitHelpers
102102
{
103-
public static bool IsInitializingInvoiceStatus = false;
103+
public static volatile bool IsInitializingInvoiceStatus = false;
104104
public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable)
105105
{
106106
IsInitializingInvoiceStatus = true;
@@ -160,7 +160,7 @@ public partial class {{defaultTestClassName}}
160160
161161
file static class __{{defaultTestClassName}}BindablePropertyInitHelpers
162162
{
163-
public static bool IsInitializingInvoiceStatus = false;
163+
public static volatile bool IsInitializingInvoiceStatus = false;
164164
public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable)
165165
{
166166
IsInitializingInvoiceStatus = true;
@@ -516,7 +516,7 @@ public partial class {{defaultTestClassName}}
516516
517517
file static class __{{defaultTestClassName}}BindablePropertyInitHelpers
518518
{
519-
public static bool IsInitializingIsEnabled = false;
519+
public static volatile bool IsInitializingIsEnabled = false;
520520
public static object CreateDefaultIsEnabled(global::Microsoft.Maui.Controls.BindableObject bindable)
521521
{
522522
IsInitializingIsEnabled = true;
@@ -525,7 +525,7 @@ public static object CreateDefaultIsEnabled(global::Microsoft.Maui.Controls.Bind
525525
return defaultValue;
526526
}
527527
528-
public static bool IsInitializingPi = false;
528+
public static volatile bool IsInitializingPi = false;
529529
public static object CreateDefaultPi(global::Microsoft.Maui.Controls.BindableObject bindable)
530530
{
531531
IsInitializingPi = true;
@@ -534,7 +534,7 @@ public static object CreateDefaultPi(global::Microsoft.Maui.Controls.BindableObj
534534
return defaultValue;
535535
}
536536
537-
public static bool IsInitializingLetter = false;
537+
public static volatile bool IsInitializingLetter = false;
538538
public static object CreateDefaultLetter(global::Microsoft.Maui.Controls.BindableObject bindable)
539539
{
540540
IsInitializingLetter = true;
@@ -543,7 +543,7 @@ public static object CreateDefaultLetter(global::Microsoft.Maui.Controls.Bindabl
543543
return defaultValue;
544544
}
545545
546-
public static bool IsInitializingTimeSpent = false;
546+
public static volatile bool IsInitializingTimeSpent = false;
547547
public static object CreateDefaultTimeSpent(global::Microsoft.Maui.Controls.BindableObject bindable)
548548
{
549549
IsInitializingTimeSpent = true;
@@ -552,7 +552,7 @@ public static object CreateDefaultTimeSpent(global::Microsoft.Maui.Controls.Bind
552552
return defaultValue;
553553
}
554554
555-
public static bool IsInitializingDoubleEpsilon = false;
555+
public static volatile bool IsInitializingDoubleEpsilon = false;
556556
public static object CreateDefaultDoubleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable)
557557
{
558558
IsInitializingDoubleEpsilon = true;
@@ -561,7 +561,7 @@ public static object CreateDefaultDoubleEpsilon(global::Microsoft.Maui.Controls.
561561
return defaultValue;
562562
}
563563
564-
public static bool IsInitializingSingleEpsilon = false;
564+
public static volatile bool IsInitializingSingleEpsilon = false;
565565
public static object CreateDefaultSingleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable)
566566
{
567567
IsInitializingSingleEpsilon = true;
@@ -570,7 +570,7 @@ public static object CreateDefaultSingleEpsilon(global::Microsoft.Maui.Controls.
570570
return defaultValue;
571571
}
572572
573-
public static bool IsInitializingCurrentTime = false;
573+
public static volatile bool IsInitializingCurrentTime = false;
574574
public static object CreateDefaultCurrentTime(global::Microsoft.Maui.Controls.BindableObject bindable)
575575
{
576576
IsInitializingCurrentTime = true;

src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,60 @@ public partial class {{defaultTestClassName}}<T, U>
134134
await VerifySourceGeneratorAsync(source, expectedGenerated);
135135
}
136136

137+
[Fact]
138+
public async Task GenerateBindableProperty_GenericClass_WithInitializer_GeneratesCorrectCode()
139+
{
140+
const string source =
141+
/* language=C#-test */
142+
//lang=csharp
143+
$$"""
144+
using CommunityToolkit.Maui;
145+
using Microsoft.Maui.Controls;
146+
147+
namespace {{defaultTestNamespace}};
148+
149+
public partial class {{defaultTestClassName}}<T,U> : View where T : class
150+
{
151+
[BindableProperty]
152+
public partial T? Value { get; set; } = default;
153+
}
154+
""";
155+
156+
const string expectedGenerated =
157+
/* language=C#-test */
158+
//lang=csharp
159+
$$"""
160+
// <auto-generated>
161+
// See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
162+
#pragma warning disable
163+
#nullable enable
164+
namespace {{defaultTestNamespace}};
165+
public partial class {{defaultTestClassName}}<T, U>
166+
{
167+
/// <summary>
168+
/// Backing BindableProperty for the <see cref = "Value"/> property.
169+
/// </summary>
170+
public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}<T, U>), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultValue);
171+
public partial T? Value { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingValue ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); }
172+
173+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
174+
private static class __{{defaultTestClassName}}BindablePropertyInitHelpers
175+
{
176+
public static volatile bool IsInitializingValue = false;
177+
public static object CreateDefaultValue(global::Microsoft.Maui.Controls.BindableObject bindable)
178+
{
179+
IsInitializingValue = true;
180+
var defaultValue = (({{defaultTestClassName}}<T, U>)bindable).Value;
181+
IsInitializingValue = false;
182+
return defaultValue;
183+
}
184+
}
185+
}
186+
""";
187+
188+
await VerifySourceGeneratorAsync(source, expectedGenerated);
189+
}
190+
137191
[Fact]
138192
public async Task GenerateBindableProperty_NestedClass_GeneratesCorrectCode()
139193
{

src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,17 +182,17 @@ static string GenerateSource(SemanticValues value)
182182
? classNameWithGenerics
183183
: string.Concat(value.ClassInformation.ContainingTypes, ".", classNameWithGenerics);
184184

185-
var fileStaticClassName = $"__{classNameWithGenerics}BindablePropertyInitHelpers";
185+
var bindablePropertyInitHelpersClassName = $"__{value.ClassInformation.ClassName}BindablePropertyInitHelpers";
186186

187187
foreach (var info in value.BindableProperties)
188188
{
189189
if (info.IsReadOnlyBindableProperty)
190190
{
191-
GenerateReadOnlyBindableProperty(sb, in info, fileStaticClassName);
191+
GenerateReadOnlyBindableProperty(sb, in info, bindablePropertyInitHelpersClassName);
192192
}
193193
else
194194
{
195-
GenerateBindableProperty(sb, in info, fileStaticClassName);
195+
GenerateBindableProperty(sb, in info, bindablePropertyInitHelpersClassName);
196196
}
197197

198198
if (info.ShouldUsePropertyInitializer)
@@ -209,7 +209,20 @@ static string GenerateSource(SemanticValues value)
209209
}
210210
}
211211

212-
GenerateProperty(sb, in info, fileStaticClassName);
212+
GenerateProperty(sb, in info, bindablePropertyInitHelpersClassName);
213+
}
214+
215+
// If we generated any helper members and the declaring class is generic,
216+
// emit the helper class nested inside the generated partial class so
217+
// generic type parameters are in scope for casts used by the helper.
218+
if (fileStaticClassStringBuilder.Length > 0 && !string.IsNullOrEmpty(value.ClassInformation.GenericTypeParameters))
219+
{
220+
sb.Append("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
221+
sb.Append("\n");
222+
sb.Append("private static class ").Append(bindablePropertyInitHelpersClassName).Append("\n{");
223+
sb.Append("\n");
224+
sb.Append(fileStaticClassStringBuilder.ToString());
225+
sb.Append("}\n\n");
213226
}
214227

215228
sb.Append('}');
@@ -224,10 +237,12 @@ static string GenerateSource(SemanticValues value)
224237
}
225238
}
226239

227-
// If we generated any helper members, emit a file static class with them.
228-
if (fileStaticClassStringBuilder.Length > 0)
240+
// If we generated any helper members and the declaring class is not generic,
241+
// emit a file static class with them. Generic types have their helpers emitted
242+
// nested inside the class above to ensure type parameter scope.
243+
if (fileStaticClassStringBuilder.Length > 0 && string.IsNullOrEmpty(value.ClassInformation.GenericTypeParameters))
229244
{
230-
sb.Append("\n\nfile static class ").Append(fileStaticClassName).Append("\n{\n");
245+
sb.Append("\n\nfile static class ").Append(bindablePropertyInitHelpersClassName).Append("\n{\n");
231246
sb.Append(fileStaticClassStringBuilder.ToString());
232247
sb.Append("}\n");
233248
}
@@ -598,7 +613,7 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol)
598613
static void AppendHelperInitializingField(StringBuilder fileStaticClassStringBuilder, in BindablePropertyModel info)
599614
{
600615
// Make the flag public static so it can be referenced from the generated partial class in the same file.
601-
fileStaticClassStringBuilder.Append("public static bool ")
616+
fileStaticClassStringBuilder.Append("public static volatile bool ")
602617
.Append(info.InitializingPropertyName)
603618
.Append(" = false;\n");
604619
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using CommunityToolkit.Maui.Animations;
2+
using CommunityToolkit.Maui.Core;
3+
using Xunit;
4+
5+
namespace CommunityToolkit.Maui.UnitTests.Animations;
6+
7+
public class BaseAnimationTests
8+
{
9+
[Fact]
10+
public void BaseAnimationT_EnsureDefaults()
11+
{
12+
// Arrange
13+
BaseAnimation animation = new MockBaseAnimation();
14+
15+
// Act // Assert
16+
Assert.Equal(BaseAnimationDefaults.Easing, animation.Easing);
17+
Assert.Equal(BaseAnimationDefaults.Length, animation.Length);
18+
}
19+
20+
[Fact]
21+
public void BaseAnimation_EnsureDefaults()
22+
{
23+
// Arrange
24+
BaseAnimation<VisualElement> animation = new MockBaseAnimation();
25+
26+
// Act // Assert
27+
Assert.Equal(BaseAnimationDefaults.Easing, animation.Easing);
28+
Assert.Equal(BaseAnimationDefaults.Length, animation.Length);
29+
}
30+
31+
class MockBaseAnimation : BaseAnimation
32+
{
33+
public override Task Animate(VisualElement view, CancellationToken token = default)
34+
{
35+
throw new NotImplementedException();
36+
}
37+
}
38+
}

src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using CommunityToolkit.Maui.Animations;
2+
using CommunityToolkit.Maui.Core;
23
using CommunityToolkit.Maui.UnitTests.Mocks;
34
using FluentAssertions;
45
using Xunit;
@@ -67,4 +68,16 @@ public async Task AnimateShouldReturnToOriginalOpacity()
6768

6869
label.Opacity.Should().Be(0.9);
6970
}
71+
72+
[Fact]
73+
public void EnsureDefaults()
74+
{
75+
// Arrange
76+
var animation = new FadeAnimation();
77+
78+
// Act // Assert
79+
Assert.Equal(BaseAnimationDefaults.Easing, animation.Easing);
80+
Assert.Equal(FadeAnimationDefaults.Length, animation.Length);
81+
Assert.Equal(FadeAnimationDefaults.Opacity, animation.Opacity);
82+
}
7083
}

0 commit comments

Comments
 (0)