diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml index 2731240a8a..ffb609b7a6 100644 --- a/.github/workflows/dotnet-build.yml +++ b/.github/workflows/dotnet-build.yml @@ -42,6 +42,8 @@ env: PathToCommunityToolkitCameraAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.Camera.Analyzers.CodeFixes/CommunityToolkit.Maui.Camera.Analyzers.CodeFixes.csproj' PathToCommunityToolkitMediaElementAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.MediaElement.Analyzers.CodeFixes/CommunityToolkit.Maui.MediaElement.Analyzers.CodeFixes.csproj' PathToCommunityToolkitAnalyzersUnitTestProjectDirectory: 'src/CommunityToolkit.Maui.Analyzers.UnitTests' + PathToCommunityToolkitSourceGeneratorsInternalUnitTestDirectory: 'src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests' + PathToCommunityToolkitSourceGeneratorsInternalUnitTestCsproj: 'src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.csproj' PathToCommunityToolkitAnalyzersBenchmarkCsproj: 'src/CommunityToolkit.Maui.Analyzers.Benchmarks/CommunityToolkit.Maui.Analyzers.Benchmarks.csproj' CommunityToolkitLibrary_Xcode_Version: '26.1' @@ -209,6 +211,12 @@ jobs: run: | cd ${{ env.PathToCommunityToolkitAnalyzersUnitTestProjectDirectory }} dotnet run -c Release --results-directory "${{ runner.temp }}" --coverage --coverage-output "${{ runner.temp }}/ut-analyzers.cobertura.xml" --coverage-output-format cobertura --report-xunit + + - name: Run CommunityToolkit Source Generators Internal UnitTests + if: runner.os == 'Windows' + run: | + cd ${{ env.PathToCommunityToolkitSourceGeneratorsInternalUnitTestDirectory }} + dotnet run -c Release --results-directory "${{ runner.temp }}" --coverage --coverage-output "${{ runner.temp }}/ut-sourcegenerators-internal.cobertura.xml" --coverage-output-format cobertura --report-xunit - name: Run CommunityToolkit UnitTests run: | diff --git a/Directory.Build.props b/Directory.Build.props index 33e658a5af..73569d08ad 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -235,6 +235,7 @@ + diff --git a/samples/CommunityToolkit.Maui.Sample.slnx b/samples/CommunityToolkit.Maui.Sample.slnx index eaa637530e..10daac0d10 100644 --- a/samples/CommunityToolkit.Maui.Sample.slnx +++ b/samples/CommunityToolkit.Maui.Sample.slnx @@ -27,6 +27,7 @@ + diff --git a/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj b/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj index 6872375c9b..140af48816 100644 --- a/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj +++ b/src/CommunityToolkit.Maui.Analyzers.UnitTests/CommunityToolkit.Maui.Analyzers.UnitTests.csproj @@ -6,7 +6,8 @@ $(BaseIntermediateOutputPath)\GF true true - + false + true Exe CommunityToolkit.Maui.Analyzers.UnitTests diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/ProgressBarAnimationBehaviorDefaults.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/ProgressBarAnimationBehaviorDefaults.cs new file mode 100644 index 0000000000..f105fabb8b --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/ProgressBarAnimationBehaviorDefaults.cs @@ -0,0 +1,8 @@ +namespace CommunityToolkit.Maui.Core; + +static class ProgressBarAnimationBehaviorDefaults +{ + public const double Progress = 0.0; + public const uint Length = 500; + public static Easing Easing { get; } = Easing.Linear; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/UserStoppedTypingBehaviorDefaults.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/UserStoppedTypingBehaviorDefaults.cs new file mode 100644 index 0000000000..3fd0bf7024 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/UserStoppedTypingBehaviorDefaults.cs @@ -0,0 +1,8 @@ +namespace CommunityToolkit.Maui.Core; + +static class UserStoppedTypingBehaviorDefaults +{ + public const int StoppedTypingTimeThreshold = 1000; + public const int MinimumLengthThreshold = 0; + public const bool ShouldDismissKeyboardAutomatically = false; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BaseTest.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BaseTest.cs new file mode 100644 index 0000000000..7c2515f991 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BaseTest.cs @@ -0,0 +1,49 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests; + +public abstract class BaseTest +{ + protected static async Task VerifySourceGeneratorAsync(string source, string expectedAttribute, params List<(string FileName, string GeneratedFile)> expectedGenerated) + { + const string sourceGeneratorNamespace = "CommunityToolkit.Maui.SourceGenerators.Internal"; + const string bindablePropertyAttributeGeneratedFileName = "BindablePropertyAttribute.g.cs"; + var sourceGeneratorFullName = typeof(BindablePropertyAttributeSourceGenerator).FullName ?? throw new InvalidOperationException("Source Generator Type Path cannot be null"); + + var test = new CSharpSourceGeneratorTest + { +#if NET10_0 + ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net100, +#else +#error ReferenceAssemblies must be updated to current version of .NET +#endif + TestState = + { + Sources = { source }, + + AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableObject).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindingMode).Assembly.Location) + } + } + }; + + var expectedAttributeText = Microsoft.CodeAnalysis.Text.SourceText.From(expectedAttribute, System.Text.Encoding.UTF8); + var bindablePropertyAttributeFilePath = Path.Combine(sourceGeneratorNamespace, sourceGeneratorFullName, bindablePropertyAttributeGeneratedFileName); + test.TestState.GeneratedSources.Add((bindablePropertyAttributeFilePath, expectedAttributeText)); + + foreach (var generatedFile in expectedGenerated.Where(static x => !string.IsNullOrEmpty(x.GeneratedFile))) + { + var expectedGeneratedText = Microsoft.CodeAnalysis.Text.SourceText.From(generatedFile.GeneratedFile, System.Text.Encoding.UTF8); + var generatedFilePath = Path.Combine(sourceGeneratorNamespace, sourceGeneratorFullName, generatedFile.FileName); + test.TestState.GeneratedSources.Add((generatedFilePath, expectedGeneratedText)); + } + + await test.RunAsync(TestContext.Current.CancellationToken); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BaseBindablePropertyAttributeSourceGeneratorTest.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BaseBindablePropertyAttributeSourceGeneratorTest.cs new file mode 100644 index 0000000000..a03cc4af07 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BaseBindablePropertyAttributeSourceGeneratorTest.cs @@ -0,0 +1,37 @@ +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.BindablePropertyAttributeSourceGeneratorTests; + +public class BaseBindablePropertyAttributeSourceGeneratorTest : BaseTest +{ + protected const string defaultTestClassName = "TestView"; + protected const string defaultTestNamespace = "TestNamespace"; + + protected const string expectedAttribute = + /* language=C#-test */ + //lang=csharp + """ + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Maui; + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + sealed partial class BindablePropertyAttribute : global::System.Attribute + { + public string? PropertyName { get; } + public global::System.Type? DeclaringType { get; set; } + public object? DefaultValue { get; set; } + public global::Microsoft.Maui.Controls.BindingMode DefaultBindingMode { get; set; } + public string ValidateValueMethodName { get; set; } = string.Empty; + public string PropertyChangedMethodName { get; set; } = string.Empty; + public string PropertyChangingMethodName { get; set; } = string.Empty; + public string CoerceValueMethodName { get; set; } = string.Empty; + public string DefaultValueCreatorMethodName { get; set; } = string.Empty; + } + """; + + protected static Task VerifySourceGeneratorAsync(string source, string expectedGenerated) => + VerifySourceGeneratorAsync(source, expectedAttribute, ($"{defaultTestClassName}.g.cs", expectedGenerated)); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGeneratorTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGeneratorTests.cs new file mode 100644 index 0000000000..502892253a --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGeneratorTests.cs @@ -0,0 +1,354 @@ +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.BindablePropertyAttributeSourceGeneratorTests; + +public class BindablePropertyAttributeSourceGeneratorTests : BaseBindablePropertyAttributeSourceGeneratorTest +{ + [Fact] + public async Task GenerateBindableProperty_SimpleExample_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); + } + + [Fact] + public async Task GenerateBindableProperty_WithDefaultValue_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindablePropertyAttribute(DefaultValue = "Hello")] + 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}}), (string)"Hello", 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); + } + + [Fact] + public async Task GenerateBindableProperty_WithNewKeyword_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 new 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 new 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 new partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_NullableReferenceType_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); + } + + [Fact] + public async Task GenerateBindableProperty_MultipleProperties_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; } + + [BindablePropertyAttribute] + public partial int Number { 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); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Number", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial int Number { get => (int)GetValue(NumberProperty); set => SetValue(NumberProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_WithAllParameters_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindablePropertyAttribute( + DefaultValue = 42, + DefaultBindingMode = BindingMode.TwoWay, + ValidateValueMethodName = "ValidateValue", + PropertyChangedMethodName = "OnPropertyChanged", + PropertyChangingMethodName = "OnPropertyChanging", + CoerceValueMethodName = "CoerceValue", + DefaultValueCreatorMethodName = "CreateDefaultValue")] + public partial int Value { get; set; } + + static bool ValidateValue(BindableObject bindable, object value) => true; + static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue) { } + static void OnPropertyChanging(BindableObject bindable, object oldValue, object newValue) { } + static object CoerceValue(BindableObject bindable, object value) => value; + static object CreateDefaultValue(BindableObject bindable) => 0; + } + """; + + 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(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (int)42, (Microsoft.Maui.Controls.BindingMode)Microsoft.Maui.Controls.BindingMode.TwoWay, ValidateValue, OnPropertyChanged, OnPropertyChanging, CoerceValue, CreateDefaultValue); + public partial int Value { get => (int)GetValue(ValueProperty); set => SetValue(ValueProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_InternalClass_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + internal partial class TestView : 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}}; + internal partial class TestView + { + /// + /// 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); + } + + [Fact] + public async Task GenerateBindableProperty_NoAttribute_GeneratesNoCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + public string Text { get; set; } + } + """; + + await VerifySourceGeneratorAsync(source, string.Empty); + } + + [Fact] + public async Task GenerateBindableProperty_EmptyClass_GeneratesAttributeOnly() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + } + """; + + await VerifySourceGeneratorAsync(source, string.Empty); + } +} \ 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 new file mode 100644 index 0000000000..8391d60fc2 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/EdgeCaseTests.cs @@ -0,0 +1,363 @@ +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.BindablePropertyAttributeSourceGeneratorTests; + +public class EdgeCaseTests : BaseBindablePropertyAttributeSourceGeneratorTest +{ + [Fact] + public async Task GenerateBindableProperty_PropertyWithReservedKeywords_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty] + public partial string @class { get; set; } + + [BindableProperty] + public partial string @namespace { 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 classProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("@class", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string @class { get => (string)GetValue(classProperty); set => SetValue(classProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty namespaceProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("@namespace", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string @namespace { get => (string)GetValue(namespaceProperty); set => SetValue(namespaceProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_NullableValueTypes_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using System; + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty] + public partial int? NullableInt { get; set; } + + [BindableProperty] + public partial DateTime? NullableDateTime { get; set; } + + [BindableProperty] + public partial bool? NullableBool { 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 NullableIntProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableInt", typeof(int? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial int? NullableInt { get => (int? )GetValue(NullableIntProperty); set => SetValue(NullableIntProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableDateTimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableDateTime", typeof(System.DateTime? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial System.DateTime? NullableDateTime { get => (System.DateTime? )GetValue(NullableDateTimeProperty); set => SetValue(NullableDateTimeProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty NullableBoolProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("NullableBool", typeof(bool? ), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial bool? NullableBool { get => (bool? )GetValue(NullableBoolProperty); set => SetValue(NullableBoolProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_ArrayTypes_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty] + public partial string[] StringArray { get; set; } + + [BindableProperty] + public partial int[,] MultiDimensionalArray { get; set; } + + [BindableProperty] + public partial byte[][] JaggedArray { 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 StringArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("StringArray", typeof(string[]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string[] StringArray { get => (string[])GetValue(StringArrayProperty); set => SetValue(StringArrayProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty MultiDimensionalArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("MultiDimensionalArray", typeof(int[, ]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial int[, ] MultiDimensionalArray { get => (int[, ])GetValue(MultiDimensionalArrayProperty); set => SetValue(MultiDimensionalArrayProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty JaggedArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("JaggedArray", typeof(byte[][]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial byte[][] JaggedArray { get => (byte[][])GetValue(JaggedArrayProperty); set => SetValue(JaggedArrayProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_LongNamespaces_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace Very.Long.Namespace.With.Many.Segments.TestNamespace; + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty] + 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 Very.Long.Namespace.With.Many.Segments.TestNamespace; + 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(Very.Long.Namespace.With.Many.Segments.TestNamespace.TestView), 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); + } + + [Fact] + public async Task GenerateBindableProperty_GlobalNamespace_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty] + 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 + 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(TestView), 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); + } + + [Fact] + public async Task GenerateBindableProperty_SpecialCharactersInPropertyName_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty()] + public partial string Property_With_Underscores { get; set; } + + [BindableProperty()] + public partial string Property123WithNumbers { 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 Property_With_UnderscoresProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Property_With_Underscores", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string Property_With_Underscores { get => (string)GetValue(Property_With_UnderscoresProperty); set => SetValue(Property_With_UnderscoresProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty Property123WithNumbersProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Property123WithNumbers", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string Property123WithNumbers { get => (string)GetValue(Property123WithNumbersProperty); set => SetValue(Property123WithNumbersProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_WithComplexDefaultValues_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty(DefaultValue = true)] + public partial bool IsEnabled { get; set; } + + [BindableProperty(DefaultValue = 3.14)] + public partial double Pi { get; set; } + + [BindableProperty(DefaultValue = 'A')] + public partial char Letter { 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 IsEnabledProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("IsEnabled", typeof(bool), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (bool)true, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial bool IsEnabled { get => (bool)GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty PiProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Pi", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (double)3.14, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial double Pi { get => (double)GetValue(PiProperty); set => SetValue(PiProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty LetterProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Letter", typeof(char), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), (char)'A', Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial char Letter { get => (char)GetValue(LetterProperty); set => SetValue(LetterProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs new file mode 100644 index 0000000000..3df8bd95cc --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyAttributeSourceGeneratorTests/IntegrationTests.cs @@ -0,0 +1,317 @@ +using CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.BindablePropertyAttributeSourceGeneratorTests; + +public class IntegrationTests : BaseBindablePropertyAttributeSourceGeneratorTest +{ + [Fact] + public async Task GenerateBindableProperty_ComplexInheritanceScenario_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public abstract partial class BaseView : View + { + [BindableProperty] + public partial string BaseText { get; set; } + } + + public partial class DerivedView : BaseView + { + [BindableProperty] + public partial string DerivedText { get; set; } + + [BindableProperty] + public new partial string BaseText { get; set; } + } + """; + + const string expectedBaseGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + #pragma warning disable + #nullable enable + namespace {{defaultTestNamespace}}; + public partial class BaseView + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty BaseTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("BaseText", typeof(string), typeof(TestNamespace.BaseView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string BaseText { get => (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, value); } + } + """; + + const string expectedDerivedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + #pragma warning disable + #nullable enable + namespace {{defaultTestNamespace}}; + public partial class DerivedView + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty DerivedTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("DerivedText", typeof(string), typeof(TestNamespace.DerivedView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string DerivedText { get => (string)GetValue(DerivedTextProperty); set => SetValue(DerivedTextProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public new static readonly global::Microsoft.Maui.Controls.BindableProperty BaseTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("BaseText", typeof(string), typeof(TestNamespace.DerivedView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public new partial string BaseText { get => (string)GetValue(BaseTextProperty); set => SetValue(BaseTextProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedAttribute, ("BaseView.g.cs", expectedBaseGenerated), ("DerivedView.g.cs", expectedDerivedGenerated)); + } + + [Fact] + public async Task GenerateBindableProperty_GenericClass_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; } + + [BindableProperty] + public partial U? Name { 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 ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial T? Value { get => (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty NameProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Name", typeof(U), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial U? Name { get => (U? )GetValue(NameProperty); set => SetValue(NameProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_NestedClass_GeneratesCorrectCode() + { + const string outerClassName = "Outerclass"; + + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{outerClassName}} + { + public partial class {{defaultTestClassName}} : View + { + [BindableProperty] + 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 {{outerClassName}} +{ + 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}}.{{outerClassName}}.{{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, expectedAttribute, ($"{outerClassName}.{defaultTestClassName}.g.cs", expectedGenerated)); + } + + [Fact] + public async Task GenerateBindableProperty_WithCustomTypes_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + using System.Collections.Generic; + + namespace {{defaultTestNamespace}}; + + public class CustomModel + { + public string Name { get; set; } + } + + public partial class {{defaultTestClassName}} : View + { + [BindableProperty] + public partial CustomModel Model { get; set; } + + [BindableProperty] + public partial List Items { get; set; } + + [BindableProperty] + public partial Dictionary Properties { 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 ModelProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Model", typeof(TestNamespace.CustomModel), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial {{defaultTestNamespace}}.CustomModel Model { get => ({{defaultTestNamespace}}.CustomModel)GetValue(ModelProperty); set => SetValue(ModelProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty ItemsProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Items", typeof(System.Collections.Generic.List), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial System.Collections.Generic.List Items { get => (System.Collections.Generic.List)GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty PropertiesProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Properties", typeof(System.Collections.Generic.Dictionary), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial System.Collections.Generic.Dictionary Properties { get => (System.Collections.Generic.Dictionary)GetValue(PropertiesProperty); set => SetValue(PropertiesProperty, value); } + } + """; + + await VerifySourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateBindableProperty_MultipleClassesInSameFile_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class FirstView : View + { + [BindableProperty] + public partial string FirstText { get; set; } + } + + public partial class SecondView : View + { + [BindableProperty] + public partial string SecondText { get; set; } + } + """; + + const string expectedFirstGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + #pragma warning disable + #nullable enable + namespace {{defaultTestNamespace}}; + public partial class FirstView + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty FirstTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("FirstText", typeof(string), typeof(TestNamespace.FirstView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string FirstText { get => (string)GetValue(FirstTextProperty); set => SetValue(FirstTextProperty, value); } + } + """; + + const string expectedSecondGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + #pragma warning disable + #nullable enable + namespace {{defaultTestNamespace}}; + public partial class SecondView + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty SecondTextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("SecondText", typeof(string), typeof(TestNamespace.SecondView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string SecondText { get => (string)GetValue(SecondTextProperty); set => SetValue(SecondTextProperty, value); } + } + """; + + + await VerifySourceGeneratorAsync(source, expectedAttribute, ("FirstView.g.cs", expectedFirstGenerated), ("SecondView.g.cs", expectedSecondGenerated)); + } +} \ 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 new file mode 100644 index 0000000000..2f0946f120 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BindablePropertyModelTests.cs @@ -0,0 +1,150 @@ +using CommunityToolkit.Maui.SourceGenerators.Internal.Models; +using CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests; + +public class BindablePropertyModelTests : BaseTest +{ + [Fact] + public void BindablePropertyName_ReturnsCorrectPropertyName() + { + // Arrange + var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } }"); + var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; + var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); + + var model = new BindablePropertyModel( + "TestProperty", + propertySymbol.Type, + typeSymbol, + "null", + "Microsoft.Maui.Controls.BindingMode.OneWay", + "null", + "null", + "null", + "null", + "null", + string.Empty); + + // Act + var bindablePropertyName = model.BindablePropertyName; + + // Assert + Assert.Equal("TestPropertyProperty", bindablePropertyName); + } + + [Fact] + public void BindablePropertyModel_WithAllParameters_StoresCorrectValues() + { + // Arrange + var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } }"); + var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; + var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); + + const string propertyName = "TestProperty"; + const string defaultValue = "\"Hello\""; + const string defaultBindingMode = "Microsoft.Maui.Controls.BindingMode.TwoWay"; + const string validateValueMethodName = "ValidateValue"; + const string propertyChangedMethodName = "OnPropertyChanged"; + const string propertyChangingMethodName = "OnPropertyChanging"; + const string coerceValueMethodName = "CoerceValue"; + const string defaultValueCreatorMethodName = "CreateDefaultValue"; + const string newKeywordText = "new "; + + // Act + var model = new BindablePropertyModel( + propertyName, + propertySymbol.Type, + typeSymbol, + defaultValue, + defaultBindingMode, + validateValueMethodName, + propertyChangedMethodName, + propertyChangingMethodName, + coerceValueMethodName, + defaultValueCreatorMethodName, + newKeywordText); + + // Assert + Assert.Equal(propertyName, model.PropertyName); + Assert.Equal(propertySymbol.Type, model.ReturnType); + Assert.Equal(typeSymbol, model.DeclaringType); + Assert.Equal(defaultValue, model.DefaultValue); + Assert.Equal(defaultBindingMode, model.DefaultBindingMode); + Assert.Equal(validateValueMethodName, model.ValidateValueMethodName); + Assert.Equal(propertyChangedMethodName, model.PropertyChangedMethodName); + Assert.Equal(propertyChangingMethodName, model.PropertyChangingMethodName); + Assert.Equal(coerceValueMethodName, model.CoerceValueMethodName); + Assert.Equal(defaultValueCreatorMethodName, model.DefaultValueCreatorMethodName); + Assert.Equal(newKeywordText, model.NewKeywordText); + Assert.Equal("TestPropertyProperty", model.BindablePropertyName); + } + + [Fact] + public void ClassInformation_WithAllParameters_StoresCorrectValues() + { + // Arrange + const string className = "TestClass"; + const string declaredAccessibility = "public"; + const string containingNamespace = "TestNamespace"; + + // Act + var classInfo = new ClassInformation(className, declaredAccessibility, containingNamespace); + + // Assert + Assert.Equal(className, classInfo.ClassName); + Assert.Equal(declaredAccessibility, classInfo.DeclaredAccessibility); + Assert.Equal(containingNamespace, classInfo.ContainingNamespace); + } + + [Fact] + public void SemanticValues_WithClassInfoAndProperties_StoresCorrectValues() + { + // Arrange + var compilation = CreateCompilation("public class TestClass { public string TestProperty { get; set; } }"); + var typeSymbol = compilation.GetTypeByMetadataName("TestClass")!; + var propertySymbol = typeSymbol.GetMembers("TestProperty").OfType().First(); + + var classInfo = new ClassInformation("TestClass", "public", "TestNamespace"); + var bindableProperty = new BindablePropertyModel( + "TestProperty", + propertySymbol.Type, + typeSymbol, + "null", + "Microsoft.Maui.Controls.BindingMode.OneWay", + "null", + "null", + "null", + "null", + "null", + string.Empty); + + var bindableProperties = new[] { bindableProperty }.ToImmutableArray(); + + // Act + var semanticValues = new SemanticValues(classInfo, bindableProperties); + + // Assert + Assert.Equal(classInfo, semanticValues.ClassInformation); + Assert.Equal(bindableProperties, semanticValues.BindableProperties); + Assert.Single(semanticValues.BindableProperties); + } + + static Compilation CreateCompilation(string source) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var references = new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }; + + return CSharpCompilation.Create( + "TestAssembly", + [syntaxTree], + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.csproj b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.csproj new file mode 100644 index 0000000000..ba06befedb --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.csproj @@ -0,0 +1,40 @@ + + + + $(NetVersion) + true + $(BaseIntermediateOutputPath)\GF + true + true + false + true + Exe + CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/GlobalUsings.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/GlobalUsings.cs new file mode 100644 index 0000000000..324928b6a0 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using System; +global using System.Collections.Immutable; +global using System.Linq; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/ReferenceAssembliesExtensions.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/ReferenceAssembliesExtensions.cs new file mode 100644 index 0000000000..93f06db774 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/ReferenceAssembliesExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis.Testing; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests; + +static class ReferenceAssembliesExtensions +{ + static readonly Lazy lazyNet100 = new(() => + new(targetFramework: "net10.0", + referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.0-rc.2.25502.107"), + referenceAssemblyPath: Path.Combine("ref", "net10.0"))); + + extension(ReferenceAssemblies.Net) + { + public static ReferenceAssemblies Net100 => lazyNet100.Value; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/xunit.runner.json b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/xunit.runner.json new file mode 100644 index 0000000000..86e7fbb26f --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs deleted file mode 100644 index a79318f291..0000000000 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System.Collections.Immutable; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Text; -using CommunityToolkit.Maui.SourceGenerators.Helpers; -using CommunityToolkit.Maui.SourceGenerators.Internal.Helpers; -using CommunityToolkit.Maui.SourceGenerators.Internal.Models; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; - -namespace CommunityToolkit.Maui.SourceGenerators.Internal; - -[Generator] -public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator -{ - static readonly SemanticValues emptySemanticValues = new(default, []); - - const string bpFullName = "global::Microsoft.Maui.Controls.BindableProperty"; - const string bindingModeFullName = "global::Microsoft.Maui.Controls."; - - const string bpAttribute = - /* language=C#-test */ - //lang=csharp - $$""" - // - // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator - - #pragma warning disable - #nullable enable - namespace CommunityToolkit.Maui; - - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - sealed partial class BindablePropertyAttribute : Attribute - { - public string? PropertyName { get; } - public Type? DeclaringType { get; set; } - public object? DefaultValue { get; set; } - public string DefaultBindingMode { get; set; } = string.Empty; - public string ValidateValueMethodName { get; set; } = string.Empty; - public string PropertyChangedMethodName { get; set; } = string.Empty; - public string PropertyChangingMethodName { get; set; } = string.Empty; - public string CoerceValueMethodName { get; set; } = string.Empty; - public string DefaultValueCreatorMethodName { get; set; } = string.Empty; - - public BindablePropertyAttribute(string propertyName) - { - PropertyName = propertyName; - } - - public BindablePropertyAttribute() - { - } - } - """; - - public void Initialize(IncrementalGeneratorInitializationContext context) - { -#if DEBUG - - if (!Debugger.IsAttached) - { - // To debug this SG, uncomment the line below and rebuild the SourceGenerator project. - - //Debugger.Launch(); - } -#endif - - context.RegisterPostInitializationOutput(static ctx => ctx.AddSource("BindablePropertyAttribute.g.cs", SourceText.From(bpAttribute, Encoding.UTF8))); - - var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.BindablePropertyAttribute", - SyntaxPredicate, SemanticTransform) - .Where(static x => x.ClassInformation != default || !x.BindableProperties.IsEmpty) - .Collect(); - - - context.RegisterSourceOutput(provider, ExecuteAllValues); - } - - static void ExecuteAllValues(SourceProductionContext context, ImmutableArray semanticValues) - { - var groupedValues = semanticValues - .GroupBy(static sv => (sv.ClassInformation.ClassName, sv.ClassInformation.ContainingNamespace)) - .ToDictionary(static d => d.Key, static d => d.ToArray()); - - foreach (var keyValuePair in groupedValues) - { - var (className, containingNamespace) = keyValuePair.Key; - var values = keyValuePair.Value; - - if (values.Length is 0 || string.IsNullOrEmpty(className) || string.IsNullOrEmpty(containingNamespace)) - { - continue; - } - - var bindableProperties = values.SelectMany(static x => x.BindableProperties).ToImmutableArray(); - - var classAccessibility = values[0].ClassInformation.DeclaredAccessibility; - - var combinedClassInfo = new ClassInformation(className, classAccessibility, containingNamespace); - var combinedValues = new SemanticValues(combinedClassInfo, bindableProperties); - - var source = GenerateSource(combinedValues); - SourceStringService.FormatText(ref source); - context.AddSource($"{className}.g.cs", SourceText.From(source, Encoding.UTF8)); - } - } - - - static string GenerateSource(SemanticValues value) - { - var sb = new StringBuilder( - /* language=C#-test */ - //lang=csharp - $$""" - - // - // Test2 : {{DateTime.Now}} - - namespace {{value.ClassInformation.ContainingNamespace}}; - - {{value.ClassInformation.DeclaredAccessibility}} partial class {{value.ClassInformation.ClassName}} - { - - """); - - foreach (var info in value.BindableProperties) - { - GenerateBindableProperty(sb, info); - GenerateProperty(sb, info); - } - - sb.AppendLine().Append('}'); - return sb.ToString(); - - static void GenerateBindableProperty(StringBuilder sb, BindablePropertyModel info) - { - /* - /// - /// Backing BindableProperty for the property. - /// - */ - sb.AppendLine("/// ") - .AppendLine($"/// Backing BindableProperty for the property.") - .AppendLine("/// "); - - // public static readonly BindableProperty TextProperty = BindableProperty.Create(...); - sb.AppendLine($"public {info.NewKeywordText} static readonly {bpFullName} {info.PropertyName}Property = ") - .Append($"{bpFullName}.Create(") - .Append($"\"{info.PropertyName}\", ") - .Append($"typeof({info.ReturnType}), ") - .Append($"typeof({info.DeclaringType}), ") - .Append($"{info.DefaultValue}, ") - .Append($"{bindingModeFullName}{info.DefaultBindingMode}, ") - .Append($"{info.ValidateValueMethodName}, ") - .Append($"{info.PropertyChangedMethodName}, ") - .Append($"{info.PropertyChangingMethodName}, ") - .Append($"{info.CoerceValueMethodName}, ") - .Append($"{info.DefaultValueCreatorMethodName}") - .Append(");"); - - sb.AppendLine().AppendLine(); - } - - static void GenerateProperty(StringBuilder sb, BindablePropertyModel info) - { - // The code below creates the following Property: - // - // public partial string Text - // { - // get => (string)GetValue(TextProperty); - // set => SetValue(TextProperty, value); - // } - // - sb.AppendLine($"public {info.NewKeywordText}partial {info.ReturnType} {info.PropertyName}") - .AppendLine("{") - .Append("get => (") - .Append(info.ReturnType) - .Append(")GetValue(") - .AppendLine($"{info.PropertyName}Property);") - .Append("set => SetValue(") - .AppendLine($"{info.PropertyName}Property, value);") - .AppendLine("}"); - } - } - - static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) - { - var propertyDeclarationSyntax = Unsafe.As(context.TargetNode); - var semanticModel = context.SemanticModel; - var propertySymbol = (IPropertySymbol?)ModelExtensions.GetDeclaredSymbol(semanticModel, propertyDeclarationSyntax, cancellationToken); - - if (propertySymbol is null) - { - return emptySemanticValues; - } - - var @namespace = propertySymbol.ContainingNamespace.ToDisplayString(); - var className = propertySymbol.ContainingType.Name; - var classAccessibility = propertySymbol.ContainingSymbol.DeclaredAccessibility.ToString().ToLower(); - var returnType = propertySymbol.Type; - - var propertyInfo = new ClassInformation(className, classAccessibility, @namespace); - - var bindablePropertyModels = new List(context.Attributes.Length); - - var doesContainNewKeyword = propertyDeclarationSyntax.Modifiers.Any(static x => x.IsKind(SyntaxKind.NewKeyword)); - - var attributeData = context.Attributes[0]; - bindablePropertyModels.Add(CreateBindablePropertyModel(attributeData, propertySymbol.Type.ToDisplayString(), propertySymbol.Name, returnType, doesContainNewKeyword)); - - return new(propertyInfo, bindablePropertyModels.ToImmutableArray()); - } - - static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in string declaringTypeString, in string defaultName, in ITypeSymbol returnType, in bool doesContainNewKeyword) - { - if (attributeData.AttributeClass is null) - { - throw new ArgumentException($"{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData.AttributeClass)); - } - - var defaultValue = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValue)); - var coerceValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName)); - var defaultBindingMode = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultBindingMode), "BindingMode.OneWay"); - var defaultValueCreatorMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName)); - var declaringType = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DeclaringType), declaringTypeString); - var propertyChangedMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName)); - var propertyChangingMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName)); - var propertyName = attributeData.GetConstructorArgumentsAttributeValueByNameAsString(defaultName); - var validateValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName)); - var newKeywordText = doesContainNewKeyword ? "new " : string.Empty; - - return new BindablePropertyModel(propertyName, returnType, declaringType, defaultValue, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText); - } - - static bool SyntaxPredicate(SyntaxNode node, CancellationToken cancellationToken) => - node is PropertyDeclarationSyntax { AttributeLists.Count: > 0 }; -} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/CommunityToolkit.Maui.SourceGenerators.Internal.csproj b/src/CommunityToolkit.Maui.SourceGenerators.Internal/CommunityToolkit.Maui.SourceGenerators.Internal.csproj index 7f246746b1..996822d31e 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/CommunityToolkit.Maui.SourceGenerators.Internal.csproj +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/CommunityToolkit.Maui.SourceGenerators.Internal.csproj @@ -16,6 +16,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs new file mode 100644 index 0000000000..b9be7baaf4 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -0,0 +1,361 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using CommunityToolkit.Maui.SourceGenerators.Helpers; +using CommunityToolkit.Maui.SourceGenerators.Internal.Helpers; +using CommunityToolkit.Maui.SourceGenerators.Internal.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal; + +[Generator] +public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator +{ + static readonly SemanticValues emptySemanticValues = new(default, []); + + const string bpFullName = "global::Microsoft.Maui.Controls.BindableProperty"; + + const string bpAttribute = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Maui; + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + sealed partial class BindablePropertyAttribute : global::System.Attribute + { + public string? PropertyName { get; } + public global::System.Type? DeclaringType { get; set; } + public object? DefaultValue { get; set; } + public global::Microsoft.Maui.Controls.BindingMode DefaultBindingMode { get; set; } + public string ValidateValueMethodName { get; set; } = string.Empty; + public string PropertyChangedMethodName { get; set; } = string.Empty; + public string PropertyChangingMethodName { get; set; } = string.Empty; + public string CoerceValueMethodName { get; set; } = string.Empty; + public string DefaultValueCreatorMethodName { get; set; } = string.Empty; + } + """; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { +#if DEBUG + + if (!Debugger.IsAttached) + { + // To debug this SG, uncomment the line below and rebuild the SourceGenerator project. + + //Debugger.Launch(); + } +#endif + + context.RegisterPostInitializationOutput(static ctx => ctx.AddSource("BindablePropertyAttribute.g.cs", SourceText.From(bpAttribute, Encoding.UTF8))); + + var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.BindablePropertyAttribute", + IsNonEmptyPropertyDeclarationSyntax, SemanticTransform) + .Where(static x => x.ClassInformation != default || !x.BindableProperties.IsEmpty) + .Collect(); + + + context.RegisterSourceOutput(provider, ExecuteAllValues); + } + + static void ExecuteAllValues(SourceProductionContext context, ImmutableArray semanticValues) + { + var groupedValues = semanticValues + .GroupBy(static sv => (sv.ClassInformation.ClassName, sv.ClassInformation.ContainingNamespace, sv.ClassInformation.ContainingTypes, sv.ClassInformation.GenericTypeParameters)) + .ToDictionary(static d => d.Key, static d => d.ToArray()); + + foreach (var keyValuePair in groupedValues) + { + var (className, containingNamespace, containingTypes, genericTypeParameters) = keyValuePair.Key; + var values = keyValuePair.Value; + + if (values.Length is 0 || string.IsNullOrEmpty(className) || string.IsNullOrEmpty(containingNamespace)) + { + continue; + } + + var bindableProperties = values.SelectMany(static x => x.BindableProperties).ToImmutableArray(); + + var classAccessibility = values[0].ClassInformation.DeclaredAccessibility; + + var combinedClassInfo = new ClassInformation(className, classAccessibility, containingNamespace, containingTypes, genericTypeParameters); + var combinedValues = new SemanticValues(combinedClassInfo, bindableProperties); + + var fileNameSuffix = string.IsNullOrEmpty(containingTypes) ? className : $"{containingTypes}.{className}"; + var source = GenerateSource(combinedValues); + SourceStringService.FormatText(ref source); + context.AddSource($"{fileNameSuffix}.g.cs", SourceText.From(source, Encoding.UTF8)); + } + } + + + static string GenerateSource(SemanticValues value) + { + var namespaceLine = IsGlobalNamespace(value.ClassInformation) + ? string.Empty + : $"namespace {value.ClassInformation.ContainingNamespace};"; + + var sb = new StringBuilder( + /* language=C#-test */ + //lang=csharp + $""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + + #pragma warning disable + #nullable enable + + {namespaceLine} + + """); + + // Generate nested class hierarchy + if (!string.IsNullOrEmpty(value.ClassInformation.ContainingTypes)) + { + var containingTypeNames = value.ClassInformation.ContainingTypes.Split('.'); + foreach (var typeName in containingTypeNames) + { + sb.AppendLine($"{value.ClassInformation.DeclaredAccessibility} partial class {typeName}") + .AppendLine("{") + .AppendLine(); + } + } + + // Get the class name with generic parameters + var classNameWithGenerics = value.ClassInformation.ClassName; + if (!string.IsNullOrEmpty(value.ClassInformation.GenericTypeParameters)) + { + classNameWithGenerics = $"{value.ClassInformation.ClassName}<{value.ClassInformation.GenericTypeParameters}>"; + } + + sb.AppendLine($"{value.ClassInformation.DeclaredAccessibility} partial class {classNameWithGenerics}") + .AppendLine("{") + .AppendLine(); + + foreach (var info in value.BindableProperties) + { + GenerateBindableProperty(ref sb, info); + GenerateProperty(ref sb, info); + } + + sb.Append('}'); + + // Close nested class hierarchy + if (!string.IsNullOrEmpty(value.ClassInformation.ContainingTypes)) + { + var containingTypeNames = value.ClassInformation.ContainingTypes.Split('.'); + for (int i = 0; i < containingTypeNames.Length; i++) + { + sb.AppendLine().Append('}'); + } + } + + return sb.ToString(); + + static void GenerateBindableProperty(ref readonly StringBuilder sb, in BindablePropertyModel info) + { + // Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator + var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType); + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? "@" + info.PropertyName : info.PropertyName; + + /* + // The code below creates the following XML Tag: + /// + /// Backing BindableProperty for the property. + /// + */ + sb.AppendLine("/// ") + .AppendLine($"/// Backing BindableProperty for the property.") + .AppendLine("/// "); + + /* + // The code below creates the following BindableProperty: + // + // public static readonly BindableProperty TextProperty = BindableProperty.Create(...); + */ + sb.AppendLine($"public {info.NewKeywordText}static readonly {bpFullName} {info.BindablePropertyName} = ") + .Append($"{bpFullName}.Create(") + .Append($"\"{sanitizedPropertyName}\", ") + .Append($"typeof({GetFormattedReturnType(nonNullableReturnType)}), ") + .Append($"typeof({info.DeclaringType}), ") + .Append($"{info.DefaultValue}, ") + .Append($"{info.DefaultBindingMode}, ") + .Append($"{info.ValidateValueMethodName}, ") + .Append($"{info.PropertyChangedMethodName}, ") + .Append($"{info.PropertyChangingMethodName}, ") + .Append($"{info.CoerceValueMethodName}, ") + .Append($"{info.DefaultValueCreatorMethodName}") + .Append(");"); + + sb.AppendLine().AppendLine(); + } + + static void GenerateProperty(ref readonly StringBuilder sb, in BindablePropertyModel info) + { + // The code below creates the following Property: + // + // public partial string Text + // { + // get => (string)GetValue(TextProperty); + // set => SetValue(TextProperty, value); + // } + // + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? "@" + info.PropertyName : info.PropertyName; + + sb.AppendLine($"public {info.NewKeywordText}partial {GetFormattedReturnType(info.ReturnType)} {sanitizedPropertyName}") + .AppendLine("{") + .Append("get => (") + .Append(GetFormattedReturnType(info.ReturnType)) + .Append(")GetValue(") + .AppendLine($"{info.BindablePropertyName});") + .Append("set => SetValue(") + .AppendLine($"{info.BindablePropertyName}, value);") + .AppendLine("}"); + } + } + + static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + var propertyDeclarationSyntax = Unsafe.As(context.TargetNode); + var semanticModel = context.SemanticModel; + var propertySymbol = (IPropertySymbol?)ModelExtensions.GetDeclaredSymbol(semanticModel, propertyDeclarationSyntax, cancellationToken); + + if (propertySymbol is null) + { + return emptySemanticValues; + } + + var @namespace = propertySymbol.ContainingNamespace.ToDisplayString(); + var className = propertySymbol.ContainingType.Name; + var classAccessibility = propertySymbol.ContainingSymbol.DeclaredAccessibility.ToString().ToLower(); + var returnType = propertySymbol.Type; + + // Build containing types hierarchy + var containingTypes = GetContainingTypes(propertySymbol.ContainingType); + + // Extract generic type parameters + var genericTypeParameters = GetGenericTypeParameters(propertySymbol.ContainingType); + + var propertyInfo = new ClassInformation(className, classAccessibility, @namespace, containingTypes, genericTypeParameters); + + var bindablePropertyModels = new List(context.Attributes.Length); + + var doesContainNewKeyword = propertyDeclarationSyntax.Modifiers.Any(static x => x.IsKind(SyntaxKind.NewKeyword)); + + var attributeData = context.Attributes[0]; + bindablePropertyModels.Add(CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword)); + + return new(propertyInfo, bindablePropertyModels.ToImmutableArray()); + } + + static string GetContainingTypes(INamedTypeSymbol typeSymbol) + { + var types = new List(); + var current = typeSymbol.ContainingType; + + while (current is not null) + { + types.Insert(0, current.Name); + current = current.ContainingType; + } + + return string.Join(".", types); + } + + static string GetGenericTypeParameters(INamedTypeSymbol typeSymbol) + { + if (!typeSymbol.IsGenericType || typeSymbol.TypeParameters.IsEmpty) + { + return string.Empty; + } + + var typeParams = typeSymbol.TypeParameters.Select(tp => tp.Name); + return string.Join(", ", typeParams); + } + + static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword) + { + if (attributeData.AttributeClass is null) + { + throw new ArgumentException($"{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData.AttributeClass)); + } + + var defaultValue = attributeData.GetNamedTypeArgumentsAttributeValueByNameAsCastedString(nameof(BindablePropertyModel.DefaultValue)); + var coerceValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName)); + var defaultBindingMode = attributeData.GetNamedTypeArgumentsAttributeValueByNameAsCastedString(nameof(BindablePropertyModel.DefaultBindingMode), "Microsoft.Maui.Controls.BindingMode.OneWay"); + var defaultValueCreatorMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName)); + var propertyChangedMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName)); + var propertyChangingMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName)); + 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); + } + + static ITypeSymbol ConvertToNonNullableTypeSymbol(in ITypeSymbol typeSymbol) + { + // Check for Nullable + if (typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }) + { + return typeSymbol; + } + + // Check for Nullable Reference Type + if (typeSymbol.NullableAnnotation is NullableAnnotation.Annotated) + { + // For reference types, NullableAnnotation.None indicates non-nullable. + return typeSymbol.WithNullableAnnotation(NullableAnnotation.None); + } + + return typeSymbol; + } + + static bool IsNonEmptyPropertyDeclarationSyntax(SyntaxNode node, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return node is PropertyDeclarationSyntax { AttributeLists.Count: > 0 }; + } + + static bool IsDotnetKeyword(in string name) => SyntaxFacts.GetKeywordKind(name) is not SyntaxKind.None; + + static bool IsGlobalNamespace(in ClassInformation classInformation) + { + if (classInformation.ContainingNamespace is "") + { + return true; + } + return false; + } + + static string GetFormattedReturnType(ITypeSymbol typeSymbol) + { + if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + { + // Get the element type name (e.g., "int") + string elementType = GetFormattedReturnType(arrayTypeSymbol.ElementType); + + // Construct the correct rank syntax with commas (e.g., "[,]") + string rank = new(',', arrayTypeSymbol.Rank - 1); + + return $"{elementType}[{rank}]"; + } + else + { + // Use ToDisplayString with the correct format for the base type (e.g., "int") + // SymbolDisplayFormat.CSharpErrorMessageFormat often works well for standard C# names + return typeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs index e9d0c66167..da2259a77e 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs @@ -1,4 +1,6 @@ -using Microsoft.CodeAnalysis; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Maui.SourceGenerators.Internal.Helpers; @@ -10,23 +12,40 @@ public static TypedConstant GetAttributeValueByName(this AttributeData attribute return x; } - public static string GetNamedArgumentsAttributeValueByNameAsString(this AttributeData attribute, string name, string placeholder = "null") + public static string GetNamedTypeArgumentsAttributeValueByNameAsCastedString(this AttributeData attribute, string name, string placeholder = "null") { var data = attribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == name).Value; - return data.Value is null ? placeholder : data.Value.ToString(); - } + // true.ToString() => "True" and false.ToString() => "False", but we want "true" and "false" + if (data.Kind is TypedConstantKind.Primitive && data.Type?.SpecialType is SpecialType.System_Boolean) + { + return data.Value is null ? placeholder : $"({data.Type}){data.Value.ToString().ToLowerInvariant()}"; + } - public static string GetConstructorArgumentsAttributeValueByNameAsString(this AttributeData attribute, string placeholder) - { - if (attribute.ConstructorArguments.Length is 0) + if (data.Kind is TypedConstantKind.Enum && data.Type is not null && data.Value is not null) { - return placeholder; + var members = data.Type.GetMembers(); + + return $"({data.Type}){members[(int)data.Value]}"; } - var data = attribute.ConstructorArguments[0]; + if(data.Type?.SpecialType is SpecialType.System_String) + { + return data.Value is null ? $"\"{placeholder}\"": $"({data.Type})\"{data.Value}\""; + } - return data.Value is null ? placeholder : data.Value.ToString(); + if (data.Type?.SpecialType is SpecialType.System_Char) + { + return data.Value is null ? $"\"{placeholder}\"" : $"({data.Type})\'{data.Value}\'"; + } + + return data.Value is null ? placeholder : $"({data.Type}){data.Value}"; } + public static string GetNamedMethodGroupArgumentsAttributeValueByNameAsString(this AttributeData attribute, string name, string placeholder = "null") + { + var data = attribute.NamedArguments.SingleOrDefault(kvp => kvp.Key == name).Value; + + return data.Value is null ? placeholder : data.Value.ToString(); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/IsExternalInit.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/IsExternalInit.cs deleted file mode 100644 index 50b4f95c6c..0000000000 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/IsExternalInit.cs +++ /dev/null @@ -1,66 +0,0 @@ -// -// This code file has automatically been added by the "IsExternalInit" NuGet package (https://www.nuget.org/packages/IsExternalInit). -// Please see https://github.com/manuelroemer/IsExternalInit for more information. -// -// IMPORTANT: -// DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references. -// Consider migrating to PackageReferences instead: -// https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference -// Migrating brings the following benefits: -// * The "IsExternalInit" folder and the "IsExternalInit.cs" file don't appear in your project. -// * The added file is immutable and can therefore not be modified by coincidence. -// * Updating/Uninstalling the package will work flawlessly. -// - -#region License -// MIT License -// -// Copyright (c) Manuel Römer -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -#endregion - -#if !ISEXTERNALINIT_DISABLE -#nullable enable -#pragma warning disable - -namespace System.Runtime.CompilerServices -{ - using global::System.Diagnostics; - using global::System.Diagnostics.CodeAnalysis; - - /// - /// Reserved to be used by the compiler for tracking metadata. - /// This class should not be used by developers in source code. - /// - /// - /// This definition is provided by the IsExternalInit NuGet package (https://www.nuget.org/packages/IsExternalInit). - /// Please see https://github.com/manuelroemer/IsExternalInit for more information. - /// -#if !ISEXTERNALINIT_INCLUDE_IN_CODE_COVERAGE - [ExcludeFromCodeCoverage, DebuggerNonUserCode] -#endif - internal static class IsExternalInit - { - } -} - -#pragma warning restore -#nullable restore -#endif // ISEXTERNALINIT_DISABLE diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs index fd9cb5133f..55b4b162a1 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Models/Records.cs @@ -3,8 +3,11 @@ namespace CommunityToolkit.Maui.SourceGenerators.Internal.Models; -record BindablePropertyModel(string PropertyName, ITypeSymbol? ReturnType, string DeclaringType, string DefaultValue, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText); +record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultValue, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText) +{ + public string BindablePropertyName => $"{PropertyName}Property"; +} record SemanticValues(ClassInformation ClassInformation, EquatableArray BindableProperties); -readonly record struct ClassInformation(string ClassName, string DeclaredAccessibility, string ContainingNamespace); \ No newline at end of file +readonly record struct ClassInformation(string ClassName, string DeclaredAccessibility, string ContainingNamespace, string ContainingTypes = "", string GenericTypeParameters = ""); \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Properties/launchSettings.json b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Properties/launchSettings.json new file mode 100644 index 0000000000..904d4b884a --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Profile 1": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\CommunityToolkit.Maui\\CommunityToolkit.Maui.csproj" + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Behaviors/ProgressBarAnimationBehaviorTests.cs b/src/CommunityToolkit.Maui.UnitTests/Behaviors/ProgressBarAnimationBehaviorTests.cs index 4da68694d1..7db7e75ab3 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Behaviors/ProgressBarAnimationBehaviorTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Behaviors/ProgressBarAnimationBehaviorTests.cs @@ -33,7 +33,7 @@ public async Task ValidPropertiesTests(double progress, uint length, Easing easi Assert.Equal(0.0d, progressBar.Progress); Assert.Equal(0.0d, ProgressBarAnimationBehavior.ProgressProperty.DefaultValue); Assert.Equal((uint)500, ProgressBarAnimationBehavior.LengthProperty.DefaultValue); - Assert.Equal(Easing.Linear, ProgressBarAnimationBehavior.EasingProperty.DefaultValue); + Assert.Equal(Easing.Linear, progressBarAnimationBehavior.Easing); progressBarAnimationBehavior.Length = length; progressBarAnimationBehavior.Easing = easing; @@ -59,19 +59,20 @@ void HandleAnimationCompleted(object? sender, EventArgs e) } [Theory] - [InlineData(double.MinValue, 0)] - [InlineData(-1, 0)] - [InlineData(-0.0000000000001, 0)] - [InlineData(1.0000000000001, 1)] - [InlineData(double.MaxValue, 1)] - public void InvalidProgressValuesTest(double inputProgressValue, double expectedProgressValue) + [InlineData(double.MinValue)] + [InlineData(-1)] + [InlineData(-0.0000000000001)] + [InlineData(1.0000000000001)] + [InlineData(double.MaxValue)] + public void InvalidProgressValuesTest(double inputProgressValue) { - var progressBarAnimationBehavior = new ProgressBarAnimationBehavior + Assert.Throws(() => { - Progress = inputProgressValue - }; - - Assert.Equal(expectedProgressValue, progressBarAnimationBehavior.Progress); + new ProgressBarAnimationBehavior + { + Progress = inputProgressValue + }; + }); } [Fact] diff --git a/src/CommunityToolkit.Maui.UnitTests/Behaviors/TouchBehaviorTests.cs b/src/CommunityToolkit.Maui.UnitTests/Behaviors/TouchBehaviorTests.cs index 6ab65e4fff..8a3af263e3 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Behaviors/TouchBehaviorTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Behaviors/TouchBehaviorTests.cs @@ -859,8 +859,6 @@ public void SetHoveredOpacityTest() touchBehavior.SetBinding(TouchBehavior.HoveredOpacityProperty, nameof(TouchBehaviorViewModel.HoveredOpacity), mode: BindingMode.TwoWay); - Assert.Throws(() => touchBehavior.HoveredOpacity = 1.01); - Assert.Throws(() => touchBehavior.HoveredOpacity = -0.01); Assert.Equal(default, viewModel.HoveredOpacity); touchBehavior.HoveredOpacity = hoveredOpacity; @@ -868,6 +866,30 @@ public void SetHoveredOpacityTest() Assert.Equal(hoveredOpacity, viewModel.HoveredOpacity); } + [Fact] + public void SetHoveredOpacityAbove1Test() + { + var viewModel = new TouchBehaviorViewModel(); + touchBehavior.BindingContext = viewModel; + + touchBehavior.SetBinding(TouchBehavior.HoveredOpacityProperty, nameof(TouchBehaviorViewModel.HoveredOpacity), mode: BindingMode.TwoWay); + + Assert.Throws(() => touchBehavior.HoveredOpacity = 1.01); + Assert.Equal(default, viewModel.HoveredOpacity); + } + + [Fact] + public void SetHoveredOpacityBelow0Test() + { + var viewModel = new TouchBehaviorViewModel(); + touchBehavior.BindingContext = viewModel; + + touchBehavior.SetBinding(TouchBehavior.HoveredOpacityProperty, nameof(TouchBehaviorViewModel.HoveredOpacity), mode: BindingMode.TwoWay); + + Assert.Throws(() => touchBehavior.HoveredOpacity = -0.01); + Assert.Equal(default, viewModel.HoveredOpacity); + } + [Fact] public void SetPressedOpacityTest() { @@ -876,9 +898,6 @@ public void SetPressedOpacityTest() touchBehavior.BindingContext = viewModel; touchBehavior.SetBinding(TouchBehavior.PressedOpacityProperty, nameof(TouchBehaviorViewModel.PressedOpacity), mode: BindingMode.TwoWay); - - Assert.Throws(() => touchBehavior.PressedOpacity = 1.01); - Assert.Throws(() => touchBehavior.PressedOpacity = -0.01); Assert.Equal(default, viewModel.PressedOpacity); touchBehavior.PressedOpacity = pressedOpacity; @@ -886,6 +905,30 @@ public void SetPressedOpacityTest() Assert.Equal(pressedOpacity, viewModel.PressedOpacity); } + [Fact] + public void SetPressedOpacityAbove1Test() + { + var viewModel = new TouchBehaviorViewModel(); + touchBehavior.BindingContext = viewModel; + + touchBehavior.SetBinding(TouchBehavior.PressedOpacityProperty, nameof(TouchBehaviorViewModel.PressedOpacity), mode: BindingMode.TwoWay); + + Assert.Throws(() => touchBehavior.PressedOpacity = 1.01); + Assert.Equal(default, viewModel.PressedOpacity); + } + + [Fact] + public void SetPressedOpacityBelow0Test() + { + var viewModel = new TouchBehaviorViewModel(); + touchBehavior.BindingContext = viewModel; + + touchBehavior.SetBinding(TouchBehavior.PressedOpacityProperty, nameof(TouchBehaviorViewModel.PressedOpacity), mode: BindingMode.TwoWay); + + Assert.Throws(() => touchBehavior.PressedOpacity = -0.01); + Assert.Equal(default, viewModel.PressedOpacity); + } + [Fact] public void SetDefaultOpacityTest() { @@ -895,13 +938,33 @@ public void SetDefaultOpacityTest() touchBehavior.SetBinding(TouchBehavior.DefaultOpacityProperty, nameof(TouchBehaviorViewModel.DefaultOpacity), mode: BindingMode.TwoWay); + touchBehavior.DefaultOpacity = defaultOpacity; + + Assert.Equal(defaultOpacity, viewModel.DefaultOpacity); + } + + [Fact] + public void SetDefaultOpacityAbove1Test() + { + var viewModel = new TouchBehaviorViewModel(); + touchBehavior.BindingContext = viewModel; + + touchBehavior.SetBinding(TouchBehavior.DefaultOpacityProperty, nameof(TouchBehaviorViewModel.DefaultOpacity), mode: BindingMode.TwoWay); + Assert.Throws(() => touchBehavior.DefaultOpacity = 1.01); - Assert.Throws(() => touchBehavior.DefaultOpacity = -0.01); Assert.Equal(default, viewModel.DefaultOpacity); + } - touchBehavior.DefaultOpacity = defaultOpacity; + [Fact] + public void SetDefaultOpacityBelow0Test() + { + var viewModel = new TouchBehaviorViewModel(); + touchBehavior.BindingContext = viewModel; - Assert.Equal(defaultOpacity, viewModel.DefaultOpacity); + touchBehavior.SetBinding(TouchBehavior.DefaultOpacityProperty, nameof(TouchBehaviorViewModel.DefaultOpacity), mode: BindingMode.TwoWay); + + Assert.Throws(() => touchBehavior.DefaultOpacity = -0.01); + Assert.Equal(default, viewModel.DefaultOpacity); } [Fact] diff --git a/src/CommunityToolkit.Maui.slnx b/src/CommunityToolkit.Maui.slnx index bba33d4380..cd52b727af 100644 --- a/src/CommunityToolkit.Maui.slnx +++ b/src/CommunityToolkit.Maui.slnx @@ -18,6 +18,7 @@ + diff --git a/src/CommunityToolkit.Maui/Behaviors/AnimationBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/AnimationBehavior.shared.cs index afa9446b19..de62dff003 100644 --- a/src/CommunityToolkit.Maui/Behaviors/AnimationBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/AnimationBehavior.shared.cs @@ -18,24 +18,6 @@ public partial class AnimationBehavior : EventToCommandBehavior behavior.SetBinding(AnimationBehavior.AnimateCommandProperty, nameof(ViewModel.TriggerAnimationCommand)); """; - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty AnimationTypeProperty = - BindableProperty.Create(nameof(AnimationType), typeof(BaseAnimation), typeof(AnimationBehavior)); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty AnimateCommandProperty = - BindableProperty.CreateReadOnly(nameof(AnimateCommand), typeof(Command), typeof(AnimationBehavior), default, BindingMode.OneWayToSource, propertyChanging: OnAnimateCommandChanging, defaultValueCreator: CreateAnimateCommand).BindableProperty; - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty AnimateOnTapProperty = - BindableProperty.Create(nameof(AnimateOnTap), typeof(bool), typeof(AnimationBehavior), propertyChanged: OnAnimateOnTapPropertyChanged); - TapGestureRecognizer? tapGestureRecognizer; /// @@ -49,35 +31,25 @@ public partial class AnimationBehavior : EventToCommandBehavior /// /// has a of Command<CancellationToken> which requires a as a CommandParameter. See and for more information on passing a into as a CommandParameter" /// - public Command AnimateCommand + [BindableProperty(DefaultValue = null, DefaultBindingMode = BindingMode.OneWayToSource, PropertyChangingMethodName = nameof(OnAnimateCommandChanging), DefaultValueCreatorMethodName = nameof(CreateAnimateCommand))] + public partial Command AnimateCommand { - get => (Command)GetValue(AnimateCommandProperty); - + get; [Obsolete(animateCommandSetterWarning), EditorBrowsable(EditorBrowsableState.Never)] - set - { - Trace.WriteLine(animateCommandSetterWarning); - SetValue(AnimateCommandProperty, value); - } + set; } /// /// The type of animation to perform. /// - public BaseAnimation? AnimationType - { - get => (BaseAnimation?)GetValue(AnimationTypeProperty); - set => SetValue(AnimationTypeProperty, value); - } + [BindableProperty] + public partial BaseAnimation? AnimationType { get; set; } /// /// Whether a TapGestureRecognizer is added to the control or not /// - public bool AnimateOnTap - { - get => (bool)GetValue(AnimateOnTapProperty); - set => SetValue(AnimateOnTapProperty, value); - } + [BindableProperty(PropertyChangedMethodName = nameof(OnAnimateOnTapPropertyChanged))] + public partial bool AnimateOnTap { get; set; } /// protected override void OnAttachedTo(VisualElement bindable) diff --git a/src/CommunityToolkit.Maui/Behaviors/EventToCommandBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/EventToCommandBehavior.shared.cs index 928b9c14fe..ca57df8704 100644 --- a/src/CommunityToolkit.Maui/Behaviors/EventToCommandBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/EventToCommandBehavior.shared.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.ComponentModel; +using System.Globalization; using System.Reflection; using System.Windows.Input; using CommunityToolkit.Maui.Converters; @@ -10,30 +11,6 @@ namespace CommunityToolkit.Maui.Behaviors; /// public partial class EventToCommandBehavior : BaseBehavior { - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty EventNameProperty = - BindableProperty.Create(nameof(EventName), typeof(string), typeof(EventToCommandBehavior), propertyChanged: OnEventNamePropertyChanged); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty CommandProperty = - BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(EventToCommandBehavior)); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty CommandParameterProperty = - BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(EventToCommandBehavior)); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty EventArgsConverterProperty = - BindableProperty.Create(nameof(EventArgsConverter), typeof(IValueConverter), typeof(EventToCommandBehavior)); - readonly MethodInfo eventHandlerMethodInfo = typeof(EventToCommandBehavior).GetTypeInfo().GetDeclaredMethod(nameof(OnTriggerHandled)) ?? throw new InvalidOperationException($"Cannot find method {nameof(OnTriggerHandled)}"); Delegate? eventHandler; @@ -43,38 +20,26 @@ public partial class EventToCommandBehavior : BaseBehavior /// /// The name of the event that should be associated with . This is bindable property. /// - public string? EventName - { - get => (string?)GetValue(EventNameProperty); - set => SetValue(EventNameProperty, value); - } + [BindableProperty(PropertyChangedMethodName = nameof(OnEventNamePropertyChanged))] + public partial string? EventName { get; set; } /// /// The Command that should be executed when the event configured with is triggered. This is a bindable property. /// - public ICommand? Command - { - get => (ICommand?)GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } + [BindableProperty] + public partial ICommand? Command { get; set; } /// /// An optional parameter to forward to the . This is a bindable property. /// - public object? CommandParameter - { - get => GetValue(CommandParameterProperty); - set => SetValue(CommandParameterProperty, value); - } + [BindableProperty] + public partial object? CommandParameter { get; set; } /// /// An optional that can be used to convert values, associated with the event configured with , to values passed into the . This is a bindable property. /// - public IValueConverter? EventArgsConverter - { - get => (IValueConverter?)GetValue(EventArgsConverterProperty); - set => SetValue(EventArgsConverterProperty, value); - } + [BindableProperty] + public partial IValueConverter? EventArgsConverter { get; set; } /// protected override void OnAttachedTo(VisualElement bindable) diff --git a/src/CommunityToolkit.Maui/Behaviors/MaskedBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/MaskedBehavior.shared.cs index a9a9bde807..d2e7b33002 100644 --- a/src/CommunityToolkit.Maui/Behaviors/MaskedBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/MaskedBehavior.shared.cs @@ -8,18 +8,6 @@ namespace CommunityToolkit.Maui.Behaviors; /// public partial class MaskedBehavior : BaseBehavior, IDisposable { - /// - /// BindableProperty for the property. - /// - public static readonly BindableProperty MaskProperty = - BindableProperty.Create(nameof(Mask), typeof(string), typeof(MaskedBehavior), propertyChanged: OnMaskPropertyChanged); - - /// - /// BindableProperty for the property. - /// - public static readonly BindableProperty UnmaskedCharacterProperty = - BindableProperty.Create(nameof(UnmaskedCharacter), typeof(char), typeof(MaskedBehavior), 'X', propertyChanged: OnUnmaskedCharacterPropertyChanged); - readonly SemaphoreSlim applyMaskSemaphoreSlim = new(1, 1); bool isDisposed; @@ -34,24 +22,18 @@ public partial class MaskedBehavior : BaseBehavior, IDisposable /// /// The mask that the input value needs to match. This is a bindable property. /// - public string? Mask - { - get => (string?)GetValue(MaskProperty); - set => SetValue(MaskProperty, value); - } + [BindableProperty(PropertyChangedMethodName = nameof(OnMaskPropertyChanged))] + public partial string? Mask { get; set; } /// /// Gets or sets which character in the property that will be visible and entered by a user. Defaults to 'X'. This is a bindable property. ///
- /// By default the 'X' character will be unmasked therefore a of "XX XX XX" would display "12 34 56". + /// By default, the 'X' character will be unmasked therefore a of "XX XX XX" would display "12 34 56". /// If you wish to include 'X' in your then you could set this to something else /// e.g. '0' and then use a of "00X00X00" which would then display "12X34X56". ///
- public char UnmaskedCharacter - { - get => (char)GetValue(UnmaskedCharacterProperty); - set => SetValue(UnmaskedCharacterProperty, value); - } + [BindableProperty(DefaultValueCreatorMethodName = nameof(UnmaskedCharacterValueCreator), PropertyChangedMethodName = nameof(OnUnmaskedCharacterPropertyChanged))] + public partial char UnmaskedCharacter { get; set; } /// public void Dispose() @@ -87,6 +69,8 @@ protected override async void OnViewPropertyChanged(InputView sender, PropertyCh } } + static object UnmaskedCharacterValueCreator(BindableObject bindable) => 'X'; + static void OnMaskPropertyChanged(BindableObject bindable, object oldValue, object newValue) { var mask = (string?)newValue; @@ -104,7 +88,7 @@ static async void OnUnmaskedCharacterPropertyChanged(BindableObject bindable, ob Task OnTextPropertyChanged(CancellationToken token) { // Android does not play well when we update the Text inside the TextChanged event. - // Therefore if we dispatch the mechanism of updating the Text property it solves the issue of the caret position being updated incorrectly. + // Therefore, if we dispatch the mechanism of updating the Text property it solves the issue of the caret position being updated incorrectly. // https://github.com/CommunityToolkit/Maui/issues/460 return View?.Dispatcher.DispatchAsync(() => ApplyMask(View?.Text, token)) ?? Task.CompletedTask; } diff --git a/src/CommunityToolkit.Maui/Behaviors/MaxLengthReachedBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/MaxLengthReachedBehavior.shared.cs index 2445d152bf..c2465a28f0 100644 --- a/src/CommunityToolkit.Maui/Behaviors/MaxLengthReachedBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/MaxLengthReachedBehavior.shared.cs @@ -10,35 +10,17 @@ public partial class MaxLengthReachedBehavior : BaseBehavior { readonly WeakEventManager maxLengthReachedEventManager = new(); - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty CommandProperty - = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(MaxLengthReachedBehavior)); - /// /// Command that is triggered when the value configured in is reached. Both the event and this command are triggered. This is a bindable property. /// - public ICommand? Command - { - get => (ICommand?)GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty ShouldDismissKeyboardAutomaticallyProperty - = BindableProperty.Create(nameof(ShouldDismissKeyboardAutomatically), typeof(bool), typeof(MaxLengthReachedBehavior), false); + [BindableProperty] + public partial ICommand? Command { get; set; } /// - /// Indicates whether or not the keyboard should be dismissed automatically after the maximum length is reached. This is a bindable property. + /// Indicates whether the keyboard should be dismissed automatically after the maximum length is reached. This is a bindable property. /// - public bool ShouldDismissKeyboardAutomatically - { - get => (bool)GetValue(ShouldDismissKeyboardAutomaticallyProperty); - set => SetValue(ShouldDismissKeyboardAutomaticallyProperty, value); - } + [BindableProperty(DefaultValue = false)] + public partial bool ShouldDismissKeyboardAutomatically { get; set; } /// /// Event that is triggered when the value configured in is reached. Both the and this event are triggered. This is a bindable property. diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/IconTintColor/IconTintColorBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/IconTintColor/IconTintColorBehavior.shared.cs index bef0c66c33..0dea419356 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/IconTintColor/IconTintColorBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/IconTintColor/IconTintColorBehavior.shared.cs @@ -5,18 +5,9 @@ /// public partial class IconTintColorBehavior : BasePlatformBehavior { - /// - /// Attached Bindable Property for the . - /// - public static readonly BindableProperty TintColorProperty = - BindableProperty.Create(nameof(TintColor), typeof(Color), typeof(IconTintColorBehavior), default); - /// /// Property that represents the that Icon will be tinted. /// - public Color? TintColor - { - get => (Color?)GetValue(TintColorProperty); - set => SetValue(TintColorProperty, value); - } + [BindableProperty] + public partial Color? TintColor { get; set; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs index f0d81bb450..d2e5e2100a 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/ImageTouch/ImageTouchBehavior.shared.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using CommunityToolkit.Maui.Core; namespace CommunityToolkit.Maui.Behaviors; @@ -7,129 +8,45 @@ namespace CommunityToolkit.Maui.Behaviors; /// public partial class ImageTouchBehavior : TouchBehavior { - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultImageSourceProperty = BindableProperty.Create( - nameof(DefaultImageSource), - typeof(ImageSource), - typeof(TouchBehavior), - ImageTouchBehaviorDefaults.DefaultBackgroundImageSource); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredBackgroundImageSourceProperty = BindableProperty.Create( - nameof(HoveredImageSource), - typeof(ImageSource), - typeof(TouchBehavior), - ImageTouchBehaviorDefaults.HoveredBackgroundImageSource); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedBackgroundImageSourceProperty = BindableProperty.Create( - nameof(PressedImageSource), - typeof(ImageSource), - typeof(TouchBehavior), - ImageTouchBehaviorDefaults.PressedBackgroundImageSource); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultImageAspectProperty = BindableProperty.Create( - nameof(DefaultImageAspect), - typeof(Aspect), - typeof(TouchBehavior), - ImageTouchBehaviorDefaults.DefaultBackgroundImageAspect); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredImageAspectProperty = BindableProperty.Create( - nameof(HoveredImageAspect), - typeof(Aspect), - typeof(TouchBehavior), - ImageTouchBehaviorDefaults.HoveredBackgroundImageAspect); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedImageAspectProperty = BindableProperty.Create( - nameof(PressedImageAspect), - typeof(Aspect), - typeof(TouchBehavior), - ImageTouchBehaviorDefaults.PressedBackgroundImageAspect); - - /// - /// Bindable property for - /// - public static readonly BindableProperty ShouldSetImageOnAnimationEndProperty = BindableProperty.Create( - nameof(ShouldSetImageOnAnimationEnd), - typeof(bool), - typeof(TouchBehavior), - ImageTouchBehaviorDefaults.ShouldSetImageOnAnimationEnd); - /// /// Gets or sets the when is . /// - public ImageSource? DefaultImageSource - { - get => (ImageSource?)GetValue(DefaultImageSourceProperty); - set => SetValue(DefaultImageSourceProperty, value); - } + [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.DefaultBackgroundImageSource)] + public partial ImageSource? DefaultImageSource { get; set; } /// /// Gets or sets the when the is /// - public ImageSource? HoveredImageSource - { - get => (ImageSource?)GetValue(HoveredBackgroundImageSourceProperty); - set => SetValue(HoveredBackgroundImageSourceProperty, value); - } + [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.HoveredBackgroundImageSource)] + public partial ImageSource? HoveredImageSource { get; set; } /// /// Gets or sets the when the is /// - public ImageSource? PressedImageSource - { - get => (ImageSource?)GetValue(PressedBackgroundImageSourceProperty); - set => SetValue(PressedBackgroundImageSourceProperty, value); - } + [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.PressedBackgroundImageSource)] + public partial ImageSource? PressedImageSource { get; set; } /// /// Gets or sets the when is . /// - public Aspect DefaultImageAspect - { - get => (Aspect)GetValue(DefaultImageAspectProperty); - set => SetValue(DefaultImageAspectProperty, value); - } + [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.DefaultBackgroundImageAspect)] + public partial Aspect DefaultImageAspect { get; set; } /// /// Gets or sets the when is . /// - public Aspect HoveredImageAspect - { - get => (Aspect)GetValue(HoveredImageAspectProperty); - set => SetValue(HoveredImageAspectProperty, value); - } + [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.HoveredBackgroundImageAspect)] + public partial Aspect HoveredImageAspect { get; set; } /// /// Gets or sets the when the is /// - public Aspect PressedImageAspect - { - get => (Aspect)GetValue(PressedImageAspectProperty); - set => SetValue(PressedImageAspectProperty, value); - } + [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.PressedBackgroundImageAspect)] + public partial Aspect PressedImageAspect { get; set; } /// /// Gets or sets a value indicating whether the image should be set when the animation ends. /// - public bool ShouldSetImageOnAnimationEnd - { - get => (bool)GetValue(ShouldSetImageOnAnimationEndProperty); - set => SetValue(ShouldSetImageOnAnimationEndProperty, value); - } + [BindableProperty(DefaultValue = ImageTouchBehaviorDefaults.ShouldSetImageOnAnimationEnd)] + public partial bool ShouldSetImageOnAnimationEnd { get; set; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs index 521342b6cd..c6ed7b7daf 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/StatusBar/StatusBarBehavior.shared.cs @@ -28,50 +28,23 @@ public enum StatusBarApplyOn [UnsupportedOSPlatform("Windows"), UnsupportedOSPlatform("MacCatalyst"), UnsupportedOSPlatform("MacOS"), UnsupportedOSPlatform("Tizen")] public partial class StatusBarBehavior : BasePlatformBehavior { - /// - /// that manages the StatusBarColor property. - /// - public static readonly BindableProperty StatusBarColorProperty = - BindableProperty.Create(nameof(StatusBarColor), typeof(Color), typeof(StatusBarBehavior), Colors.Transparent); - - /// - /// that manages the StatusBarColor property. - /// - public static readonly BindableProperty StatusBarStyleProperty = - BindableProperty.Create(nameof(StatusBarStyle), typeof(StatusBarStyle), typeof(StatusBarBehavior), StatusBarStyle.Default); - - /// - /// that manages the ApplyOn property. - /// - public static readonly BindableProperty ApplyOnProperty = - BindableProperty.Create(nameof(ApplyOn), typeof(StatusBarApplyOn), typeof(StatusBarBehavior), StatusBarApplyOn.OnBehaviorAttachedTo); - /// /// Property that holds the value of the Status bar color. /// - public Color StatusBarColor - { - get => (Color)GetValue(StatusBarColorProperty); - set => SetValue(StatusBarColorProperty, value); - } + [BindableProperty(DefaultValueCreatorMethodName = nameof(StatusBarColorValueCreator))] + public partial Color StatusBarColor { get; set; } /// /// Property that holds the value of the Status bar color. /// - public StatusBarStyle StatusBarStyle - { - get => (StatusBarStyle)GetValue(StatusBarStyleProperty); - set => SetValue(StatusBarStyleProperty, value); - } + [BindableProperty(DefaultValue = StatusBarStyle.Default)] + public partial StatusBarStyle StatusBarStyle { get; set; } /// /// When the status bar color and style should be applied. /// - public StatusBarApplyOn ApplyOn - { - get => (StatusBarApplyOn)GetValue(ApplyOnProperty); - set => SetValue(ApplyOnProperty, value); - } + [BindableProperty(DefaultValue = StatusBarApplyOn.OnBehaviorAttachedTo)] + public partial StatusBarApplyOn ApplyOn { get; set; } #if !(WINDOWS || MACCATALYST || TIZEN) @@ -164,4 +137,6 @@ protected override void OnPropertyChanged([CallerMemberName] string? propertyNam } } #endif + + static Color StatusBarColorValueCreator(BindableObject bindable) => Colors.Transparent; } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/GestureManager.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/GestureManager.shared.cs index cad7fc04f6..59d9155f78 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/GestureManager.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/GestureManager.shared.cs @@ -286,7 +286,7 @@ static async Task SetImageSource(TouchBehavior touchBehavior, TouchState touchSt bindable.SetValue(ImageElement.AspectProperty, imageTouchBehavior.PressedImageAspect); } - if (imageTouchBehavior.IsSet(ImageTouchBehavior.PressedBackgroundImageSourceProperty)) + if (imageTouchBehavior.IsSet(ImageTouchBehavior.PressedImageSourceProperty)) { bindable.SetValue(ImageElement.SourceProperty, imageTouchBehavior.PressedImageSource); } @@ -302,7 +302,7 @@ static async Task SetImageSource(TouchBehavior touchBehavior, TouchState touchSt bindable.SetValue(ImageElement.AspectProperty, imageTouchBehavior.DefaultImageAspect); } - if (imageTouchBehavior.IsSet(ImageTouchBehavior.HoveredBackgroundImageSourceProperty)) + if (imageTouchBehavior.IsSet(ImageTouchBehavior.HoveredImageSourceProperty)) { bindable.SetValue(ImageElement.SourceProperty, imageTouchBehavior.HoveredImageSource); } diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.methods.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.methods.shared.cs index dfec3980ef..711e18e5d5 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.methods.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.methods.shared.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using CommunityToolkit.Maui.Core; + namespace CommunityToolkit.Maui.Behaviors; public partial class TouchBehavior : IDisposable @@ -108,6 +109,77 @@ protected virtual void Dispose(bool disposing) isDisposed = true; } + static async void RaiseCurrentTouchStateChanged(BindableObject bindable, object oldValue, object newValue) + { + var touchBehavior = (TouchBehavior)bindable; + + await Task.WhenAll(touchBehavior.ForceUpdateState(CancellationToken.None), touchBehavior.HandleLongPress(CancellationToken.None)); + touchBehavior.weakEventManager.HandleEvent(touchBehavior, new TouchStateChangedEventArgs(touchBehavior.CurrentTouchState), nameof(CurrentTouchStateChanged)); + } + + static void RaiseInteractionStatusChanged(BindableObject bindable, object oldValue, object newValue) + { + var touchBehavior = (TouchBehavior)bindable; + touchBehavior.weakEventManager.HandleEvent(touchBehavior, new TouchInteractionStatusChangedEventArgs(touchBehavior.CurrentInteractionStatus), nameof(InteractionStatusChanged)); + } + + static void RaiseCurrentTouchStatusChanged(BindableObject bindable, object oldValue, object newValue) + { + var touchBehavior = (TouchBehavior)bindable; + touchBehavior.weakEventManager.HandleEvent(touchBehavior, new TouchStatusChangedEventArgs(touchBehavior.CurrentTouchStatus), nameof(CurrentTouchStatusChanged)); + } + + static async void RaiseHoverStateChanged(BindableObject bindable, object oldValue, object newValue) + { + var touchBehavior = (TouchBehavior)bindable; + + await touchBehavior.ForceUpdateState(CancellationToken.None); + touchBehavior.weakEventManager.HandleEvent(touchBehavior, new HoverStateChangedEventArgs(touchBehavior.CurrentHoverState), nameof(HoverStateChanged)); + } + + static void RaiseHoverStatusChanged(BindableObject bindable, object oldValue, object newValue) + { + var touchBehavior = (TouchBehavior)bindable; + + touchBehavior.weakEventManager.HandleEvent(touchBehavior, new HoverStatusChangedEventArgs(touchBehavior.CurrentHoverStatus), nameof(HoverStatusChanged)); + } + + static void HandleDefaultOpacityChanging(BindableObject bindable, object oldValue, object newValue) + { + var defaultOpacity = (double)newValue; + switch (defaultOpacity) + { + case < 0: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(DefaultOpacity)} must be greater than 0"); + case > 1: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(DefaultOpacity)} must be less than 1"); + } + } + + static void HandleHoveredOpacityChanging(BindableObject bindable, object oldValue, object newValue) + { + var hoveredOpacity = (double)newValue; + switch (hoveredOpacity) + { + case < 0: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(HoveredOpacity)} must be greater than 0"); + case > 1: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(HoveredOpacity)} must be less than 1"); + } + } + + static void HandlePressedOpacityChanging(BindableObject bindable, object oldValue, object newValue) + { + var pressedOpacity = (double)newValue; + switch (pressedOpacity) + { + case < 0: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(PressedOpacity)} must be greater than 0"); + case > 1: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(PressedOpacity)} must be less than 1"); + } + } + async Task HandleLongPress(CancellationToken token) { if (Element is null) @@ -165,26 +237,5 @@ void OnLayoutChildAdded(object? sender, ElementEventArgs e) view.InputTransparent = IsEnabled; } - async Task RaiseCurrentTouchStateChanged(CancellationToken token) - { - await Task.WhenAll(ForceUpdateState(token), HandleLongPress(token)); - weakEventManager.HandleEvent(this, new TouchStateChangedEventArgs(CurrentTouchState), nameof(CurrentTouchStateChanged)); - } - - void RaiseInteractionStatusChanged() - => weakEventManager.HandleEvent(this, new TouchInteractionStatusChangedEventArgs(CurrentInteractionStatus), nameof(InteractionStatusChanged)); - - void RaiseCurrentTouchStatusChanged() - => weakEventManager.HandleEvent(this, new TouchStatusChangedEventArgs(CurrentTouchStatus), nameof(CurrentTouchStatusChanged)); - - async Task RaiseHoverStateChanged(CancellationToken token) - { - await ForceUpdateState(token); - weakEventManager.HandleEvent(this, new HoverStateChangedEventArgs(CurrentHoverState), nameof(HoverStateChanged)); - } - - void RaiseHoverStatusChanged() - => weakEventManager.HandleEvent(this, new HoverStatusChangedEventArgs(CurrentHoverStatus), nameof(HoverStatusChanged)); - partial void PlatformDispose(); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.shared.cs index 9ba172d742..a68fb859d4 100644 --- a/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/PlatformBehaviors/Touch/TouchBehavior.shared.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.Diagnostics; using System.Windows.Input; using CommunityToolkit.Maui.Core; @@ -25,403 +26,6 @@ public partial class TouchBehavior : BasePlatformBehavior /// public const string HoveredVisualState = "Hovered"; - /// - /// Bindable property for - /// - public static readonly BindableProperty IsEnabledProperty = BindableProperty.Create( - nameof(IsEnabled), - typeof(bool), - typeof(TouchBehavior), - TouchBehaviorDefaults.IsEnabled); - - /// - /// Bindable property for - /// - public static readonly BindableProperty CommandProperty = BindableProperty.Create( - nameof(Command), - typeof(ICommand), - typeof(TouchBehavior), - null); - - /// - /// Bindable property for - /// - public static readonly BindableProperty ShouldMakeChildrenInputTransparentProperty = BindableProperty.Create( - nameof(ShouldMakeChildrenInputTransparent), - typeof(bool), - typeof(TouchBehavior), - TouchBehaviorDefaults.ShouldMakeChildrenInputTransparent); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DisallowTouchThresholdProperty = BindableProperty.Create( - nameof(DisallowTouchThreshold), - typeof(int), - typeof(TouchBehavior), - TouchBehaviorDefaults.DisallowTouchThreshold); - - /// - /// Bindable property for - /// - public static readonly BindableProperty LongPressCommandProperty = BindableProperty.Create( - nameof(LongPressCommand), - typeof(ICommand), - typeof(TouchBehavior), - null); - - /// - /// Bindable property for - /// - public static readonly BindableProperty CurrentTouchStatusProperty = BindableProperty.Create( - nameof(CurrentTouchStatus), - typeof(TouchStatus), - typeof(TouchBehavior), - TouchBehaviorDefaults.CurrentTouchStatus, - BindingMode.OneWayToSource, - propertyChanged: static (bindable, _, _) => ((TouchBehavior)bindable).RaiseCurrentTouchStatusChanged()); - - /// - /// Bindable property for - /// - public static readonly BindableProperty LongPressDurationProperty = BindableProperty.Create( - nameof(LongPressDuration), - typeof(int), - typeof(TouchBehavior), - TouchBehaviorDefaults.LongPressDuration); - - /// - /// Bindable property for - /// - public static readonly BindableProperty LongPressCommandParameterProperty = BindableProperty.Create( - nameof(LongPressCommandParameter), - typeof(object), - typeof(TouchBehavior), - default); - - /// - /// Bindable property for - /// - public static readonly BindableProperty CurrentHoverStatusProperty = BindableProperty.Create( - nameof(CurrentHoverStatus), - typeof(HoverStatus), - typeof(TouchBehavior), - TouchBehaviorDefaults.CurrentHoverStatus, - BindingMode.OneWayToSource, - propertyChanged: static (bindable, _, _) => ((TouchBehavior)bindable).RaiseHoverStatusChanged()); - - /// - /// Bindable property for - /// - public static readonly BindableProperty CurrentInteractionStatusProperty = BindableProperty.Create( - nameof(CurrentInteractionStatus), - typeof(TouchInteractionStatus), - typeof(TouchBehavior), - TouchBehaviorDefaults.CurrentInteractionStatus, - BindingMode.OneWayToSource, - propertyChanged: static (bindable, _, _) => ((TouchBehavior)bindable).RaiseInteractionStatusChanged()); - - /// - /// Bindable property for - /// - public static readonly BindableProperty CurrentTouchStateProperty = BindableProperty.Create( - nameof(CurrentTouchState), - typeof(TouchState), - typeof(TouchBehavior), - TouchBehaviorDefaults.CurrentTouchState, - BindingMode.OneWayToSource, - propertyChanged: static async (bindable, _, _) => await ((TouchBehavior)bindable).RaiseCurrentTouchStateChanged(CancellationToken.None)); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultBackgroundColorProperty = BindableProperty.Create( - nameof(DefaultBackgroundColor), - typeof(Color), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultBackgroundColor); - - /// - /// Bindable property for - /// - public static readonly BindableProperty CurrentHoverStateProperty = BindableProperty.Create( - nameof(CurrentHoverState), - typeof(HoverState), - typeof(TouchBehavior), - TouchBehaviorDefaults.CurrentHoverState, - BindingMode.OneWayToSource, - propertyChanged: static async (bindable, _, _) => await ((TouchBehavior)bindable).RaiseHoverStateChanged(CancellationToken.None)); - - /// - /// Bindable property for - /// - public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create( - nameof(CommandParameter), - typeof(object), - typeof(TouchBehavior), - default); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultScaleProperty = BindableProperty.Create( - nameof(DefaultScale), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultScale); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedOpacityProperty = BindableProperty.Create( - nameof(PressedOpacity), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedOpacity); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredOpacityProperty = BindableProperty.Create( - nameof(HoveredOpacity), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredOpacity); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultOpacityProperty = BindableProperty.Create( - nameof(DefaultOpacity), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultOpacity); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedScaleProperty = BindableProperty.Create( - nameof(PressedScale), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedScale); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredScaleProperty = BindableProperty.Create( - nameof(HoveredScale), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredScale); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedBackgroundColorProperty = BindableProperty.Create( - nameof(PressedBackgroundColor), - typeof(Color), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedBackgroundColor); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredBackgroundColorProperty = BindableProperty.Create( - nameof(HoveredBackgroundColor), - typeof(Color), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredBackgroundColor); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedTranslationXProperty = BindableProperty.Create( - nameof(PressedTranslationX), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedTranslationX); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredTranslationXProperty = BindableProperty.Create( - nameof(HoveredTranslationX), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredTranslationX); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredTranslationYProperty = BindableProperty.Create( - nameof(HoveredTranslationY), - typeof(double), - typeof(TouchBehavior), - 0.0); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultTranslationXProperty = BindableProperty.Create( - nameof(DefaultTranslationX), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultTranslationX); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedRotationXProperty = BindableProperty.Create( - nameof(PressedRotationX), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedRotationX); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredRotationXProperty = BindableProperty.Create( - nameof(HoveredRotationX), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredRotationX); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultRotationXProperty = BindableProperty.Create( - nameof(DefaultRotationX), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultRotationX); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedRotationProperty = BindableProperty.Create( - nameof(PressedRotation), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedRotation); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedRotationYProperty = BindableProperty.Create( - nameof(PressedRotationY), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedRotationY); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultRotationYProperty = BindableProperty.Create( - nameof(DefaultRotationY), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultRotationY); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredRotationProperty = BindableProperty.Create( - nameof(HoveredRotation), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredRotation); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultRotationProperty = BindableProperty.Create( - nameof(DefaultRotation), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultRotation); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedTranslationYProperty = BindableProperty.Create( - nameof(PressedTranslationY), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedTranslationY); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredAnimationEasingProperty = BindableProperty.Create( - nameof(HoveredAnimationEasing), - typeof(Easing), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredAnimationEasing); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredRotationYProperty = BindableProperty.Create( - nameof(HoveredRotationY), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredRotationY); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultTranslationYProperty = BindableProperty.Create( - nameof(DefaultTranslationY), - typeof(double), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultTranslationY); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedAnimationDurationProperty = BindableProperty.Create( - nameof(PressedAnimationDuration), - typeof(int), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedAnimationDuration); - - /// - /// Bindable property for - /// - public static readonly BindableProperty HoveredAnimationDurationProperty = BindableProperty.Create( - nameof(HoveredAnimationDuration), - typeof(int), - typeof(TouchBehavior), - TouchBehaviorDefaults.HoveredAnimationDuration); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultAnimationEasingProperty = BindableProperty.Create( - nameof(DefaultAnimationEasing), - typeof(Easing), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultAnimationEasing); - - /// - /// Bindable property for - /// - public static readonly BindableProperty DefaultAnimationDurationProperty = BindableProperty.Create( - nameof(DefaultAnimationDuration), - typeof(int), - typeof(TouchBehavior), - TouchBehaviorDefaults.DefaultAnimationDuration); - - /// - /// Bindable property for - /// - public static readonly BindableProperty PressedAnimationEasingProperty = BindableProperty.Create( - nameof(PressedAnimationEasing), - typeof(Easing), - typeof(TouchBehavior), - TouchBehaviorDefaults.PressedAnimationEasing); - readonly WeakEventManager weakEventManager = new(); readonly GestureManager gestureManager = new(); @@ -491,426 +95,261 @@ public event EventHandler LongPressCompleted /// /// Gets or sets a value indicating whether the behavior is enabled. /// - public bool IsEnabled - { - get => (bool)GetValue(IsEnabledProperty); - set => SetValue(IsEnabledProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.IsEnabled)] + public partial bool IsEnabled { get; set; } /// /// Gets or sets a value indicating whether the children of the element should be made input transparent. /// - public bool ShouldMakeChildrenInputTransparent - { - get => (bool)GetValue(ShouldMakeChildrenInputTransparentProperty); - set => SetValue(ShouldMakeChildrenInputTransparentProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.ShouldMakeChildrenInputTransparent)] + public partial bool ShouldMakeChildrenInputTransparent { get; set; } /// /// Gets or sets the to invoke when the user has completed a touch gesture. /// - public ICommand? Command - { - get => (ICommand?)GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } + [BindableProperty(DefaultValue = null)] + public partial ICommand? Command { get; set; } /// - /// Gets or sets the to invoke when the user has completed a long press. + /// Gets or sets the parameter to pass to the property. /// - public ICommand? LongPressCommand - { - get => (ICommand?)GetValue(LongPressCommandProperty); - set => SetValue(LongPressCommandProperty, value); - } + [BindableProperty(DefaultValue = null)] + public partial object? CommandParameter { get; set; } /// - /// Gets or sets the parameter to pass to the property. + /// Gets or sets the to invoke when the user has completed a long press. /// - public object? CommandParameter - { - get => (object?)GetValue(CommandParameterProperty); - set => SetValue(CommandParameterProperty, value); - } + [BindableProperty(DefaultValue = null)] + public partial ICommand? LongPressCommand { get; set; } /// /// Gets or sets the parameter to pass to the property. /// - public object? LongPressCommandParameter - { - get => (object?)GetValue(LongPressCommandParameterProperty); - set => SetValue(LongPressCommandParameterProperty, value); - } + [BindableProperty(DefaultValue = null)] + public partial object? LongPressCommandParameter { get; set; } /// /// Gets or sets the duration required to trigger the long press gesture. /// - public int LongPressDuration - { - get => (int)GetValue(LongPressDurationProperty); - set => SetValue(LongPressDurationProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.LongPressDuration)] + public partial int LongPressDuration { get; set; } /// /// Gets the current of the behavior. /// - public TouchStatus CurrentTouchStatus - { - get => (TouchStatus)GetValue(CurrentTouchStatusProperty); - set => SetValue(CurrentTouchStatusProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.CurrentTouchStatus, PropertyChangedMethodName = nameof(RaiseCurrentTouchStatusChanged), DefaultBindingMode = BindingMode.OneWayToSource)] + public partial TouchStatus CurrentTouchStatus { get; set; } /// /// Gets the current of the behavior. /// - public TouchState CurrentTouchState - { - get => (TouchState)GetValue(CurrentTouchStateProperty); - set => SetValue(CurrentTouchStateProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.CurrentTouchState, DefaultBindingMode = BindingMode.OneWayToSource, PropertyChangedMethodName = nameof(RaiseCurrentTouchStateChanged))] + public partial TouchState CurrentTouchState { get; set; } /// /// Gets the current of the behavior. /// - public TouchInteractionStatus CurrentInteractionStatus - { - get => (TouchInteractionStatus)GetValue(CurrentInteractionStatusProperty); - set => SetValue(CurrentInteractionStatusProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.CurrentInteractionStatus, DefaultBindingMode = BindingMode.OneWayToSource, PropertyChangedMethodName = nameof(RaiseInteractionStatusChanged))] + public partial TouchInteractionStatus CurrentInteractionStatus { get; set; } /// /// Gets the current of the behavior. /// - public HoverStatus CurrentHoverStatus - { - get => (HoverStatus)GetValue(CurrentHoverStatusProperty); - set => SetValue(CurrentHoverStatusProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.CurrentHoverStatus, DefaultBindingMode = BindingMode.OneWayToSource, PropertyChangedMethodName = nameof(RaiseHoverStatusChanged))] + public partial HoverStatus CurrentHoverStatus { get; set; } /// /// Gets the current of the behavior. /// - public HoverState CurrentHoverState - { - get => (HoverState)GetValue(CurrentHoverStateProperty); - set => SetValue(CurrentHoverStateProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.CurrentHoverState, DefaultBindingMode = BindingMode.OneWayToSource, PropertyChangedMethodName = nameof(RaiseHoverStateChanged))] + public partial HoverState CurrentHoverState { get; set; } /// /// Gets or sets the background color of the element when the is . /// - public Color? DefaultBackgroundColor - { - get => (Color?)GetValue(DefaultBackgroundColorProperty); - set => SetValue(DefaultBackgroundColorProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultBackgroundColor)] + public partial Color? DefaultBackgroundColor { get; set; } /// /// Gets or sets the background color of the element when the is . /// - public Color? HoveredBackgroundColor - { - get => (Color?)GetValue(HoveredBackgroundColorProperty); - set => SetValue(HoveredBackgroundColorProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredBackgroundColor)] + public partial Color? HoveredBackgroundColor { get; set; } /// /// Gets or sets the background color of the element when the is . /// - public Color? PressedBackgroundColor - { - get => (Color?)GetValue(PressedBackgroundColorProperty); - set => SetValue(PressedBackgroundColorProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedBackgroundColor)] + public partial Color? PressedBackgroundColor { get; set; } /// /// Gets or sets the opacity of the element when the is . /// /// - public double DefaultOpacity - { - get => (double)GetValue(DefaultOpacityProperty); - set - { - switch (value) - { - case < 0: - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(DefaultOpacity)} must be greater than 0"); - case > 1: - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(DefaultOpacity)} must be less than 1"); - default: - SetValue(DefaultOpacityProperty, value); - break; - } - } - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultOpacity, PropertyChangingMethodName = nameof(HandleDefaultOpacityChanging))] + public partial double DefaultOpacity { get; set; } /// /// Gets or sets the opacity of the element when the is . /// - public double HoveredOpacity - { - get => (double)GetValue(HoveredOpacityProperty); - set - { - switch (value) - { - case < 0: - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(HoveredOpacity)} must be greater than 0"); - case > 1: - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(HoveredOpacity)} must be less than 1"); - default: - SetValue(HoveredOpacityProperty, value); - break; - } - } - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredOpacity, PropertyChangingMethodName = nameof(HandleHoveredOpacityChanging))] + public partial double HoveredOpacity { get; set; } /// /// Gets or sets the opacity of the element when the is . /// - public double PressedOpacity - { - get => (double)GetValue(PressedOpacityProperty); - set - { - switch (value) - { - case < 0: - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(PressedOpacity)} must be greater than 0"); - case > 1: - throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(PressedOpacity)} must be less than 1"); - default: - SetValue(PressedOpacityProperty, value); - break; - } - } - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedOpacity, PropertyChangingMethodName = nameof(HandlePressedOpacityChanging))] + public partial double PressedOpacity { get; set; } /// /// Gets or sets the scale of the element when the is . /// - public double DefaultScale - { - get => (double)GetValue(DefaultScaleProperty); - set => SetValue(DefaultScaleProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultScale)] + public partial double DefaultScale { get; set; } /// /// Gets or sets the scale of the element when the is . /// - public double HoveredScale - { - get => (double)GetValue(HoveredScaleProperty); - set => SetValue(HoveredScaleProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredScale)] + public partial double HoveredScale { get; set; } /// /// Gets or sets the scale of the element when the is . /// - public double PressedScale - { - get => (double)GetValue(PressedScaleProperty); - set => SetValue(PressedScaleProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedScale)] + public partial double PressedScale { get; set; } /// - /// Gets or sets the translation X of the element when when the is . + /// Gets or sets the translation X of the element when the is . /// - public double DefaultTranslationX - { - get => (double)GetValue(DefaultTranslationXProperty); - set => SetValue(DefaultTranslationXProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultTranslationX)] + public partial double DefaultTranslationX { get; set; } /// /// Gets or sets the translation X of the element when the is . /// - public double HoveredTranslationX - { - get => (double)GetValue(HoveredTranslationXProperty); - set => SetValue(HoveredTranslationXProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredTranslationX)] + public partial double HoveredTranslationX { get; set; } /// /// Gets or sets the translation X of the element when the is . /// - public double PressedTranslationX - { - get => (double)GetValue(PressedTranslationXProperty); - set => SetValue(PressedTranslationXProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedTranslationX)] + public partial double PressedTranslationX { get; set; } /// - /// Gets or sets the translation Y of the element when when the is . + /// Gets or sets the translation Y of the element when the is . /// - public double DefaultTranslationY - { - get => (double)GetValue(DefaultTranslationYProperty); - set => SetValue(DefaultTranslationYProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultTranslationY)] + public partial double DefaultTranslationY { get; set; } /// /// Gets or sets the translation Y of the element when the is . /// - public double HoveredTranslationY - { - get => (double)GetValue(HoveredTranslationYProperty); - set => SetValue(HoveredTranslationYProperty, value); - } + [BindableProperty(DefaultValue = 0.0)] + public partial double HoveredTranslationY { get; set; } /// /// Gets or sets the translation Y of the element when the is . /// - public double PressedTranslationY - { - get => (double)GetValue(PressedTranslationYProperty); - set => SetValue(PressedTranslationYProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedTranslationY)] + public partial double PressedTranslationY { get; set; } /// - /// Gets or sets the rotation of the element when when the is . + /// Gets or sets the rotation of the element when the is . /// - public double DefaultRotation - { - get => (double)GetValue(DefaultRotationProperty); - set => SetValue(DefaultRotationProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultRotation)] + public partial double DefaultRotation { get; set; } /// /// Gets or sets the rotation of the element when the is . /// - public double HoveredRotation - { - get => (double)GetValue(HoveredRotationProperty); - set => SetValue(HoveredRotationProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredRotation)] + public partial double HoveredRotation { get; set; } /// /// Gets or sets the rotation of the element when the is . /// - public double PressedRotation - { - get => (double)GetValue(PressedRotationProperty); - set => SetValue(PressedRotationProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedRotation)] + public partial double PressedRotation { get; set; } /// - /// Gets or sets the rotation X of the element when when the is . + /// Gets or sets the rotation X of the element when the is . /// - public double DefaultRotationX - { - get => (double)GetValue(DefaultRotationXProperty); - set => SetValue(DefaultRotationXProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultRotationX)] + public partial double DefaultRotationX { get; set; } /// /// Gets or sets the rotation X of the element when the is . /// - public double HoveredRotationX - { - get => (double)GetValue(HoveredRotationXProperty); - set => SetValue(HoveredRotationXProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredRotationX)] + public partial double HoveredRotationX { get; set; } /// /// Gets or sets the rotation X of the element when the is . /// - public double PressedRotationX - { - get => (double)GetValue(PressedRotationXProperty); - set => SetValue(PressedRotationXProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedRotationX)] + public partial double PressedRotationX { get; set; } /// - /// Gets or sets the rotation Y of the element when when the is . + /// Gets or sets the rotation Y of the element when the is . /// - public double DefaultRotationY - { - get => (double)GetValue(DefaultRotationYProperty); - set => SetValue(DefaultRotationYProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultRotationY)] + public partial double DefaultRotationY { get; set; } /// /// Gets or sets the rotation Y of the element when the is . /// - public double HoveredRotationY - { - get => (double)GetValue(HoveredRotationYProperty); - set => SetValue(HoveredRotationYProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredRotationY)] + public partial double HoveredRotationY { get; set; } /// /// Gets or sets the rotation Y of the element when the is . /// - public double PressedRotationY - { - get => (double)GetValue(PressedRotationYProperty); - set => SetValue(PressedRotationYProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedRotationY)] + public partial double PressedRotationY { get; set; } /// /// Gets or sets the duration of the animation when the is . /// - public int PressedAnimationDuration - { - get => (int)GetValue(PressedAnimationDurationProperty); - set => SetValue(PressedAnimationDurationProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedAnimationDuration)] + public partial int PressedAnimationDuration { get; set; } /// /// Gets or sets the easing of the animation when the is . /// - public Easing? PressedAnimationEasing - { - get => (Easing?)GetValue(PressedAnimationEasingProperty); - set => SetValue(PressedAnimationEasingProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.PressedAnimationEasing)] + public partial Easing? PressedAnimationEasing { get; set; } /// /// Gets or sets the duration of the animation when is . /// - public int DefaultAnimationDuration - { - get => (int)GetValue(DefaultAnimationDurationProperty); - set => SetValue(DefaultAnimationDurationProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultAnimationDuration)] + public partial int DefaultAnimationDuration { get; set; } /// /// Gets or sets the of the animation when is . /// - public Easing? DefaultAnimationEasing - { - get => (Easing?)GetValue(DefaultAnimationEasingProperty); - set => SetValue(DefaultAnimationEasingProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DefaultAnimationEasing)] + public partial Easing? DefaultAnimationEasing { get; set; } /// /// Gets or sets the duration of the animation when the is . /// - public int HoveredAnimationDuration - { - get => (int)GetValue(HoveredAnimationDurationProperty); - set => SetValue(HoveredAnimationDurationProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredAnimationDuration)] + public partial int HoveredAnimationDuration { get; set; } /// /// Gets or sets the easing of the animation when the is . /// - public Easing? HoveredAnimationEasing - { - get => (Easing?)GetValue(HoveredAnimationEasingProperty); - set => SetValue(HoveredAnimationEasingProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.HoveredAnimationEasing)] + public partial Easing? HoveredAnimationEasing { get; set; } /// /// Gets or sets the threshold for disallowing touch. /// - public int DisallowTouchThreshold - { - get => (int)GetValue(DisallowTouchThresholdProperty); - set => SetValue(DisallowTouchThresholdProperty, value); - } + [BindableProperty(DefaultValue = TouchBehaviorDefaults.DisallowTouchThreshold)] + public partial int DisallowTouchThreshold { get; set; } internal bool CanExecute => IsEnabled && Element?.IsEnabled is true diff --git a/src/CommunityToolkit.Maui/Behaviors/ProgressBarAnimationBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/ProgressBarAnimationBehavior.shared.cs index fcdfbc6ccc..b28107491c 100644 --- a/src/CommunityToolkit.Maui/Behaviors/ProgressBarAnimationBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/ProgressBarAnimationBehavior.shared.cs @@ -1,4 +1,5 @@ -using ProgressBar = Microsoft.Maui.Controls.ProgressBar; +using CommunityToolkit.Maui.Core; +using ProgressBar = Microsoft.Maui.Controls.ProgressBar; namespace CommunityToolkit.Maui.Behaviors; @@ -9,24 +10,6 @@ public partial class ProgressBarAnimationBehavior : BaseBehavior { readonly WeakEventManager animationCompletedEventManager = new(); - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty ProgressProperty = - BindableProperty.CreateAttached(nameof(Progress), typeof(double), typeof(ProgressBarAnimationBehavior), 0.0d, propertyChanged: OnAnimateProgressPropertyChanged); - - /// - /// BindableProperty for the property - /// - public static readonly BindableProperty LengthProperty = - BindableProperty.CreateAttached(nameof(Length), typeof(uint), typeof(ProgressBarAnimationBehavior), (uint)500); - - /// - /// BindableProperty for the property - /// - public static readonly BindableProperty EasingProperty = - BindableProperty.CreateAttached(nameof(Easing), typeof(Easing), typeof(ProgressBarAnimationBehavior), Easing.Linear); - /// /// Event that is triggered when the ProgressBar.ProgressTo() animation completes /// @@ -39,29 +22,20 @@ public event EventHandler AnimationCompleted /// /// Value of , clamped to a minimum value of 0 and a maximum value of 1 /// - public double Progress - { - get => (double)GetValue(ProgressProperty); - set => SetValue(ProgressProperty, Math.Clamp(value, 0, 1)); - } + [BindableProperty(DefaultValue = ProgressBarAnimationBehaviorDefaults.Progress, PropertyChangingMethodName = nameof(OnAnimateProgressPropertyChanging), PropertyChangedMethodName = nameof(OnAnimateProgressPropertyChanged))] + public partial double Progress { get; set; } /// /// Length in milliseconds of the progress bar animation /// - public uint Length - { - get => (uint)GetValue(LengthProperty); - set => SetValue(LengthProperty, value); - } + [BindableProperty(DefaultValue = ProgressBarAnimationBehaviorDefaults.Length)] + public partial uint Length { get; set; } /// /// Easing of the progress bar animation /// - public Easing Easing - { - get => (Easing)GetValue(EasingProperty); - set => SetValue(EasingProperty, value); - } + [BindableProperty(DefaultValueCreatorMethodName = nameof(EasingValueCreator))] + public partial Easing Easing { get; set; } static async void OnAnimateProgressPropertyChanged(BindableObject bindable, object oldValue, object newValue) { @@ -79,6 +53,21 @@ await AnimateProgress(progressBarAnimationBehavior.View, } } + static void OnAnimateProgressPropertyChanging(BindableObject bindable, object oldValue, object newValue) + { + var progress = (double)newValue; + switch (progress) + { + case < 0: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(Progress)} must be greater than 0"); + case > 1: + throw new ArgumentOutOfRangeException(nameof(newValue), newValue, $"{nameof(Progress)} must be less than 1"); + } + } + + + static Easing EasingValueCreator(BindableObject bindable) => ProgressBarAnimationBehaviorDefaults.Easing; + static Task AnimateProgress(in ProgressBar progressBar, in double progress, in uint animationLength, in Easing animationEasing, in CancellationToken token) { return progressBar.ProgressTo(progress, animationLength, animationEasing).WaitAsync(token); diff --git a/src/CommunityToolkit.Maui/Behaviors/UserStoppedTypingBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/UserStoppedTypingBehavior.shared.cs index 470d8aea07..0c2075204f 100644 --- a/src/CommunityToolkit.Maui/Behaviors/UserStoppedTypingBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/UserStoppedTypingBehavior.shared.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Windows.Input; +using CommunityToolkit.Maui.Core; namespace CommunityToolkit.Maui.Behaviors; @@ -9,36 +10,6 @@ namespace CommunityToolkit.Maui.Behaviors; /// public partial class UserStoppedTypingBehavior : BaseBehavior, IDisposable { - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty CommandProperty - = BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(UserStoppedTypingBehavior)); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty CommandParameterProperty - = BindableProperty.Create(nameof(CommandParameter), typeof(object), typeof(UserStoppedTypingBehavior)); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty StoppedTypingTimeThresholdProperty - = BindableProperty.Create(nameof(StoppedTypingTimeThreshold), typeof(int), typeof(UserStoppedTypingBehavior), 1000); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty MinimumLengthThresholdProperty - = BindableProperty.Create(nameof(MinimumLengthThreshold), typeof(int), typeof(UserStoppedTypingBehavior), 0); - - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty ShouldDismissKeyboardAutomaticallyProperty - = BindableProperty.Create(nameof(ShouldDismissKeyboardAutomatically), typeof(bool), typeof(UserStoppedTypingBehavior), false); - CancellationTokenSource? tokenSource; bool isDisposed; @@ -48,47 +19,32 @@ public static readonly BindableProperty ShouldDismissKeyboardAutomaticallyProper /// /// Command that is triggered when the is reached. When is set, it's only triggered when both conditions are met. This is a bindable property. /// - public ICommand? Command - { - get => (ICommand?)GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } + [BindableProperty] + public partial ICommand? Command { get; set; } /// /// An optional parameter to forward to the . This is a bindable property. /// - public object? CommandParameter - { - get => GetValue(CommandParameterProperty); - set => SetValue(CommandParameterProperty, value); - } + [BindableProperty] + public partial object? CommandParameter { get; set; } /// /// The time of inactivity in milliseconds after which will be executed. If is also set, the condition there also needs to be met. This is a bindable property. /// - public int StoppedTypingTimeThreshold - { - get => (int)GetValue(StoppedTypingTimeThresholdProperty); - set => SetValue(StoppedTypingTimeThresholdProperty, value); - } + [BindableProperty(DefaultValue = UserStoppedTypingBehaviorDefaults.StoppedTypingTimeThreshold)] + public partial int StoppedTypingTimeThreshold { get; set; } /// /// The minimum length of the input value required before will be executed but only after has passed. This is a bindable property. /// - public int MinimumLengthThreshold - { - get => (int)GetValue(MinimumLengthThresholdProperty); - set => SetValue(MinimumLengthThresholdProperty, value); - } + [BindableProperty(DefaultValue = UserStoppedTypingBehaviorDefaults.MinimumLengthThreshold)] + public partial int MinimumLengthThreshold { get; set; } /// - /// Indicates whether or not the keyboard should be dismissed automatically after the user stopped typing. This is a bindable property. + /// Indicates whether the keyboard should be dismissed automatically after the user stopped typing. This is a bindable property. /// - public bool ShouldDismissKeyboardAutomatically - { - get => (bool)GetValue(ShouldDismissKeyboardAutomaticallyProperty); - set => SetValue(ShouldDismissKeyboardAutomaticallyProperty, value); - } + [BindableProperty(DefaultValue = UserStoppedTypingBehaviorDefaults.ShouldDismissKeyboardAutomatically)] + public partial bool ShouldDismissKeyboardAutomatically { get; set; } /// public void Dispose() diff --git a/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs index bb56ecf741..f532f7683b 100644 --- a/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs @@ -10,6 +10,12 @@ namespace CommunityToolkit.Maui.Views; [RequiresUnreferencedCode("Calls Microsoft.Maui.Controls.Binding.Binding(String, BindingMode, IValueConverter, Object, String, Object)")] public partial class Expander : ContentView, IExpander { + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty DirectionProperty + = BindableProperty.Create(nameof(Direction), typeof(ExpandDirection), typeof(Expander), ExpandDirection.Down, propertyChanged: OnDirectionPropertyChanged); + /// /// Gets or sets the command to execute when the expander is expanded or collapsed. /// @@ -40,12 +46,6 @@ public partial class Expander : ContentView, IExpander [BindableProperty(PropertyChangedMethodName = nameof(OnHeaderPropertyChanged))] public partial IView Header { get; set; } - /// - /// Backing BindableProperty for the property. - /// - public static readonly BindableProperty DirectionProperty - = BindableProperty.Create(nameof(Direction), typeof(ExpandDirection), typeof(Expander), ExpandDirection.Down, propertyChanged: OnDirectionPropertyChanged); - readonly WeakEventManager tappedEventManager = new(); ///