Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
57877ec
Extend BindableProperty source gen to handle partial property initial…
stephenquan Dec 5, 2025
a9085ef
Update CommonUsageTests.cs
TheCodeTraveler Dec 5, 2025
fba8b25
Only implement new initializer code for when initializers are present
stephenquan Dec 5, 2025
fd700dc
Revert IntegrationTests
stephenquan Dec 5, 2025
b2dc373
Revert setter accessibility changes
stephenquan Dec 5, 2025
30fabbd
Merge branch 'feature/bp-sourcegen-init' of github.com:stephenquan/Ma…
stephenquan Dec 5, 2025
6ddcd78
Corrected setter
stephenquan Dec 5, 2025
c4bda26
Refactor GetDefaultValueCreatorMethod. Update XML Doc. Apply to reado…
stephenquan Dec 5, 2025
06dd458
Include global root namespace for BindableObject
stephenquan Dec 5, 2025
825adf8
Corrected typo in GetDefautValueCreatorMethodName
stephenquan Dec 5, 2025
1727770
Revert initializer-setter changes (not needed). Refactor getter-initi…
stephenquan Dec 5, 2025
9d785b6
Remove unneeded test from BindablePropertyModelTests
stephenquan Dec 5, 2025
8c823de
Refctor EffectiveDefaultValueCreatorMethodName property (replacing Ge…
stephenquan Dec 6, 2025
ebab9e1
Update BindablePropertyName_WithInitializer_ReturnsCorrectEffectiveDe…
stephenquan Dec 6, 2025
c80418c
Corrected unit test name GenerateBindableProperty_WithInitializer_Gen…
stephenquan Dec 6, 2025
0aed7ad
Refactor InitializingPropertyName property
stephenquan Dec 6, 2025
1ba82b4
Add Edge Case Test `GenerateBindableProperty_WithBothInitializerAndDe…
TheCodeTraveler Dec 8, 2025
50b229b
Merge branch 'main' into feature/bp-sourcegen-init
TheCodeTraveler Dec 8, 2025
1ce2b6a
Merge branch 'main' into pr/2987
TheCodeTraveler Dec 8, 2025
07d0087
Use `"null"` instead of `string.IsNullOrEmpty()`
TheCodeTraveler Dec 8, 2025
45adcdf
Merge branch 'feature/bp-sourcegen-init' of https://github.com/stephe…
TheCodeTraveler Dec 8, 2025
3a642d0
Remove `DefaultValue`
TheCodeTraveler Dec 8, 2025
77b3fa5
Move out-of-scope variables to `file static class`
TheCodeTraveler Dec 8, 2025
fc82b82
Use raw string literal
TheCodeTraveler Dec 8, 2025
4060d68
Update variable name
TheCodeTraveler Dec 9, 2025
a382be9
Implement `[BindableProperty]` for `AvatarView`
TheCodeTraveler Dec 9, 2025
a7ad7bc
Include class name in `file static class`
TheCodeTraveler Dec 9, 2025
ead0fea
Use `[BindableProperty]` for AvatarView
TheCodeTraveler Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ namespace CommunityToolkit.Maui.Core;
static class AvatarViewDefaults
{
/// <summary>Default avatar border width.</summary>
public const double DefaultBorderWidth = 1;
public const double BorderWidth = 1;

/// <summary>Default height request.</summary>
public const double DefaultHeightRequest = 48;
public const double HeightRequest = 48;

/// <summary>Default avatar text.</summary>
public const string DefaultText = "?";
public const string Text = "?";

/// <summary>Default width request.</summary>
public const double DefaultWidthRequest = 48;
public const double WidthRequest = 48;

/// <summary>default avatar border colour.</summary>
public static Color DefaultBorderColor { get; } = Colors.White;
public static Color BorderColor { get; } = Colors.White;

/// <summary>Default corner radius.</summary>
public static CornerRadius DefaultCornerRadius { get; } = new(24, 24, 24, 24);
public static CornerRadius CornerRadius { get; } = new(24, 24, 24, 24);

/// <summary>Default padding.</summary>
public static Thickness DefaultPadding { get; } = new(1);
public static Thickness Padding { get; } = new(1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
$$"""
// <auto-generated>
// See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
#pragma warning disable
#nullable enable
namespace {{defaultTestNamespace}};
public partial class {{defaultTestClassName}}
{
/// <summary>
/// Backing BindableProperty for the <see cref = "Text"/> property.
/// </summary>
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); }

/// <summary>
/// Backing BindableProperty for the <see cref = "CustomDuration"/> property.
/// </summary>
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we report a warning here? It feels like it could be nice to warn the developer that their initial value will be ignored.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! Before we promote [BindableProperty] to stable, we'll create a slew of Analyzers, similar to the CommunityToolkit.Mvvm Analyzers to help make using these dummy-proof. It'll include things this, generating a compiler error when missing the partial modifier, generating a compiler error when the provided method signatures are incorrect, etc.

My plan is to start working on the initializers in the new year after we've publish the first preview release of [BindableProperty].

public partial string Text { get; set; } = "Initial Value";

static string CreateDefaultText(BindableObject bindable)
{
return "Initial Value";
}
}
""";

const string expectedGenerated =
/* language=C#-test */
//lang=csharp
$$"""
// <auto-generated>
// See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator
#pragma warning disable
#nullable enable
namespace {{defaultTestNamespace}};
public partial class {{defaultTestClassName}}
{
/// <summary>
/// Backing BindableProperty for the <see cref = "Text"/> property.
/// </summary>
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public void BindablePropertyName_ReturnsCorrectPropertyName()
"null",
string.Empty,
true, // IsReadOnlyBindableProperty
string.Empty // SetterAccessibility
string.Empty, // SetterAccessibility
false
);

// Act
Expand All @@ -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(
Expand All @@ -71,7 +73,8 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues()
defaultValueCreatorMethodName,
newKeywordText,
true, // IsReadOnlyBindableProperty
string.Empty // SetterAccessibility
string.Empty, // SetterAccessibility
hasInitializer
);

// Assert
Expand All @@ -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]
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<IPropertySymbol>().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<IPropertySymbol>().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);
}
}
Loading
Loading