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
30 changes: 17 additions & 13 deletions src/Controls/src/SourceGen/NodeSGExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -599,21 +599,25 @@ public static (IFieldSymbol?, IPropertySymbol?) GetFieldOrBP(ITypeSymbol owner,
return (bpFieldSymbol, property);
}

public static IFieldSymbol GetBindableProperty(this ValueNode node, SourceGenContext context)
/// <summary>
/// Gets the TargetType symbol from a parent node (Style, Trigger, DataTrigger, MultiTrigger).
/// Used to resolve bindable properties when only the property name is specified.
/// </summary>
public static ITypeSymbol? GetTargetTypeSymbol(INode node, SourceGenContext context)
{
static ITypeSymbol? GetTargetTypeSymbol(INode node, SourceGenContext context)
{
var ttnode = (node as ElementNode)?.Properties[new XmlName("", "TargetType")];
//it's either a value
if (ttnode is ValueNode { Value: string tt })
return XmlTypeExtensions.GetTypeSymbol(tt, context, node);
//or a x:Type that we parsed earlier
if (context.Types.TryGetValue(ttnode!, out var typeSymbol))
return typeSymbol;
//FIXME: report diagnostic on missing TargetType
return null;
}
var ttnode = (node as ElementNode)?.Properties[new XmlName("", "TargetType")];
//it's either a value
if (ttnode is ValueNode { Value: string tt })
return XmlTypeExtensions.GetTypeSymbol(tt, context, node);
//or a x:Type that we parsed earlier
if (ttnode != null && context.Types.TryGetValue(ttnode, out var typeSymbol))
return typeSymbol;
//FIXME: report diagnostic on missing TargetType
return null;
}

public static IFieldSymbol GetBindableProperty(this ValueNode node, SourceGenContext context)
{
var parts = ((string)node.Value).Split('.');
if (parts.Length == 1)
{
Expand Down
81 changes: 76 additions & 5 deletions src/Controls/src/SourceGen/SetterValueProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ public bool TryProvideValue(ElementNode node, IndentedTextWriter writer, SourceG
}

var bpNode = (ValueNode)node.Properties[new XmlName("", "Property")];
var bpRef = bpNode.GetBindableProperty(context);
IFieldSymbol? bpRef = bpNode.GetBindableProperty(context);
if (!TryGetBindablePropertyNameAndType(bpRef, bpNode, context, out var bpName, out var bpType))
{
value = string.Empty;
return false;
}

string targetsetter;
if (node.Properties.TryGetValue(new XmlName("", "TargetName"), out var targetNode))
Expand All @@ -53,18 +58,32 @@ public bool TryProvideValue(ElementNode node, IndentedTextWriter writer, SourceG

if (valueNode is ValueNode vn)
{
value = $"new global::Microsoft.Maui.Controls.Setter {{{targetsetter}Property = {bpRef.ToFQDisplayString()}, Value = {vn.ConvertTo(bpRef, writer, context)}}}";
string valueString;
if (bpRef != null)
{
valueString = vn.ConvertTo(bpRef, writer, context);
}
else if (bpType != null)
{
valueString = vn.ConvertTo(bpType, null, writer, context);
}
else
{
value = string.Empty;
return false;
}
value = $"new global::Microsoft.Maui.Controls.Setter {{{targetsetter}Property = {bpName}, Value = {valueString}}}";
return true;
}
else if (getNodeValue != null)
{
var lvalue = getNodeValue(valueNode, bpRef.Type);
value = $"new global::Microsoft.Maui.Controls.Setter {{{targetsetter}Property = {bpRef.ToFQDisplayString()}, Value = {lvalue.ValueAccessor}}}";
var lvalue = getNodeValue(valueNode, bpType ?? context.Compilation.ObjectType);
value = $"new global::Microsoft.Maui.Controls.Setter {{{targetsetter}Property = {bpName}, Value = {lvalue.ValueAccessor}}}";
return true;
}
else if (context.Variables.TryGetValue(valueNode, out var variable))
{
value = $"new global::Microsoft.Maui.Controls.Setter {{{targetsetter}Property = {bpRef.ToFQDisplayString()}, Value = {variable.ValueAccessor}}}";
value = $"new global::Microsoft.Maui.Controls.Setter {{{targetsetter}Property = {bpName}, Value = {variable.ValueAccessor}}}";
return true;
}

Expand All @@ -73,6 +92,58 @@ public bool TryProvideValue(ElementNode node, IndentedTextWriter writer, SourceG
return false;
}

/// <summary>
/// Gets the bindable property name and type for a Setter's Property attribute.
/// Uses the resolved field symbol if available, otherwise uses heuristics for source-generated properties.
/// </summary>
/// <returns>True if the property could be resolved, false otherwise.</returns>
private static bool TryGetBindablePropertyNameAndType(IFieldSymbol? bpRef, ValueNode bpNode, SourceGenContext context, out string bpName, out ITypeSymbol? bpType)
{
if (bpRef != null)
{
bpName = bpRef.ToFQDisplayString();
bpType = bpRef.GetBPTypeAndConverter(context)?.type;
return true;
}

var propertyText = bpNode.Value as string ?? string.Empty;
var parts = propertyText.Split('.');

ITypeSymbol? targetType = null;
string propertyName;

if (parts.Length == 1)
{
propertyName = parts[0];
var parent = bpNode.Parent?.Parent as ElementNode ?? (bpNode.Parent?.Parent as IListNode)?.Parent as ElementNode;
Comment thread
simonrozsival marked this conversation as resolved.
if (parent?.XmlType.IsOfAnyType("Trigger", "DataTrigger", "MultiTrigger", "Style") == true)
targetType = NodeSGExtensions.GetTargetTypeSymbol(parent, context);
}
else if (parts.Length == 2)
{
targetType = XmlTypeExtensions.GetTypeSymbol(parts[0], context, bpNode);
propertyName = parts[1];
}
else
{
bpName = string.Empty;
bpType = null;
return false;
}

if (targetType != null && targetType.HasBindablePropertyHeuristic(propertyName, context, out var explicitPropertyName))
{
var bpFieldName = explicitPropertyName ?? $"{propertyName}Property";
bpName = $"{targetType.ToFQDisplayString()}.{bpFieldName}";
bpType = targetType.GetAllProperties(propertyName, context).FirstOrDefault()?.Type;
return true;
}

bpName = string.Empty;
bpType = null;
return false;
}

/// <summary>
/// Shared helper to get the value node from a Setter element.
/// Checks properties first, then collection items.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,70 @@ public class BalanceView : Label
Assert.NotNull(generated);
Assert.Contains(".Balance = ", generated, StringComparison.Ordinal);
}

[Fact]
public void StyleSetter_WithSourceGeneratedBindableProperty_ShouldGenerateInlineSetter()
{
var xaml =
"""
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Test"
x:Class="Test.TestPage">
<ContentPage.Resources>
<Style TargetType="local:BalanceView">
<Setter Property="Balance" Value="42.5" />
</Style>
</ContentPage.Resources>
<local:BalanceView />
</ContentPage>
""";

var code =
"""
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace CommunityToolkit.Maui
{
// Simulates an attribute from a third-party library
public class BindablePropertyAttribute : Attribute
{
public string? PropertyName { get; set; }
}
}

namespace Test
{
[XamlProcessing(XamlInflator.SourceGen)]
public partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
}
}

public class BalanceView : Label
{
[CommunityToolkit.Maui.BindableProperty]
public double Balance { get; set; }
}
}
""";

// assertNoCompilationErrors: false because BalanceProperty is expected to be generated by another source generator
var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: false);

// Should NOT have errors
var errorDiagnostics = result.Diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error).ToList();
Assert.Empty(errorDiagnostics);

// Should have generated code with the Setter using the heuristically determined BalanceProperty
Assert.NotNull(generated);
Assert.Contains("Property = global::Test.BalanceView.BalanceProperty", generated, StringComparison.Ordinal);
}
}
Loading