diff --git a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml index e2ee4dd67f7..f267182f6f1 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml +++ b/src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml @@ -177,7 +177,8 @@ - + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 6e7a0473784..1765a848a6a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -197,8 +197,13 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte if (avaloniaPropertyFieldMaybe != null) { - nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, - XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); + var isDataContextProperty = avaloniaPropertyFieldMaybe.Name == "DataContextProperty" && Equals(avaloniaPropertyFieldMaybe.DeclaringType, context.GetAvaloniaTypes().StyledElement); + var propertyType = isDataContextProperty + ? (nodes.LastOrDefault() as IXamlIlBindingPathNodeWithDataContextType)?.DataContextType + : null; + propertyType ??= XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo); + + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, propertyType)); } else if (GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName) is IXamlProperty clrProperty) { @@ -297,34 +302,55 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte nodes.Add(new TemplatedParentPathElementNode(templatedParentType.GetClrType())); break; case BindingExpressionGrammar.AncestorNode ancestor: - if (ancestor.Namespace is null && ancestor.TypeName is null) + var styledElement = context.GetAvaloniaTypes().StyledElement; + var ancestorTypeFilter = !(ancestor.Namespace is null && ancestor.TypeName is null) ? GetType(ancestor.Namespace, ancestor.TypeName) : null; + + var ancestorNode = context + .ParentNodes() + .OfType() + .Where(x => styledElement.IsAssignableFrom(x.Type.GetClrType())) + .Skip(1) + .Where(x => ancestorTypeFilter is not null + ? ancestorTypeFilter.IsAssignableFrom(x.Type.GetClrType()) : true) + .ElementAtOrDefault(ancestor.Level); + + IXamlType? dataContextType = null; + if (ancestorNode is not null) { - var styledElementType = context.GetAvaloniaTypes().StyledElement; - var ancestorType = context - .ParentNodes() - .OfType() - .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) - .Skip(1) - .ElementAtOrDefault(ancestor.Level) - ?.Type.GetClrType(); - - if (ancestorType is null) + var isSkipping = true; + foreach (var node in context.ParentNodes()) { - throw new XamlX.XamlTransformException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); + if (node == ancestorNode) + isSkipping = false; + if (node is AvaloniaNameScopeRegistrationXamlIlNode) + break; + if (!isSkipping && node is AvaloniaXamlIlDataContextTypeMetadataNode metadataNode) + { + dataContextType = metadataNode.DataContextType; + break; + } } - - nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level)); } - else + + // We need actual ancestor for a correct DataContextType, + // but since in current design bindings do a double-work by enumerating the tree, + // we want to keep original ancestor type filter, if it was present. + var bindingAncestorType = ancestorTypeFilter is not null + ? ancestorTypeFilter + : ancestorNode?.Type.GetClrType(); + + if (bindingAncestorType is null) { - nodes.Add(new FindAncestorPathElementNode(GetType(ancestor.Namespace, ancestor.TypeName), ancestor.Level)); + throw new XamlX.XamlTransformException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); } + + nodes.Add(new FindAncestorPathElementNode(bindingAncestorType, ancestor.Level, dataContextType)); break; case BindingExpressionGrammar.NameNode elementName: - IXamlType? elementType = null; + IXamlType? elementType = null, dataType = null; foreach (var deferredContent in context.ParentNodes().OfType()) { - elementType = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name); + (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name) ?? default; if (!(elementType is null)) { break; @@ -332,14 +358,14 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte } if (elementType is null) { - elementType = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name); + (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name) ?? default; } if (elementType is null) { 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); } - nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); + nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType, dataType)); break; case BindingExpressionGrammar.TypeCastNode typeCastNode: var castType = GetType(typeCastNode.Namespace, typeCastNode.TypeName); @@ -420,8 +446,9 @@ private ScopeRegistrationFinder(string name) string Name { get; } IXamlType? TargetType { get; set; } + IXamlType? DataContextType { get; set; } - public static IXamlType? GetTargetType(IXamlAstNode namescopeRoot, string name) + public static (IXamlType Target, IXamlType? DataContextType)? GetTargetType(IXamlAstNode namescopeRoot, string name) { // If we start from the nested scope - skip it. if (namescopeRoot is NestedScopeMetadataNode scope) @@ -431,7 +458,7 @@ private ScopeRegistrationFinder(string name) var finder = new ScopeRegistrationFinder(name); namescopeRoot.Visit(finder); - return finder.TargetType; + return finder.TargetType is not null ? (finder.TargetType, DataType: finder.DataContextType) : null; } void IXamlAstVisitor.Pop() @@ -455,12 +482,20 @@ void IXamlAstVisitor.Push(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) { // Ignore name registrations, if we are inside of the nested namescope. - if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration) + if (_childScopesStack.Count == 0) { - if (registration.Name is XamlAstTextNode text && text.Text == Name) + if (node is AvaloniaNameScopeRegistrationXamlIlNode registration + && registration.Name is XamlAstTextNode text && text.Text == Name) { TargetType = registration.TargetType; } + // We are visiting nodes top to bottom. + // If we have already found target type by its name, + // it means all next nodes will be below, and not applicable for data context inheritance. + else if (TargetType is null && node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextTypeMetadata) + { + DataContextType = dataContextTypeMetadata.DataContextType; + } } return node; } @@ -473,6 +508,11 @@ interface IXamlIlBindingPathElementNode void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen); } + interface IXamlIlBindingPathNodeWithDataContextType + { + IXamlType? DataContextType { get; } + } + class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode { public XamlIlNotPathElementNode(IXamlType boolType) @@ -533,17 +573,19 @@ public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) } } - class FindAncestorPathElementNode : IXamlIlBindingPathElementNode + class FindAncestorPathElementNode : IXamlIlBindingPathElementNode, IXamlIlBindingPathNodeWithDataContextType { private readonly int _level; - public FindAncestorPathElementNode(IXamlType ancestorType, int level) + public FindAncestorPathElementNode(IXamlType ancestorType, int level, IXamlType? dataContextType) { Type = ancestorType; _level = level; + DataContextType = dataContextType; } public IXamlType Type { get; } + public IXamlType? DataContextType { get; } public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { @@ -573,17 +615,19 @@ public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) } } - class ElementNamePathElementNode : IXamlIlBindingPathElementNode + class ElementNamePathElementNode : IXamlIlBindingPathElementNode, IXamlIlBindingPathNodeWithDataContextType { private readonly string _name; - public ElementNamePathElementNode(string name, IXamlType elementType) + public ElementNamePathElementNode(string name, IXamlType elementType, IXamlType? dataType) { _name = name; Type = elementType; + DataContextType = dataType; } public IXamlType Type { get; } + public IXamlType? DataContextType { get; } public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 5e0813c9e6d..5f5c2f9fe52 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1282,6 +1282,28 @@ public void SupportParentInPath() } } + [Fact] + public void SupportsParentInPathWithTypeAndLevelFilter() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + + + +"); + var textBlock = window.GetControl("textBlock"); + + Assert.Equal("p1", textBlock.Text); + } + } + [Fact] public void SupportConverterWithParameter() { @@ -2175,6 +2197,140 @@ public void Can_Bind_Brush_To_Hex_String() } } + [Fact] + public void ResolvesElementNameDataContextTypeBasedOnContext() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + +"); + var textBlock = window.GetControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesElementNameDataContextTypeBasedOnContextShortSyntax() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + +"); + var textBlock = window.GetControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void TypeCastWorksWithElementNameDataContext() + { + // By default, DataContext will infer DataType from the XAML context, which will be local:TestDataContext here. + // But developer should be able to re-define this type via type casing, if they know better. + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + +"); + var textBlock = window.GetControl("textBlock"); + + var panelDataContext = new Button { Tag = "foo" }; + ((Panel)window.Content!).DataContext = panelDataContext; + + Assert.Equal(panelDataContext.Tag, textBlock.Text); + } + } + + [Fact] + public void ResolvesParentDataContextTypeBasedOnContext() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + +"); + var textBlock = window.GetControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesParentDataContextTypeBasedOnContextShortSyntax() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + +"); + var textBlock = window.GetControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + static void Throws(string type, Action cb) { try