Skip to content

Add [AlsoNotifyCanExecuteFor] attribute #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 9, 2021
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
Expand Up @@ -181,58 +181,41 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(
string propertyName = GetGeneratedPropertyName(fieldSymbol);

INamedTypeSymbol alsoNotifyChangeForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute")!;
INamedTypeSymbol alsoNotifyCanExecuteForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute")!;
INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute");

List<StatementSyntax> dependentPropertyNotificationStatements = new();
List<StatementSyntax> dependentNotificationStatements = new();
List<AttributeSyntax> validationAttributes = new();

foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
{
// Add dependent property notifications, if needed
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyChangeForAttributeSymbol))
{
foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments)
foreach (string dependentPropertyName in attributeData.GetConstructorArguments<string>())
{
if (attributeArgument.IsNull)
{
continue;
}

if (attributeArgument.Kind == TypedConstantKind.Primitive &&
attributeArgument.Value is string dependentPropertyName)
{
propertyChangedNames.Add(dependentPropertyName);

// OnPropertyChanged("OtherPropertyName");
dependentPropertyNotificationStatements.Add(ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanged"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}"))))));
}
else if (attributeArgument.Kind == TypedConstantKind.Array)
{
foreach (TypedConstant nestedAttributeArgument in attributeArgument.Values)
{
if (nestedAttributeArgument.IsNull)
{
continue;
}

string currentPropertyName = (string)nestedAttributeArgument.Value!;

propertyChangedNames.Add(currentPropertyName);

// Additional property names
dependentPropertyNotificationStatements.Add(ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanged"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{currentPropertyName}{nameof(PropertyChangedEventArgs)}"))))));
}
}
propertyChangedNames.Add(dependentPropertyName);

// OnPropertyChanged("<PROPERTY_NAME>");
dependentNotificationStatements.Add(ExpressionStatement(
InvocationExpression(IdentifierName("OnPropertyChanged"))
.AddArgumentListArguments(Argument(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}"))))));
}
}
else if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyCanExecuteForAttributeSymbol))
{
// Add dependent relay command notifications, if needed
foreach (string commandName in attributeData.GetConstructorArguments<string>())
{
// <PROPERTY_NAME>.NotifyCanExecuteChanged();
dependentNotificationStatements.Add(ExpressionStatement(
InvocationExpression(MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(commandName),
IdentifierName("NotifyCanExecuteChanged")))));
}
}
else if (validationAttributeSymbol is not null &&
Expand Down Expand Up @@ -276,14 +259,18 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(
//
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(this.<FIELD_NAME>, value))
// {
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs); // Optional
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangingEventArgs); // Optional
// this.<FIELD_NAME> = value;
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangedEventArgs);
// ValidateProperty(value, <PROPERTY_NAME>);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_1>PropertyChangedEventArgs); // Optional
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_2>PropertyChangedEventArgs);
// ...
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_N>PropertyChangedEventArgs);
// <COMMAND_1>.NotifyCanExecuteChanged(); // Optional
// <COMMAND_2>.NotifyCanExecuteChanged();
// ...
// <COMMAND_N>.NotifyCanExecuteChanged();
// }
//
// The reason why the code is explicitly generated instead of just calling ObservableValidator.SetProperty() is so that we can
Expand Down Expand Up @@ -327,7 +314,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(
.AddArgumentListArguments(
Argument(IdentifierName("value")),
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName))))))
.AddStatements(dependentPropertyNotificationStatements.ToArray())));
.AddStatements(dependentNotificationStatements.ToArray())));
}
}
else
Expand Down Expand Up @@ -367,19 +354,23 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(
IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}"))))));

// Add the dependent property notifications at the end
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentPropertyNotificationStatements.ToArray());
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentNotificationStatements.ToArray());

// Generate the inner setter block as follows:
//
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(<FIELD_NAME>, value))
// {
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs); // Optional
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangingEventArgs); // Optional
// <FIELD_NAME> = value;
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangedEventArgs);
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_1>PropertyChangedEventArgs); // Optional
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_2>PropertyChangedEventArgs);
// ...
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_N>PropertyChangedEventArgs);
// <COMMAND_1>.NotifyCanExecuteChanged(); // Optional
// <COMMAND_2>.NotifyCanExecuteChanged();
// ...
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs);
// <COMMAND_N>.NotifyCanExecuteChanged();
// }
setterBlock = Block(
IfStatement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,42 @@ public static bool TryGetNamedArgument<T>(this AttributeData attributeData, stri
return false;
}

/// <summary>
/// Enumerates all items in a flattened sequence of constructor arguments for a given <see cref="AttributeData"/> instance.
/// </summary>
/// <typeparam name="T">The type of constructor arguments to retrieve.</typeparam>
/// <param name="attributeData">The target <see cref="AttributeData"/> instance to get the arguments from.</param>
/// <returns>A sequence of all constructor arguments of the specified type from <paramref name="attributeData"/>.</returns>
[Pure]
public static IEnumerable<T> GetConstructorArguments<T>(this AttributeData attributeData)
{
static IEnumerable<T> Enumerate(IEnumerable<TypedConstant> constants)
{
foreach (TypedConstant constant in constants)
{
if (constant.IsNull)
{
continue;
}

if (constant.Kind == TypedConstantKind.Primitive &&
constant.Value is T value)
{
yield return value;
}
else if (constant.Kind == TypedConstantKind.Array)
{
foreach (T item in Enumerate(constant.Values))
{
yield return item;
}
}
}
}

return Enumerate(attributeData.ConstructorArguments);
}

/// <summary>
/// Creates an <see cref="AttributeSyntax"/> node that is equivalent to the input <see cref="AttributeData"/> instance.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Linq;
using CommunityToolkit.Mvvm.Input;

namespace CommunityToolkit.Mvvm.ComponentModel;

/// <summary>
/// An attribute that can be used to support <see cref="IRelayCommand"/> properties in generated properties. When this attribute is
/// used, the generated property setter will also call <see cref="IRelayCommand.NotifyCanExecuteChanged"/> for the properties specified
/// in the attribute data, causing the validation logic for the command to be executed again. This can be useful to keep the code compact
/// when there are one or more dependent commands that should also be notified when a property is updated. If this attribute is used in
/// a field without <see cref="ObservablePropertyAttribute"/>, it is ignored (just like <see cref="AlsoNotifyChangeForAttribute"/>).
/// <para>
/// In order to use this attribute, the target property has to implement the <see cref="IRelayCommand"/> interface.
/// </para>
/// <para>
/// This attribute can be used as follows:
/// <code>
/// partial class MyViewModel : ObservableObject
/// {
/// [ObservableProperty]
/// [AlsoNotifyCanExecuteFor(nameof(GreetUserCommand))]
/// private string name;
///
/// public IRelayCommand GreetUserCommand { get; }
/// }
/// </code>
/// </para>
/// And with this, code analogous to this will be generated:
/// <code>
/// partial class MyViewModel
/// {
/// public string Name
/// {
/// get => name;
/// set
/// {
/// if (SetProperty(ref name, value))
/// {
/// GreetUserCommand.NotifyCanExecuteChanged();
/// }
/// }
/// }
/// }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
public sealed class AlsoNotifyCanExecuteForAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AlsoNotifyCanExecuteForAttribute"/> class.
/// </summary>
/// <param name="commandName">The name of the command to also notify when the annotated property changes.</param>
public AlsoNotifyCanExecuteForAttribute(string commandName)
{
CommandNames = new[] { commandName };
}

/// <summary>
/// Initializes a new instance of the <see cref="AlsoNotifyCanExecuteForAttribute"/> class.
/// </summary>
/// <param name="commandName">The name of the property to also notify when the annotated property changes.</param>
/// <param name="otherCommandNames">
/// The other command names to also notify when the annotated property changes. This parameter can optionally
/// be used to indicate a series of dependent commands from the same attribute, to keep the code more compact.
/// </param>
public AlsoNotifyCanExecuteForAttribute(string commandName, params string[] otherCommandNames)
{
CommandNames = new[] { commandName }.Concat(otherCommandNames).ToArray();
}

/// <summary>
/// Gets the command names to also notify when the annotated property changes.
/// </summary>
public string[] CommandNames { get; }
}
Loading