Skip to content

Commit 77b3fa5

Browse files
Move out-of-scope variables to file static class
1 parent 3a642d0 commit 77b3fa5

File tree

4 files changed

+132
-62
lines changed

4 files changed

+132
-62
lines changed

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

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -691,32 +691,35 @@ public partial class {{defaultTestClassName}}
691691
/// <summary>
692692
/// Backing BindableProperty for the <see cref = "Text"/> property.
693693
/// </summary>
694-
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);
695-
bool __initializingText = false;
696-
static object __createDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable)
697-
{
698-
(({{defaultTestClassName}})bindable).__initializingText = true;
699-
var defaultValue = (({{defaultTestClassName}})bindable).Text;
700-
(({{defaultTestClassName}})bindable).__initializingText = false;
701-
return defaultValue;
702-
}
703-
704-
public partial string Text { get => __initializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
694+
public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __BindablePropertyInitHelpers.CreateDefaultText);
695+
public partial string Text { get => __BindablePropertyInitHelpers.IsInitializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
705696
706697
/// <summary>
707698
/// Backing BindableProperty for the <see cref = "CustomDuration"/> property.
708699
/// </summary>
709-
public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __createDefaultCustomDuration);
710-
bool __initializingCustomDuration = false;
711-
static object __createDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable)
700+
public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, __BindablePropertyInitHelpers.CreateDefaultCustomDuration);
701+
public partial System.TimeSpan CustomDuration { get => __BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); }
702+
}
703+
704+
file static class __BindablePropertyInitHelpers
705+
{
706+
public static bool IsInitializingText = false;
707+
public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable)
712708
{
713-
(({{defaultTestClassName}})bindable).__initializingCustomDuration = true;
714-
var defaultValue = (({{defaultTestClassName}})bindable).CustomDuration;
715-
(({{defaultTestClassName}})bindable).__initializingCustomDuration = false;
709+
IsInitializingText = true;
710+
var defaultValue = ((TestView)bindable).Text;
711+
IsInitializingText = false;
716712
return defaultValue;
717713
}
718714
719-
public partial System.TimeSpan CustomDuration { get => __initializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); }
715+
public static bool IsInitializingCustomDuration = false;
716+
public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable)
717+
{
718+
IsInitializingCustomDuration = true;
719+
var defaultValue = ((TestView)bindable).CustomDuration;
720+
IsInitializingCustomDuration = false;
721+
return defaultValue;
722+
}
720723
}
721724
""";
722725

src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues()
9292
Assert.Equal(hasInitializer, model.HasInitializer);
9393
Assert.Equal("TestPropertyProperty", model.BindablePropertyName);
9494
Assert.Equal(defaultValueCreatorMethodName, model.EffectiveDefaultValueCreatorMethodName);
95-
Assert.Equal("__initializingTestProperty", model.InitializingPropertyName);
95+
Assert.Equal("IsInitializingTestProperty", model.InitializingPropertyName);
9696
}
9797

9898
[Fact]
@@ -196,6 +196,41 @@ public void BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDefaultV
196196
var effectiveDefaultValueCreatorMethodName = model.EffectiveDefaultValueCreatorMethodName;
197197

198198
// Assert
199-
Assert.Equal("__createDefault" + propertyName, effectiveDefaultValueCreatorMethodName);
199+
Assert.Equal("CreateDefault" + propertyName, effectiveDefaultValueCreatorMethodName);
200+
}
201+
202+
[Fact]
203+
public void BindablePropertyName_WithInitializerAndDefaulValueCreator_ReturnsCorrectEffectiveDefaultValueCreatorMethodName()
204+
{
205+
// Arrange
206+
var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } = \"Initial Value\"; }");
207+
var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!;
208+
var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType<IPropertySymbol>().First();
209+
210+
const string propertyName = "TestProperty";
211+
const bool hasInitializer = true;
212+
213+
var model = new BindablePropertyModel(
214+
propertyName,
215+
propertySymbol.Type,
216+
typeSymbol,
217+
"null",
218+
"Microsoft.Maui.Controls.BindingMode.OneWay",
219+
"null",
220+
"null",
221+
"null",
222+
"null",
223+
"CreateTextDefaultValue",
224+
string.Empty,
225+
true,
226+
string.Empty,
227+
hasInitializer
228+
);
229+
230+
// Act
231+
var effectiveDefaultValueCreatorMethodName = model.EffectiveDefaultValueCreatorMethodName;
232+
233+
// Assert
234+
Assert.Equal("CreateTextDefaultValue", effectiveDefaultValueCreatorMethodName);
200235
}
201236
}

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

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -174,24 +174,42 @@ static string GenerateSource(SemanticValues value)
174174

175175
sb.Append(value.ClassInformation.DeclaredAccessibility).Append(" partial class ").Append(classNameWithGenerics).Append("\n{\n\n");
176176

177+
// Prepare helper builder for file-static class members (static flags + default creators)
178+
var fileStaticClassStringBuilder = new StringBuilder(256);
179+
var helperNames = new HashSet<string>();
180+
const string fileStaticClassName = "__BindablePropertyInitHelpers";
181+
182+
// Build fully-qualified declaring type name for helper method casts (include containing types)
183+
var fullDeclaringType = string.IsNullOrEmpty(value.ClassInformation.ContainingTypes)
184+
? classNameWithGenerics
185+
: string.Concat(value.ClassInformation.ContainingTypes, ".", classNameWithGenerics);
186+
177187
foreach (var info in value.BindableProperties)
178188
{
179189
if (info.IsReadOnlyBindableProperty)
180190
{
181-
GenerateReadOnlyBindableProperty(sb, in info);
191+
GenerateReadOnlyBindableProperty(sb, in info, fileStaticClassName);
182192
}
183193
else
184194
{
185-
GenerateBindableProperty(sb, in info);
195+
GenerateBindableProperty(sb, in info, fileStaticClassName);
186196
}
187197

188198
if (info.ShouldUsePropertyInitializer)
189199
{
190-
GenerateInitializingProperty(sb, in info);
191-
GenerateDefaultValueMethod(sb, in info, classNameWithGenerics);
200+
// Generate only references within the class; actual static field and creator method
201+
// will be placed inside the file static helper class below.
202+
if (helperNames.Add(info.InitializingPropertyName))
203+
{
204+
AppendHelperInitializingField(fileStaticClassStringBuilder, in info);
205+
}
206+
if (helperNames.Add(info.EffectiveDefaultValueCreatorMethodName))
207+
{
208+
AppendHelperDefaultValueMethod(fileStaticClassStringBuilder, in info, fullDeclaringType);
209+
}
192210
}
193211

194-
GenerateProperty(sb, in info);
212+
GenerateProperty(sb, in info, fileStaticClassName);
195213
}
196214

197215
sb.Append('}');
@@ -206,11 +224,19 @@ static string GenerateSource(SemanticValues value)
206224
}
207225
}
208226

227+
// If we generated any helper members, emit a file static class with them.
228+
if (fileStaticClassStringBuilder.Length > 0)
229+
{
230+
sb.Append("\n\nfile static class ").Append(fileStaticClassName).Append("\n{\n");
231+
sb.Append(fileStaticClassStringBuilder.ToString());
232+
sb.Append("}\n");
233+
}
234+
209235
return sb.ToString();
210236
}
211237

212238
[MethodImpl(MethodImplOptions.AggressiveInlining)]
213-
static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindablePropertyModel info)
239+
static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindablePropertyModel info, in string fileStaticClassName)
214240
{
215241
// Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator
216242
var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType);
@@ -243,8 +269,15 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper
243269
.Append(info.PropertyChangingMethodName)
244270
.Append(", ")
245271
.Append(info.CoerceValueMethodName)
246-
.Append(", ")
247-
.Append(info.EffectiveDefaultValueCreatorMethodName)
272+
.Append(", ");
273+
274+
if (info.ShouldUsePropertyInitializer)
275+
{
276+
sb.Append(fileStaticClassName)
277+
.Append('.');
278+
}
279+
280+
sb.Append(info.EffectiveDefaultValueCreatorMethodName)
248281
.Append(");\n");
249282

250283
// Generate public BindableProperty from the key
@@ -262,7 +295,7 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper
262295
}
263296

264297
[MethodImpl(MethodImplOptions.AggressiveInlining)]
265-
static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel info)
298+
static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel info, in string helperClassName)
266299
{
267300
// Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator
268301
var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType);
@@ -299,15 +332,21 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel
299332
.Append(info.PropertyChangingMethodName)
300333
.Append(", ")
301334
.Append(info.CoerceValueMethodName)
302-
.Append(", ")
303-
.Append(info.EffectiveDefaultValueCreatorMethodName)
304-
.Append(");\n");
335+
.Append(", ");
305336

306-
sb.Append('\n');
337+
if (info.ShouldUsePropertyInitializer)
338+
{
339+
sb.Append(helperClassName)
340+
.Append('.');
341+
}
342+
343+
sb.Append(info.EffectiveDefaultValueCreatorMethodName)
344+
.Append(");\n")
345+
.Append('\n');
307346
}
308347

309348
[MethodImpl(MethodImplOptions.AggressiveInlining)]
310-
static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info)
349+
static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info, in string fileStaticClassName)
311350
{
312351
var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;
313352
var formattedReturnType = GetFormattedReturnType(info.ReturnType);
@@ -324,7 +363,8 @@ static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info)
324363
{
325364
if (info.ShouldUsePropertyInitializer)
326365
{
327-
sb.Append(info.InitializingPropertyName);
366+
// Now reference the static flag on the file static helper class
367+
sb.Append(fileStaticClassName).Append(".").Append(info.InitializingPropertyName);
328368
}
329369
else
330370
{
@@ -556,52 +596,44 @@ static string GetFormattedReturnType(ITypeSymbol typeSymbol)
556596
}
557597

558598
/// <summary>
559-
/// Generates the boolean initialization flag used by bindable properties with initializers to indicate that the getter
560-
/// should return the backing field while the generated default value method is executing.
599+
/// Appends the initializing flag into the file-static helper class.
561600
/// </summary>
562-
/// <param name="sb">The StringBuilder instance to which the initialization field declaration will be appended.</param>
563-
/// <param name="info">The model containing metadata about the property for which the initialization field is generated.</param>
564-
static void GenerateInitializingProperty(StringBuilder sb, in BindablePropertyModel info)
601+
/// <param name="helperSb">Helper StringBuilder used to collect helper members.</param>
602+
/// <param name="info">Property model.</param>
603+
static void AppendHelperInitializingField(StringBuilder helperSb, in BindablePropertyModel info)
565604
{
566-
sb.Append("bool ")
605+
// Make the flag public static so it can be referenced from the generated partial class in the same file.
606+
helperSb.Append("public static bool ")
567607
.Append(info.InitializingPropertyName)
568608
.Append(" = false;\n");
569609
}
570610

571611
/// <summary>
572-
/// Generates the default value creator static method used by BindableProperty instances with initializers.
573-
/// This method temporarily switches the property's getter to return its backing field while the default value is being computed,
574-
/// ensuring that the initializer-provided value is captured and returned as the BindableProperty's default.
612+
/// Appends a default value creator method into the file-static helper class.
613+
/// The method sets the static initializing flag, reads the property's initializer value by casting the bindable
614+
/// to the declaring type, then clears the flag and returns the value.
575615
/// </summary>
576-
/// <param name="sb">The StringBuilder instance to which the initialization field declaration will be appended.</param>
577-
/// <param name="info">The model containing metadata for the property that requires a default value creator.</param>
578-
/// <param name="classNameWithGenerics">The declaring class name including generic type parameters, if any.</param>
579-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
580-
static void GenerateDefaultValueMethod(StringBuilder sb, in BindablePropertyModel info, string classNameWithGenerics)
616+
/// <param name="helperSb">Helper StringBuilder used to collect helper members.</param>
617+
/// <param name="info">Property model.</param>
618+
/// <param name="fullDeclaringType">Declaring type including containing types and generic parameters.</param>
619+
static void AppendHelperDefaultValueMethod(StringBuilder helperSb, in BindablePropertyModel info, string fullDeclaringType)
581620
{
582621
var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName;
583622

584-
sb.Append("static object ")
623+
helperSb.Append("public static object ")
585624
.Append(info.EffectiveDefaultValueCreatorMethodName)
586625
.Append("(global::Microsoft.Maui.Controls.BindableObject bindable)\n")
587626
.Append("{\n")
588-
.Append("((")
589-
.Append(classNameWithGenerics)
590-
.Append(")bindable).")
591627
.Append(info.InitializingPropertyName)
592628
.Append(" = true;\n")
593-
.Append("var defaultValue = ")
594-
.Append("((")
595-
.Append(classNameWithGenerics)
629+
.Append("var defaultValue = ((")
630+
.Append(fullDeclaringType)
596631
.Append(")bindable).")
597632
.Append(sanitizedPropertyName)
598633
.Append(";\n")
599-
.Append("((")
600-
.Append(classNameWithGenerics)
601-
.Append(")bindable).")
602634
.Append(info.InitializingPropertyName)
603635
.Append(" = false;\n")
604636
.Append("return defaultValue;\n")
605-
.Append("}\n");
637+
.Append("}\n\n");
606638
}
607639
}

src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ public record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType,
99
public bool ShouldUsePropertyInitializer => HasInitializer && DefaultValueCreatorMethodName is "null";
1010
public string BindablePropertyName => $"{PropertyName}Property";
1111
public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey";
12-
public string EffectiveDefaultValueCreatorMethodName => ShouldUsePropertyInitializer ? $"__createDefault{PropertyName}" : DefaultValueCreatorMethodName;
13-
public string InitializingPropertyName => $"__initializing{PropertyName}";
12+
public string EffectiveDefaultValueCreatorMethodName => ShouldUsePropertyInitializer ? $"CreateDefault{PropertyName}" : DefaultValueCreatorMethodName;
13+
public string InitializingPropertyName => $"IsInitializing{PropertyName}";
1414

1515
}
1616

0 commit comments

Comments
 (0)