Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using CommunityToolkit.Maui.SourceGenerators.Helpers;
Expand All @@ -21,15 +22,19 @@ public class BindablePropertyAttributeSourceGenerator : IIncrementalGenerator
const string bpAttribute =
/* language=C#-test */
//lang=csharp
"""
$$"""
// <auto-generated>
// 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<TReturnType> : 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;
Expand All @@ -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> 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(
Expand Down Expand Up @@ -124,17 +166,16 @@ static void GenerateBindableProperty(StringBuilder sb, BindablePropertyModel inf

static void GenerateProperty(StringBuilder sb, BindablePropertyModel info)
{
/*
/// <inheritdoc />
*/
sb.AppendLine("/// <inheritdoc />");

//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)
Expand All @@ -148,45 +189,45 @@ static void GenerateProperty(StringBuilder sb, BindablePropertyModel info)

static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
{
var classDeclarationSyntax = Unsafe.As<ClassDeclarationSyntax>(context.TargetNode);
var propertyDeclarationSyntax = Unsafe.As<PropertyDeclarationSyntax>(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<BindablePropertyModel>(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");
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();
var propertyName = attributeData.GetConstructorArgumentsAttributeValueByNameAsString(defaultName);
var validateValueMethodName = attributeData.GetNamedArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName));

return new BindablePropertyModel
Expand All @@ -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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

}
37 changes: 31 additions & 6 deletions src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,41 @@
namespace CommunityToolkit.Maui.Views;

/// <inheritdoc cref="IExpander"/>
[BindableProperty<IView>("Header", PropertyChangedMethodName = nameof(OnHeaderPropertyChanged))]
[BindableProperty<IView>("Content", PropertyChangedMethodName = nameof(OnContentPropertyChanged))]
[BindableProperty<bool>("IsExpanded", PropertyChangedMethodName = nameof(OnIsExpandedPropertyChanged))]
[BindableProperty<object>("CommandParameter")]
[BindableProperty<ICommand>("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
{
/// <summary>
/// Gets or sets the command to execute when the expander is expanded or collapsed.
/// </summary>
[BindableProperty]
public partial ICommand Command { get; set; }

/// <summary>
/// Gets or sets the parameter to pass to the <see cref="Command"/> property.
/// </summary>
[BindableProperty]
public partial object CommandParameter { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the expander is expanded.
/// </summary>
[BindableProperty(PropertyChangedMethodName = nameof(OnIsExpandedPropertyChanged))]
public partial bool IsExpanded { get; set; }

/// <summary>
/// Gets or sets the content to be expanded or collapsed.
/// </summary>
[BindableProperty(PropertyChangedMethodName = nameof(OnContentPropertyChanged))]
public partial IView Content { get; set; }

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (windows-latest)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-15)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-15)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-15)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (macos-15)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

Check warning on line 36 in src/CommunityToolkit.Maui/Views/Expander/Expander.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

'Expander.Content' hides inherited member 'ContentView.Content'. Use the new keyword if hiding was intended.

/// <summary>
/// Gets or sets the header view of the expander.
/// </summary>
[BindableProperty(PropertyChangedMethodName = nameof(OnHeaderPropertyChanged))]
public partial IView Header { get; set; }

/// <summary>
/// Backing BindableProperty for the <see cref="Direction"/> property.
/// </summary>
Expand Down Expand Up @@ -43,7 +68,7 @@
}

/// <summary>
/// Triggered when the value of <see cref="IsExpanded"/> changes
/// Triggered when the value of <see cref="IsExpanded"/> changes.
/// </summary>
public event EventHandler<ExpandedChangedEventArgs> ExpandedChanged
{
Expand Down
Loading