Skip to content

Commit be7bb76

Browse files
authored
Try to infer DataContext type from the $parent and #named compiled binding path parts (#17204)
* Try to infer DataContext type from #named binding nodes * Try to infer DataContext type from $parent binding nodes * Use new syntax in the repo (Rider still marks it as an error) * Add tests ensuring type casing still works * Fix $parent regression * Make new tests StringSyntax compatible
1 parent edc3f0f commit be7bb76

File tree

3 files changed

+232
-31
lines changed

3 files changed

+232
-31
lines changed

src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@
177177
<MultiBinding Converter="{x:Static BoolConverters.And}">
178178
<MultiBinding Converter="{x:Static BoolConverters.Or}" >
179179
<Binding Path="IsActive" />
180-
<Binding Path="#Main.((vm:ControlDetailsViewModel)DataContext).ShowInactiveFrames" />
180+
<!-- Rider marks it as an error, because it doesn't know about new binding rules yet. -->
181+
<Binding Path="#Main.DataContext.ShowInactiveFrames" />
181182
</MultiBinding>
182183
<Binding Path="IsVisible" />
183184
</MultiBinding>

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,13 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte
197197

198198
if (avaloniaPropertyFieldMaybe != null)
199199
{
200-
nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe,
201-
XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo)));
200+
var isDataContextProperty = avaloniaPropertyFieldMaybe.Name == "DataContextProperty" && Equals(avaloniaPropertyFieldMaybe.DeclaringType, context.GetAvaloniaTypes().StyledElement);
201+
var propertyType = isDataContextProperty
202+
? (nodes.LastOrDefault() as IXamlIlBindingPathNodeWithDataContextType)?.DataContextType
203+
: null;
204+
propertyType ??= XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo);
205+
206+
nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, propertyType));
202207
}
203208
else if (GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName) is IXamlProperty clrProperty)
204209
{
@@ -297,49 +302,70 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte
297302
nodes.Add(new TemplatedParentPathElementNode(templatedParentType.GetClrType()));
298303
break;
299304
case BindingExpressionGrammar.AncestorNode ancestor:
300-
if (ancestor.Namespace is null && ancestor.TypeName is null)
305+
var styledElement = context.GetAvaloniaTypes().StyledElement;
306+
var ancestorTypeFilter = !(ancestor.Namespace is null && ancestor.TypeName is null) ? GetType(ancestor.Namespace, ancestor.TypeName) : null;
307+
308+
var ancestorNode = context
309+
.ParentNodes()
310+
.OfType<XamlAstConstructableObjectNode>()
311+
.Where(x => styledElement.IsAssignableFrom(x.Type.GetClrType()))
312+
.Skip(1)
313+
.Where(x => ancestorTypeFilter is not null
314+
? ancestorTypeFilter.IsAssignableFrom(x.Type.GetClrType()) : true)
315+
.ElementAtOrDefault(ancestor.Level);
316+
317+
IXamlType? dataContextType = null;
318+
if (ancestorNode is not null)
301319
{
302-
var styledElementType = context.GetAvaloniaTypes().StyledElement;
303-
var ancestorType = context
304-
.ParentNodes()
305-
.OfType<XamlAstConstructableObjectNode>()
306-
.Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType()))
307-
.Skip(1)
308-
.ElementAtOrDefault(ancestor.Level)
309-
?.Type.GetClrType();
310-
311-
if (ancestorType is null)
320+
var isSkipping = true;
321+
foreach (var node in context.ParentNodes())
312322
{
313-
throw new XamlX.XamlTransformException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo);
323+
if (node == ancestorNode)
324+
isSkipping = false;
325+
if (node is AvaloniaNameScopeRegistrationXamlIlNode)
326+
break;
327+
if (!isSkipping && node is AvaloniaXamlIlDataContextTypeMetadataNode metadataNode)
328+
{
329+
dataContextType = metadataNode.DataContextType;
330+
break;
331+
}
314332
}
315-
316-
nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level));
317333
}
318-
else
334+
335+
// We need actual ancestor for a correct DataContextType,
336+
// but since in current design bindings do a double-work by enumerating the tree,
337+
// we want to keep original ancestor type filter, if it was present.
338+
var bindingAncestorType = ancestorTypeFilter is not null
339+
? ancestorTypeFilter
340+
: ancestorNode?.Type.GetClrType();
341+
342+
if (bindingAncestorType is null)
319343
{
320-
nodes.Add(new FindAncestorPathElementNode(GetType(ancestor.Namespace, ancestor.TypeName), ancestor.Level));
344+
throw new XamlX.XamlTransformException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo);
321345
}
346+
347+
nodes.Add(new FindAncestorPathElementNode(bindingAncestorType, ancestor.Level, dataContextType));
322348
break;
323349
case BindingExpressionGrammar.NameNode elementName:
324-
IXamlType? elementType = null;
350+
IXamlType? elementType = null, dataType = null;
325351
foreach (var deferredContent in context.ParentNodes().OfType<NestedScopeMetadataNode>())
326352
{
327-
elementType = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name);
353+
(elementType, dataType) = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name) ?? default;
328354
if (!(elementType is null))
329355
{
330356
break;
331357
}
332358
}
333359
if (elementType is null)
334360
{
335-
elementType = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name);
361+
(elementType, dataType) = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name) ?? default;
336362
}
337363

338364
if (elementType is null)
339365
{
340366
throw new XamlX.XamlTransformException($"Unable to find element '{elementName.Name}' in the current namescope. Unable to use a compiled binding with a name binding if the name cannot be found at compile time.", lineInfo);
341367
}
342-
nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType));
368+
nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType, dataType));
343369
break;
344370
case BindingExpressionGrammar.TypeCastNode typeCastNode:
345371
var castType = GetType(typeCastNode.Namespace, typeCastNode.TypeName);
@@ -420,8 +446,9 @@ private ScopeRegistrationFinder(string name)
420446
string Name { get; }
421447

422448
IXamlType? TargetType { get; set; }
449+
IXamlType? DataContextType { get; set; }
423450

424-
public static IXamlType? GetTargetType(IXamlAstNode namescopeRoot, string name)
451+
public static (IXamlType Target, IXamlType? DataContextType)? GetTargetType(IXamlAstNode namescopeRoot, string name)
425452
{
426453
// If we start from the nested scope - skip it.
427454
if (namescopeRoot is NestedScopeMetadataNode scope)
@@ -431,7 +458,7 @@ private ScopeRegistrationFinder(string name)
431458

432459
var finder = new ScopeRegistrationFinder(name);
433460
namescopeRoot.Visit(finder);
434-
return finder.TargetType;
461+
return finder.TargetType is not null ? (finder.TargetType, DataType: finder.DataContextType) : null;
435462
}
436463

437464
void IXamlAstVisitor.Pop()
@@ -455,12 +482,20 @@ void IXamlAstVisitor.Push(IXamlAstNode node)
455482
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
456483
{
457484
// Ignore name registrations, if we are inside of the nested namescope.
458-
if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration)
485+
if (_childScopesStack.Count == 0)
459486
{
460-
if (registration.Name is XamlAstTextNode text && text.Text == Name)
487+
if (node is AvaloniaNameScopeRegistrationXamlIlNode registration
488+
&& registration.Name is XamlAstTextNode text && text.Text == Name)
461489
{
462490
TargetType = registration.TargetType;
463491
}
492+
// We are visiting nodes top to bottom.
493+
// If we have already found target type by its name,
494+
// it means all next nodes will be below, and not applicable for data context inheritance.
495+
else if (TargetType is null && node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextTypeMetadata)
496+
{
497+
DataContextType = dataContextTypeMetadata.DataContextType;
498+
}
464499
}
465500
return node;
466501
}
@@ -473,6 +508,11 @@ interface IXamlIlBindingPathElementNode
473508
void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen);
474509
}
475510

511+
interface IXamlIlBindingPathNodeWithDataContextType
512+
{
513+
IXamlType? DataContextType { get; }
514+
}
515+
476516
class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode
477517
{
478518
public XamlIlNotPathElementNode(IXamlType boolType)
@@ -533,17 +573,19 @@ public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
533573
}
534574
}
535575

536-
class FindAncestorPathElementNode : IXamlIlBindingPathElementNode
576+
class FindAncestorPathElementNode : IXamlIlBindingPathElementNode, IXamlIlBindingPathNodeWithDataContextType
537577
{
538578
private readonly int _level;
539579

540-
public FindAncestorPathElementNode(IXamlType ancestorType, int level)
580+
public FindAncestorPathElementNode(IXamlType ancestorType, int level, IXamlType? dataContextType)
541581
{
542582
Type = ancestorType;
543583
_level = level;
584+
DataContextType = dataContextType;
544585
}
545586

546587
public IXamlType Type { get; }
588+
public IXamlType? DataContextType { get; }
547589

548590
public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
549591
{
@@ -573,17 +615,19 @@ public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
573615
}
574616
}
575617

576-
class ElementNamePathElementNode : IXamlIlBindingPathElementNode
618+
class ElementNamePathElementNode : IXamlIlBindingPathElementNode, IXamlIlBindingPathNodeWithDataContextType
577619
{
578620
private readonly string _name;
579621

580-
public ElementNamePathElementNode(string name, IXamlType elementType)
622+
public ElementNamePathElementNode(string name, IXamlType elementType, IXamlType? dataType)
581623
{
582624
_name = name;
583625
Type = elementType;
626+
DataContextType = dataType;
584627
}
585628

586629
public IXamlType Type { get; }
630+
public IXamlType? DataContextType { get; }
587631

588632
public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
589633
{

tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,28 @@ public void SupportParentInPath()
12821282
}
12831283
}
12841284

1285+
[Fact]
1286+
public void SupportsParentInPathWithTypeAndLevelFilter()
1287+
{
1288+
using (UnitTestApplication.Start(TestServices.StyledWindow))
1289+
{
1290+
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
1291+
<Window xmlns='https://github.com/avaloniaui'
1292+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
1293+
<Border x:Name='p2'>
1294+
<Border x:Name='p1'>
1295+
<Button x:Name='p0'>
1296+
<TextBlock x:Name='textBlock' Text='{CompiledBinding $parent[Control;1].Name}' />
1297+
</Button>
1298+
</Border>
1299+
</Border>
1300+
</Window>");
1301+
var textBlock = window.GetControl<TextBlock>("textBlock");
1302+
1303+
Assert.Equal("p1", textBlock.Text);
1304+
}
1305+
}
1306+
12851307
[Fact]
12861308
public void SupportConverterWithParameter()
12871309
{
@@ -2175,6 +2197,140 @@ public void Can_Bind_Brush_To_Hex_String()
21752197
}
21762198
}
21772199

2200+
[Fact]
2201+
public void ResolvesElementNameDataContextTypeBasedOnContext()
2202+
{
2203+
using (UnitTestApplication.Start(TestServices.StyledWindow))
2204+
{
2205+
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
2206+
<Window xmlns='https://github.com/avaloniaui'
2207+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
2208+
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
2209+
x:DataType='local:TestDataContext'
2210+
x:Name='MyWindow'>
2211+
<TextBlock Text='{CompiledBinding ElementName=MyWindow, Path=DataContext.StringProperty}' Name='textBlock' />
2212+
</Window>");
2213+
var textBlock = window.GetControl<TextBlock>("textBlock");
2214+
2215+
var dataContext = new TestDataContext
2216+
{
2217+
StringProperty = "foobar"
2218+
};
2219+
2220+
window.DataContext = dataContext;
2221+
2222+
Assert.Equal(dataContext.StringProperty, textBlock.Text);
2223+
}
2224+
}
2225+
2226+
[Fact]
2227+
public void ResolvesElementNameDataContextTypeBasedOnContextShortSyntax()
2228+
{
2229+
using (UnitTestApplication.Start(TestServices.StyledWindow))
2230+
{
2231+
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
2232+
<Window xmlns='https://github.com/avaloniaui'
2233+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
2234+
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
2235+
x:DataType='local:TestDataContext'
2236+
x:Name='MyWindow'>
2237+
<TextBlock Text='{CompiledBinding #MyWindow.DataContext.StringProperty}' Name='textBlock' />
2238+
</Window>");
2239+
var textBlock = window.GetControl<TextBlock>("textBlock");
2240+
2241+
var dataContext = new TestDataContext
2242+
{
2243+
StringProperty = "foobar"
2244+
};
2245+
2246+
window.DataContext = dataContext;
2247+
2248+
Assert.Equal(dataContext.StringProperty, textBlock.Text);
2249+
}
2250+
}
2251+
2252+
[Fact]
2253+
public void TypeCastWorksWithElementNameDataContext()
2254+
{
2255+
// By default, DataContext will infer DataType from the XAML context, which will be local:TestDataContext here.
2256+
// But developer should be able to re-define this type via type casing, if they know better.
2257+
using (UnitTestApplication.Start(TestServices.StyledWindow))
2258+
{
2259+
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
2260+
<Window xmlns='https://github.com/avaloniaui'
2261+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
2262+
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
2263+
x:DataType='local:TestDataContext'
2264+
x:Name='MyWindow'>
2265+
<Panel>
2266+
<TextBlock Text='{CompiledBinding $parent.((Button)DataContext).Tag}' Name='textBlock' />
2267+
</Panel>
2268+
</Window>");
2269+
var textBlock = window.GetControl<TextBlock>("textBlock");
2270+
2271+
var panelDataContext = new Button { Tag = "foo" };
2272+
((Panel)window.Content!).DataContext = panelDataContext;
2273+
2274+
Assert.Equal(panelDataContext.Tag, textBlock.Text);
2275+
}
2276+
}
2277+
2278+
[Fact]
2279+
public void ResolvesParentDataContextTypeBasedOnContext()
2280+
{
2281+
using (UnitTestApplication.Start(TestServices.StyledWindow))
2282+
{
2283+
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
2284+
<Window xmlns='https://github.com/avaloniaui'
2285+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
2286+
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
2287+
x:DataType='local:TestDataContext'
2288+
x:Name='MyWindow'>
2289+
<Panel>
2290+
<TextBlock Text='{CompiledBinding $parent[Panel].DataContext.StringProperty}' Name='textBlock' />
2291+
</Panel>
2292+
</Window>");
2293+
var textBlock = window.GetControl<TextBlock>("textBlock");
2294+
2295+
var dataContext = new TestDataContext
2296+
{
2297+
StringProperty = "foobar"
2298+
};
2299+
2300+
window.DataContext = dataContext;
2301+
2302+
Assert.Equal(dataContext.StringProperty, textBlock.Text);
2303+
}
2304+
}
2305+
2306+
[Fact]
2307+
public void ResolvesParentDataContextTypeBasedOnContextShortSyntax()
2308+
{
2309+
using (UnitTestApplication.Start(TestServices.StyledWindow))
2310+
{
2311+
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
2312+
<Window xmlns='https://github.com/avaloniaui'
2313+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
2314+
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
2315+
x:DataType='local:TestDataContext'
2316+
x:Name='MyWindow'>
2317+
<Panel>
2318+
<TextBlock Text='{CompiledBinding $parent.DataContext.StringProperty}' Name='textBlock' />
2319+
</Panel>
2320+
</Window>");
2321+
var textBlock = window.GetControl<TextBlock>("textBlock");
2322+
2323+
var dataContext = new TestDataContext
2324+
{
2325+
StringProperty = "foobar"
2326+
};
2327+
2328+
window.DataContext = dataContext;
2329+
2330+
Assert.Equal(dataContext.StringProperty, textBlock.Text);
2331+
}
2332+
}
2333+
21782334
static void Throws(string type, Action cb)
21792335
{
21802336
try

0 commit comments

Comments
 (0)