Skip to content

Commit

Permalink
feat: Support CommunityToolkit.Mvvm in two way x:Bind
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed Apr 24, 2023
1 parent 5ec71ed commit 8ffc159
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ private bool IsUnoFluentAssembly
internal Lazy<INamedTypeSymbol> SolidColorBrushSymbol { get; }
internal Lazy<INamedTypeSymbol> RowDefinitionSymbol { get; }
internal Lazy<INamedTypeSymbol> ColumnDefinitionSymbol { get; }
internal Lazy<INamedTypeSymbol> TaskSymbol { get; }
internal Lazy<INamedTypeSymbol?> IRelayCommandSymbol { get; }
internal Lazy<INamedTypeSymbol?> IRelayCommandTSymbol { get; }
internal Lazy<INamedTypeSymbol?> IAsyncRelayCommandSymbol { get; }
internal Lazy<INamedTypeSymbol?> IAsyncRelayCommandTSymbol { get; }

public XamlCodeGeneration(GeneratorExecutionContext context)
{
Expand Down Expand Up @@ -307,6 +312,11 @@ public XamlCodeGeneration(GeneratorExecutionContext context)
SolidColorBrushSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.SolidColorBrush);
RowDefinitionSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.RowDefinition);
ColumnDefinitionSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.ColumnDefinition);
TaskSymbol = GetMandatorySymbolAsLazy("System.Threading.Tasks");
IRelayCommandSymbol = GetOptionalSymbolAsLazy("CommunityToolkit.Mvvm.Input.IRelayCommand");
IRelayCommandTSymbol = GetOptionalSymbolAsLazy("CommunityToolkit.Mvvm.Input.IRelayCommand`1");
IAsyncRelayCommandSymbol = GetOptionalSymbolAsLazy("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand");
IAsyncRelayCommandTSymbol = GetOptionalSymbolAsLazy("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand`1");

Lazy<INamedTypeSymbol> GetMandatorySymbolAsLazy(string fullyQualifiedName)
=> new(() => context.Compilation.GetTypeByMetadataName(fullyQualifiedName) ?? throw new InvalidOperationException($"Unable to find type {fullyQualifiedName}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4467,19 +4467,9 @@ private ITypeSymbol GetXBindPropertyPathType(string propertyPath, INamedTypeSymb

foreach (var part in parts)
{
if (currentType.GetAllMembersWithName(part).FirstOrDefault() is ISymbol member)
if (TryGetPartType(currentType, part) is ITypeSymbol partType)
{
var propertySymbol = member as IPropertySymbol;
var fieldSymbol = member as IFieldSymbol;

if (propertySymbol != null || fieldSymbol != null)
{
currentType = propertySymbol?.Type ?? fieldSymbol?.Type!;
}
else
{
throw new InvalidOperationException($"Cannot use member [{part}] of type [{member}], as it is not a property of a field");
}
currentType = partType;
}
else if (FindSubElementByName(_fileDefinition.Objects.First(), part) is XamlObjectDefinition elementByName)
{
Expand All @@ -4492,11 +4482,102 @@ private ITypeSymbol GetXBindPropertyPathType(string propertyPath, INamedTypeSymb
}
else
{
throw new InvalidOperationException($"Unable to find member [{part}] on type [{currentType}]");
// We can't find the type. It could be something that is source-generated, or it could be a user error.
// For source-generated members, it's not possible to reliably get this information due to https://github.com/dotnet/roslyn/issues/57239
// However, we do a best effort to handle the common scenario, which is code generated by CommunityToolkit.Mvvm.

// Case 1: ObservableProperty attribute.
// Relevant code: https://github.com/CommunityToolkit/dotnet/blob/b341ef91fe66101444d6b811bc5dff32de029d3c/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs#L1231-L1244
// The user defines a field named '_abc', 'abc', 'm_abc', '_Abc', or 'm_Abc'.
// The generated property will be named 'Abc', which is what the user will be referencing in x:Bind.
ITypeSymbol? mvvmType = TryGetMvvmObservablePropertyType(currentType, part);

// Case 2: RelayCommand attribute
// Relevant code: https://github.com/CommunityToolkit/dotnet/blob/b341ef91fe66101444d6b811bc5dff32de029d3c/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs#L455-L496
// and also: https://github.com/CommunityToolkit/dotnet/blob/b341ef91fe66101444d6b811bc5dff32de029d3c/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs#L522-L614
// The user defines a method named 'OnAbcAsync', 'AbcAsync', 'OnAbc', 'Abc'.
// The generated command property will be named 'AbcCommand'
mvvmType ??= TryGetMvvmRelayCommandType(currentType, part);
currentType = mvvmType ?? throw new InvalidOperationException($"Unable to find member [{part}] on type [{currentType}]");
}
}

return currentType;

static ITypeSymbol? TryGetPartType(ITypeSymbol currentType, string part)
{
if (currentType.GetAllMembersWithName(part).FirstOrDefault() is ISymbol member)
{
return member switch
{
IPropertySymbol propertySymbol => propertySymbol.Type,
IFieldSymbol fieldSymbol => fieldSymbol.Type,
// We avoid throwing here in case the method is a relay command, in which case we want to get into
// the TryGetMvvmRelayCommandType code path.
IMethodSymbol methodSymbol => null,
_ => throw new InvalidOperationException($"Cannot use member [{part}] of type [{member}], as it is not a property of a field"),
};
}

return null;
}

static ITypeSymbol? TryGetMvvmObservablePropertyType(ITypeSymbol currentType, string part)
{
if (part.Length > 0 && char.IsUpper(part[0]))
{
// We could first check if the field has [ObservableAttribute], but it doesn't add enough value.
var firstCharLower = char.ToLower(part[0], CultureInfo.InvariantCulture);
var firstCharStripped = part.Substring(1);
var mvvmType = TryGetPartType(currentType, $"_{firstCharLower}{firstCharStripped}");
mvvmType ??= TryGetPartType(currentType, $"{firstCharLower}{firstCharStripped}");
mvvmType ??= TryGetPartType(currentType, $"m_{firstCharLower}{firstCharStripped}");
mvvmType ??= TryGetPartType(currentType, $"_{part}");
mvvmType ??= TryGetPartType(currentType, $"m_{part}");
return mvvmType;
}

return null;
}

ITypeSymbol? TryGetMvvmRelayCommandType(ITypeSymbol currentType, string part)
{
const string CommandSuffix = "Command";
if (part.EndsWith(CommandSuffix, StringComparison.Ordinal))
{
// We could first check if the method has [RelayCommand], but it doesn't add enough value.
part = part.Substring(0, part.Length - CommandSuffix.Length);
var method = currentType.GetAllMembersWithName(part).FirstOrDefault() as IMethodSymbol;
method ??= currentType.GetAllMembersWithName($"On{part}").FirstOrDefault() as IMethodSymbol;
method ??= currentType.GetAllMembersWithName($"On{part}Async").FirstOrDefault() as IMethodSymbol;
method ??= currentType.GetAllMembersWithName($"{part}Async").FirstOrDefault() as IMethodSymbol;

if (method is not null)
{
if (method.ReturnsVoid && method.Parameters.Length == 0)
{
return Generation.IRelayCommandSymbol.Value;
}
else if (method.ReturnsVoid && method.Parameters.Length == 1)
{
return Generation.IRelayCommandTSymbol.Value?.Construct(method.Parameters[0].Type);
}
else if (method.ReturnType is INamedTypeSymbol namedType && namedType.Is(Generation.TaskSymbol.Value))
{
if (method.Parameters.Length == 0)
{
return Generation.IAsyncRelayCommandSymbol.Value;
}
else if (method.Parameters.Length is 1 or 2)
{
return Generation.IAsyncRelayCommandTSymbol.Value?.Construct(method.Parameters[0].Type);
}
}
}
}

return null;
}
}

private bool IsStaticMember(string fullMemberName)
Expand Down

0 comments on commit 8ffc159

Please sign in to comment.