Skip to content

Commit 290a52c

Browse files
committed
feat(xBind): Add attached properties path support
1 parent d422e71 commit 290a52c

File tree

8 files changed

+189
-39
lines changed

8 files changed

+189
-39
lines changed

doc/articles/features/windows-ui-xaml-xbind.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ Uno supports the [`x:Bind`](https://docs.microsoft.com/en-us/windows/uwp/xaml-pl
8282
public void OnUncheckedRaised(object sender, RoutedEventArgs args) { }
8383
```
8484

85+
- [Attached Properties](https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension#attached-properties)
86+
```xml
87+
<Button x:Name="Button22" Content="Click me!" Grid.Row="42" />
88+
<TextBlock Text="{x:Bind Button22.(Grid.Row)}" />
89+
```
90+
8591
- Type casts
8692
```xml
8793
<TextBox FontFamily="{x:Bind (FontFamily)MyComboBox.SelectedValue}" />

src/SourceGenerators/Uno.UI.SourceGenerators.Tests.Shared/Given_XBindRewriter.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public class Given_XBindRewriter
3535
[DataRow("ctx", "MyFunction2((global::System.String),(global::System.String))", "ctx.MyFunction2(((global::System.String)ctx),((global::System.String)ctx))")]
3636
[DataRow("ctx", "(global::System.String)", "((global::System.String)ctx)")]
3737

38+
// Attached properties
39+
[DataRow("ctx", "AdornerCanvas.(MyNamespace.FrameworkElementExtensions.ActualWidth)", "MyNamespace.FrameworkElementExtensions.GetActualWidth(ctx.AdornerCanvas)")]
40+
[DataRow("ctx", "AdornerCanvas.(Grid.Row)", "Grid.GetRow(ctx.AdornerCanvas)")]
41+
[DataRow("ctx", "System.String.Format('{0:X8}', AdornerCanvas.(MyNamespace.FrameworkElementExtensions.ActualWidth))", "System.String.Format('{0:X8}', MyNamespace.FrameworkElementExtensions.GetActualWidth(ctx.AdornerCanvas))")]
42+
3843
// Not supported https://github.com/unoplatform/uno/issues/5061
3944
// [DataRow("ctx", "MyFunction((global::System.Int32)MyProperty)", "ctx.MyFunction((global::System.Int32)ctx.MyProperty)")]
4045

@@ -52,20 +57,20 @@ public class Given_XBindRewriter
5257
[DataRow("", "(FontFamily)MyProperty.A", "(FontFamily)MyProperty.A")]
5358
public void When_PathRewrite(string contextName, string inputExpression, string expectedOutput)
5459
{
55-
bool IsStaticMethod(string name)
56-
{
57-
return name switch
60+
static bool IsStaticMethod(string name) =>
61+
name switch
5862
{
5963
"MyStaticProperty" => true,
6064
"MyStaticMethod" => true,
6165
"Static.MyFunction" => true,
6266
"System.String.Format" => true,
67+
"Grid.GetRow" => true,
68+
"MyNamespace.FrameworkElementExtensions.GetActualWidth" => true,
6369
"MyNameSpace.Static2.MyFunction" => true,
6470
"MyNameSpace.Static2.MyProperty" => true,
6571
"MyNameSpace.Static2.MyEnum" => true,
6672
_ => false,
6773
};
68-
}
6974

7075
var output = XBindExpressionParser.Rewrite(contextName, inputExpression, IsStaticMethod);
7176

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/Utils/XBindExpressionParser.cs

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
using System;
77
using System.Collections.Generic;
88
using System.Linq;
9-
using System.Text;
10-
using System.Threading.Tasks;
119
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
1210

1311
namespace Uno.UI.SourceGenerators.XamlGenerator.Utils
@@ -80,10 +78,9 @@ public Rewriter(string contextName, Func<string, bool> isStaticMember)
8078
{
8179
var methodName = newSyntax.Expression.ToFullString();
8280
var arguments = newSyntax.ArgumentList.ToFullString();
83-
var output = ParseCompilationUnit($"class __Temp {{ private Func<object> __prop => {ContextBuilder}{methodName}{arguments}; }}");
81+
var contextBuilder = _isStaticMember(methodName) ? "" : ContextBuilder;
8482

85-
var o2 = output.DescendantNodes().OfType<ArrowExpressionClauseSyntax>().First().Expression;
86-
return o2;
83+
return Helpers.ParseMethodBody($"{contextBuilder}{methodName}{arguments}");
8784
}
8885
else
8986
{
@@ -105,31 +102,44 @@ private string ContextBuilder
105102
var isValidParent = !Helpers.IsInsideMethod(node).result && !Helpers.IsInsideMemberAccessExpression(node).result;
106103
var isParentMemberStatic = node.Expression is MemberAccessExpressionSyntax m && _isStaticMember(m.ToFullString());
107104
var isPathLessCast = Helpers.IsPathLessCast(node);
105+
var isAttachedPropertySyntax = Helpers.IsAttachedPropertySyntax(node);
106+
var isInsideAttachedPropertySyntax = Helpers.IsInsideAttachedPropertySyntax(node);
108107

109-
if (e!= null && isValidParent && !_isStaticMember(node.Expression.ToFullString()) && !isParentMemberStatic)
108+
if (e != null && isValidParent && !_isStaticMember(node.Expression.ToFullString()) && !isParentMemberStatic)
110109
{
111110
if (isPathLessCast.result)
112111
{
113-
var expression = e.ToFullString();
114-
var output = ParseCompilationUnit($"class __Temp {{ private Func<object> __prop => ({isPathLessCast.expression?.Expression}){_contextName}; }}");
115-
116-
var newSyntax = output.DescendantNodes().OfType<ArrowExpressionClauseSyntax>().First().Expression;
117-
return newSyntax;
112+
return Helpers.ParseMethodBody($"({isPathLessCast.expression?.Expression}){_contextName}");
113+
}
114+
else if (isInsideAttachedPropertySyntax.result)
115+
{
116+
var contextBuilder = ContextBuilder;
117+
return Helpers.ParseMethodBody($"{contextBuilder}{isInsideAttachedPropertySyntax.expression?.Expression.ToString().TrimEnd('.')}");
118118
}
119119
else
120120
{
121121
var expression = e.ToFullString();
122122
var contextBuilder = _isStaticMember(expression) ? "" : ContextBuilder;
123-
var output = ParseCompilationUnit($"class __Temp {{ private Func<object> __prop => {contextBuilder}{expression}; }}");
124123

125-
var newSyntax = output.DescendantNodes().OfType<ArrowExpressionClauseSyntax>().First().Expression;
126-
return newSyntax;
124+
return Helpers.ParseMethodBody($"{contextBuilder}{expression}");
127125
}
128126
}
129-
else
127+
else if (e != null && isAttachedPropertySyntax.result)
130128
{
131-
return e;
129+
if(e is MemberAccessExpressionSyntax memberAccess
130+
&& memberAccess.Expression is IdentifierNameSyntax identifierSyntax)
131+
{
132+
if(
133+
isAttachedPropertySyntax.expression?.ArgumentList.Arguments.FirstOrDefault() is { } property
134+
&& property.Expression is MemberAccessExpressionSyntax memberAccessExpression
135+
)
136+
{
137+
return Helpers.ParseMethodBody($"{memberAccessExpression.Expression}.Get{memberAccessExpression.Name}");
138+
}
139+
}
132140
}
141+
142+
return e;
133143
}
134144

135145
public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
@@ -144,13 +154,11 @@ private string ContextBuilder
144154
var newIdentifier = node.ToFullString();
145155

146156
var rawFunction = string.IsNullOrWhiteSpace(newIdentifier)
147-
? $"class __Temp {{ private Func<object> __prop => {_contextName}; }}"
148-
: $"class __Temp {{ private Func<object> __prop => {ContextBuilder}{newIdentifier}; }}";
157+
? _contextName
158+
: $"{ContextBuilder}{newIdentifier}";
149159

150-
var output = ParseCompilationUnit(rawFunction);
160+
return Helpers.ParseMethodBody(rawFunction);
151161

152-
var o2 = output.DescendantNodes().OfType<ArrowExpressionClauseSyntax>().First().Expression;
153-
return o2;
154162
}
155163
else
156164
{
@@ -215,6 +223,13 @@ public override void VisitIdentifierName(IdentifierNameSyntax node)
215223

216224
private static class Helpers
217225
{
226+
internal static ExpressionSyntax ParseMethodBody(string body)
227+
=> ParseCompilationUnit($"class __Temp {{ private Func<object> __prop => {body}; }}")
228+
.DescendantNodes()
229+
.OfType<ArrowExpressionClauseSyntax>()
230+
.First()
231+
.Expression;
232+
218233
internal static (bool result, MemberAccessExpressionSyntax? memberAccess) IsInsideMemberAccessExpression(SyntaxNode node)
219234
{
220235
var currentNode = node.Parent;
@@ -296,6 +311,47 @@ internal static (bool result, ParenthesizedExpressionSyntax? expression) IsPathL
296311

297312
return (false, null);
298313
}
314+
315+
internal static (bool result, InvocationExpressionSyntax? expression) IsAttachedPropertySyntax(SyntaxNode node)
316+
{
317+
var currentNode = node.Parent;
318+
319+
if (node.GetText().ToString().EndsWith("."))
320+
{
321+
do
322+
{
323+
if (currentNode is InvocationExpressionSyntax arg)
324+
{
325+
return (true, arg);
326+
}
327+
328+
currentNode = currentNode?.Parent;
329+
}
330+
while (currentNode != null);
331+
}
332+
333+
return (false, null);
334+
}
335+
336+
internal static (bool result, InvocationExpressionSyntax? expression) IsInsideAttachedPropertySyntax(SyntaxNode node)
337+
{
338+
var currentNode = node.Parent;
339+
340+
do
341+
{
342+
if (currentNode is InvocationExpressionSyntax arg
343+
&& arg.Expression is MemberAccessExpressionSyntax memberAccess
344+
&& memberAccess.ToString().EndsWith("."))
345+
{
346+
return (true, arg);
347+
}
348+
349+
currentNode = currentNode?.Parent;
350+
}
351+
while (currentNode != null);
352+
353+
return (false, null);
354+
}
299355
}
300356
}
301357
}

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.Reflection.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Uno.UI.SourceGenerators.XamlGenerator
1313
internal partial class XamlFileGenerator
1414
{
1515
private Func<string, INamedTypeSymbol?>? _findType;
16-
private Func<XamlType, INamedTypeSymbol?>? _findTypeByXamlType;
16+
private Func<XamlType, bool, INamedTypeSymbol?>? _findTypeByXamlType;
1717
private Func<string, string, INamedTypeSymbol?>? _findPropertyTypeByFullName;
1818
private Func<INamedTypeSymbol?, string, INamedTypeSymbol?>? _findPropertyTypeByOwnerSymbol;
1919
private Func<XamlMember, INamedTypeSymbol?>? _findPropertyTypeByXamlMember;
@@ -41,7 +41,7 @@ private void InitCaches()
4141
_findEventType = Funcs.Create<XamlMember, IEventSymbol?>(SourceFindEventType).AsLockedMemoized();
4242
_findPropertyTypeByFullName = Funcs.Create<string, string, INamedTypeSymbol?>(SourceFindPropertyTypeByFullName).AsLockedMemoized();
4343
_findPropertyTypeByOwnerSymbol = Funcs.Create<INamedTypeSymbol?, string, INamedTypeSymbol?>(SourceFindPropertyTypeByOwnerSymbol).AsLockedMemoized();
44-
_findTypeByXamlType = Funcs.Create<XamlType, INamedTypeSymbol?>(SourceFindTypeByXamlType).AsLockedMemoized();
44+
_findTypeByXamlType = Funcs.Create<XamlType, bool, INamedTypeSymbol?>(SourceFindTypeByXamlType).AsLockedMemoized();
4545
_getEventsForType = Funcs.Create<INamedTypeSymbol, Dictionary<string, IEventSymbol>>(SourceGetEventsForType).AsLockedMemoized();
4646
_findLocalizableDeclaredProperties = Funcs.Create<INamedTypeSymbol, string[]>(SourceFindLocalizableDeclaredProperties).AsLockedMemoized();
4747

@@ -742,10 +742,10 @@ private static void ThrowOnErrorSymbol(ISymbol symbol)
742742
private INamedTypeSymbol? FindType(string name)
743743
=> _findType!(name);
744744

745-
private INamedTypeSymbol? FindType(XamlType? type)
746-
=> type != null ? _findTypeByXamlType!(type) : null;
745+
private INamedTypeSymbol? FindType(XamlType? type, bool strictSearch = false)
746+
=> type != null ? _findTypeByXamlType!(type, strictSearch) : null;
747747

748-
private INamedTypeSymbol? SourceFindTypeByXamlType(XamlType type)
748+
private INamedTypeSymbol? SourceFindTypeByXamlType(XamlType type, bool strictSearch)
749749
{
750750
if (type != null)
751751
{
@@ -779,14 +779,24 @@ private static void ThrowOnErrorSymbol(ISymbol symbol)
779779
}
780780

781781
var isKnownNamespace = ns?.Prefix?.HasValue() ?? false;
782-
var fullName = isKnownNamespace && ns != null ? ns.Prefix + ":" + type.Name : type.Name;
783782

784-
return _findType!(fullName);
785-
}
786-
else
787-
{
788-
return null;
783+
if (strictSearch)
784+
{
785+
if(isKnownNamespace && ns != null)
786+
{
787+
var nsName = GetTrimmedNamespace(ns.Namespace);
788+
return _metadataHelper.FindTypeByFullName(nsName + "." + type.Name) as INamedTypeSymbol;
789+
}
790+
}
791+
else
792+
{
793+
var fullName = isKnownNamespace && ns != null ? ns.Prefix + ":" + type.Name : type.Name;
794+
795+
return _findType!(fullName);
796+
}
789797
}
798+
799+
return null;
790800
}
791801

792802
private INamedTypeSymbol GetType(string name)

src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ internal partial class XamlFileGenerator
6060
private readonly Stack<XLoadScope> _xLoadScopeStack = new Stack<XLoadScope>();
6161
private readonly Stack<ResourceOwner> _resourceOwnerStack = new Stack<ResourceOwner>();
6262
private readonly XamlFileDefinition _fileDefinition;
63+
private readonly NamespaceDeclaration _defaultXmlNamespace;
6364
private readonly string _targetPath;
6465
private readonly string _defaultNamespace;
6566
private readonly RoslynMetadataHelper _metadataHelper;
@@ -296,6 +297,8 @@ public XamlFileGenerator(
296297

297298
_isUnoAssembly = isUnoAssembly;
298299
_isUnoFluentAssembly = isUnoFluentAssembly;
300+
301+
_defaultXmlNamespace = _fileDefinition.Namespaces.First(n => n.Prefix == "");
299302
}
300303

301304
/// <summary>
@@ -4337,9 +4340,21 @@ private bool IsStaticMember(string fullMemberName)
43374340

43384341
var isTopLevelMember = lastDotIndex == -1;
43394342

4340-
var typeSymbol = isTopLevelMember
4341-
? _xClassName?.Symbol
4342-
: _metadataHelper.FindTypeByFullName(fullMemberName.Substring(0, lastDotIndex)) as INamedTypeSymbol;
4343+
INamedTypeSymbol? GetTypeSymbol()
4344+
{
4345+
if (isTopLevelMember)
4346+
{
4347+
return _xClassName?.Symbol;
4348+
}
4349+
else
4350+
{
4351+
var typeName = fullMemberName.Substring(0, lastDotIndex);
4352+
return _metadataHelper.FindTypeByFullName(fullMemberName.Substring(0, lastDotIndex)) as INamedTypeSymbol
4353+
?? FindType(new XamlType(_defaultXmlNamespace.Namespace, typeName, new List<XamlType>(), new XamlSchemaContext()), true);
4354+
}
4355+
}
4356+
4357+
var typeSymbol = GetTypeSymbol();
43434358

43444359
var memberName = isTopLevelMember
43454360
? fullMemberName
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<UserControl x:Class="Uno.UI.Tests.Windows_UI_Xaml_Data.xBindTests.Controls.xBind_AttachedProperty"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:local="using:Uno.UI.Tests.Windows_UI_Xaml_Data.xBindTests.Controls"
5+
xmlns:xc="using:Windows.UI.Xaml.Controls"
6+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
mc:Ignorable="d"
9+
d:DesignHeight="300"
10+
d:DesignWidth="400">
11+
12+
<Grid>
13+
<TextBlock x:Name="tb2"
14+
Grid.Row="42" />
15+
16+
<TextBlock x:Name="tb1"
17+
x:FieldModifier="public"
18+
Tag="{x:Bind tb2.(Grid.Row)}" />
19+
</Grid>
20+
</UserControl>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Runtime.InteropServices.WindowsRuntime;
7+
using Windows.Foundation;
8+
using Windows.Foundation.Collections;
9+
using Windows.UI.Xaml;
10+
using Windows.UI.Xaml.Controls;
11+
using Windows.UI.Xaml.Controls.Primitives;
12+
using Windows.UI.Xaml.Data;
13+
using Windows.UI.Xaml.Input;
14+
using Windows.UI.Xaml.Media;
15+
using Windows.UI.Xaml.Navigation;
16+
17+
// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
18+
19+
namespace Uno.UI.Tests.Windows_UI_Xaml_Data.xBindTests.Controls
20+
{
21+
public sealed partial class xBind_AttachedProperty : UserControl
22+
{
23+
public xBind_AttachedProperty()
24+
{
25+
this.InitializeComponent();
26+
}
27+
}
28+
}

src/Uno.UI.Tests/Windows_UI_Xaml_Data/xBindTests/Given_xBind_Binding.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,16 @@ public async Task When_PathLessCasting_Template()
12891289
Assert.AreEqual(rootData, myObject.Tag);
12901290
}
12911291

1292+
[TestMethod]
1293+
public async Task When_AttachedProperty()
1294+
{
1295+
var SUT = new xBind_AttachedProperty();
1296+
1297+
SUT.ForceLoaded();
1298+
1299+
Assert.AreEqual(42, SUT.tb1.Tag);
1300+
}
1301+
12921302
private async Task AssertIsNullAsync<T>(Func<T> getter, TimeSpan? timeout = null) where T:class
12931303
{
12941304
timeout ??= TimeSpan.FromSeconds(1);

0 commit comments

Comments
 (0)