diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs index a494eacdf2..21cd6b6513 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/BindablePropertyAttributeSourceGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; using CommunityToolkit.Maui.SourceGenerators.Helpers; @@ -21,15 +22,19 @@ public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator const string bpAttribute = /* language=C#-test */ //lang=csharp - """ + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.BindablePropertyAttributeSourceGenerator + #pragma warning disable #nullable enable namespace CommunityToolkit.Maui; - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class BindablePropertyAttribute : Attribute + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + sealed partial class BindablePropertyAttribute : Attribute { - public string PropertyName { get; } = string.Empty; + public string? PropertyName { get; } public Type? DeclaringType { get; set; } public object? DefaultValue { get; set; } public string DefaultBindingMode { get; set; } = string.Empty; @@ -43,30 +48,67 @@ public BindablePropertyAttribute(string propertyName) { PropertyName = propertyName; } + + public BindablePropertyAttribute() + { + } } """; public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => ctx.AddSource("BindablePropertyAttribute.g.cs", SourceText.From(bpAttribute, Encoding.UTF8))); - var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.BindablePropertyAttribute`1", +#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() - .SelectMany(static (types, _) => types); + .Collect(); - context.RegisterSourceOutput(provider, Execute); + context.RegisterSourceOutput(provider, ExecuteAllValues); } - static void Execute(SourceProductionContext context, SemanticValues semanticValues) + static void ExecuteAllValues(SourceProductionContext context, ImmutableArray semanticValues) { - var source = GenerateSource(semanticValues); - SourceStringService.FormatText(ref source); - context.AddSource($"{semanticValues.ClassInformation.ClassName}.g.cs", SourceText.From(source, Encoding.UTF8)); + 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( @@ -124,17 +166,16 @@ static void GenerateBindableProperty(StringBuilder sb, BindablePropertyModel inf static void GenerateProperty(StringBuilder sb, BindablePropertyModel info) { - /* - /// - */ - sb.AppendLine("/// "); - - //public string Text - //{ - // get => (string)GetValue(TextProperty); - // set => SetValue(TextProperty, value); - //} - sb.AppendLine($"public {info.ReturnType} {info.PropertyName}") + // The code below creates the following Property: + // + // public partial string Text + // { + // get => (string)GetValue(TextProperty); + // set => SetValue(TextProperty, value); + // } + // + + sb.AppendLine($"public partial {info.ReturnType} {info.PropertyName}") .AppendLine("{") .Append("get => (") .Append(info.ReturnType) @@ -148,37 +189,37 @@ static void GenerateProperty(StringBuilder sb, BindablePropertyModel info) static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { - var classDeclarationSyntax = Unsafe.As(context.TargetNode); + var propertyDeclarationSyntax = Unsafe.As(context.TargetNode); var semanticModel = context.SemanticModel; - var classSymbol = (ITypeSymbol?)semanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken); + var propertySymbol = (IPropertySymbol?)semanticModel.GetDeclaredSymbol(propertyDeclarationSyntax, cancellationToken); - if (classSymbol is null) + if (propertySymbol is null) { return emptySemanticValues; } - var classInfo = new ClassInformation(classSymbol.Name, classSymbol.DeclaredAccessibility.ToString().ToLower(), classSymbol.ContainingNamespace.ToDisplayString()); + 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); - for (var index = 0; index < context.Attributes.Length; index++) - { - var attributeData = context.Attributes[index]; - bindablePropertyModels.Add(GetAttributeValues(attributeData, classSymbol?.ToString() ?? string.Empty)); - } + var attributeData = context.Attributes[0]; + bindablePropertyModels.Add(CreateBindablePropertyModel(attributeData, propertySymbol.Type.ToDisplayString(), propertySymbol.Name, returnType)); - return new(classInfo, bindablePropertyModels.ToImmutableArray()); + return new(propertyInfo, bindablePropertyModels.ToImmutableArray()); } - static BindablePropertyModel GetAttributeValues(in AttributeData attributeData, in string declaringTypeString) + static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in string declaringTypeString, in string defaultName, in ITypeSymbol returnType) { if (attributeData.AttributeClass is null) { throw new ArgumentException($"{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData.AttributeClass)); } - var bpType = attributeData.AttributeClass.TypeArguments[0]; var defaultValue = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValue)); var coerceValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName)); var defaultBindingMode = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultBindingMode), "BindingMode.OneWay"); @@ -186,7 +227,7 @@ static BindablePropertyModel GetAttributeValues(in AttributeData attributeData, 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(); + var propertyName = attributeData.GetConstructorArgumentsAttributeValueByNameAsString(defaultName); var validateValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName)); return new BindablePropertyModel @@ -199,11 +240,11 @@ static BindablePropertyModel GetAttributeValues(in AttributeData attributeData, PropertyChangedMethodName = propertyChangedMethodName, PropertyChangingMethodName = propertyChangingMethodName, PropertyName = propertyName, - ReturnType = bpType, + ReturnType = returnType, ValidateValueMethodName = validateValueMethodName }; } static bool SyntaxPredicate(SyntaxNode node, CancellationToken cancellationToken) => - node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }; + node is PropertyDeclarationSyntax { AttributeLists.Count: > 0 }; } \ 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 d2fb797b30..e9d0c66167 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Helpers/AttributeExtensions.cs @@ -17,11 +17,16 @@ public static string GetNamedArgumentsAttributeValueByNameAsString(this Attribut return data.Value is null ? placeholder : data.Value.ToString(); } - public static string GetConstructorArgumentsAttributeValueByNameAsString(this AttributeData attribute) + public static string GetConstructorArgumentsAttributeValueByNameAsString(this AttributeData attribute, string placeholder) { + if (attribute.ConstructorArguments.Length is 0) + { + return placeholder; + } + var data = attribute.ConstructorArguments[0]; - return data.Value is null ? throw new InvalidOperationException() : data.Value.ToString(); + return data.Value is null ? placeholder : data.Value.ToString(); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs index cad239f1e3..d9221b1e95 100644 --- a/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs @@ -6,16 +6,41 @@ namespace CommunityToolkit.Maui.Views; /// -[BindableProperty("Header", PropertyChangedMethodName = nameof(OnHeaderPropertyChanged))] -[BindableProperty("Content", PropertyChangedMethodName = nameof(OnContentPropertyChanged))] -[BindableProperty("IsExpanded", PropertyChangedMethodName = nameof(OnIsExpandedPropertyChanged))] -[BindableProperty("CommandParameter")] -[BindableProperty("Command")] [ContentProperty(nameof(Content))] [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] [RequiresUnreferencedCode("Calls Microsoft.Maui.Controls.Binding.Binding(String, BindingMode, IValueConverter, Object, String, Object)")] public partial class Expander : ContentView, IExpander { + /// + /// Gets or sets the command to execute when the expander is expanded or collapsed. + /// + [BindableProperty] + public partial ICommand Command { get; set; } + + /// + /// Gets or sets the parameter to pass to the property. + /// + [BindableProperty] + public partial object CommandParameter { get; set; } + + /// + /// Gets or sets a value indicating whether the expander is expanded. + /// + [BindableProperty(PropertyChangedMethodName = nameof(OnIsExpandedPropertyChanged))] + public partial bool IsExpanded { get; set; } + + /// + /// Gets or sets the content to be expanded or collapsed. + /// + [BindableProperty(PropertyChangedMethodName = nameof(OnContentPropertyChanged))] + public partial IView Content { get; set; } + + /// + /// Gets or sets the header view of the expander. + /// + [BindableProperty(PropertyChangedMethodName = nameof(OnHeaderPropertyChanged))] + public partial IView Header { get; set; } + /// /// Backing BindableProperty for the property. /// @@ -43,7 +68,7 @@ public Expander() } /// - /// Triggered when the value of changes + /// Triggered when the value of changes. /// public event EventHandler ExpandedChanged {