From f869e3f1781d95ae201854037d31eee950b37452 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 15 Oct 2017 13:17:52 -0500 Subject: [PATCH] Move path parsing into the binding extension. Our path shorthands will only work from XAML now. Now we can look up types. --- .../Avalonia.Markup.Xaml/Data/Binding.cs | 159 ++------------- .../MarkupExtensions/BindingExtension.cs | 189 +++++++++++++++++- .../Xaml/BindingTests_RelativeSource.cs | 50 +++++ 3 files changed, 244 insertions(+), 154 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 052f743a4b..f40e6ad84e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -91,55 +91,50 @@ namespace Avalonia.Markup.Xaml.Data bool enableDataValidation = false) { Contract.Requires(target != null); - - var pathInfo = ParsePath(Path); - ValidateState(pathInfo); + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - - var elementName = ElementName ?? pathInfo.ElementName; - var relativeSource = RelativeSource ?? pathInfo.RelativeSource; - + ExpressionObserver observer; - if (elementName != null) + if (ElementName != null) { observer = CreateElementObserver( (target as IControl) ?? (anchor as IControl), - elementName, - pathInfo.Path); + ElementName, + Path); } else if (Source != null) { - observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation); + observer = CreateSourceObserver(Source, Path, enableDataValidation); } - else if (relativeSource == null || relativeSource.Mode == RelativeSourceMode.DataContext) + else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { observer = CreateDataContexObserver( target, - pathInfo.Path, + Path, targetProperty == Control.DataContextProperty, anchor, enableDataValidation); } - else if (relativeSource.Mode == RelativeSourceMode.Self) + else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation); + observer = CreateSourceObserver(target, Path, enableDataValidation); } - else if (relativeSource.Mode == RelativeSourceMode.TemplatedParent) + else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver(target, pathInfo.Path); + observer = CreateTemplatedParentObserver(target, Path); } - else if (relativeSource.Mode == RelativeSourceMode.FindAncestor) + else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) { - if (relativeSource.AncestorType == null) + if (RelativeSource.AncestorType == null) { throw new InvalidOperationException("AncestorType must be set for RelativeSourceModel.FindAncestor."); } observer = CreateFindAncestorObserver( (target as IControl) ?? (anchor as IControl), - relativeSource, - pathInfo.Path); + RelativeSource, + Path); } else { @@ -168,128 +163,6 @@ namespace Avalonia.Markup.Xaml.Data return new InstancedBinding(subject, Mode, Priority); } - private static PathInfo ParsePath(string path) - { - var result = new PathInfo(); - - if (string.IsNullOrWhiteSpace(path) || path == ".") - { - result.Path = string.Empty; - } - else if (path.StartsWith("#")) - { - var dot = path.IndexOf('.'); - - if (dot != -1) - { - result.Path = path.Substring(dot + 1); - result.ElementName = path.Substring(1, dot - 1); - } - else - { - result.Path = string.Empty; - result.ElementName = path.Substring(1); - } - } - else if (path.StartsWith("$")) - { - var relativeSource = new RelativeSource(); - result.RelativeSource = relativeSource; - var dot = path.IndexOf('.'); - string relativeSourceMode; - if (dot != -1) - { - result.Path = path.Substring(dot + 1); - relativeSourceMode = path.Substring(1, dot - 1); - } - else - { - result.Path = string.Empty; - relativeSourceMode = path.Substring(1); - } - - if (relativeSourceMode == "self") - { - relativeSource.Mode = RelativeSourceMode.Self; - } - else if(relativeSourceMode == "parent") - { - relativeSource.Mode = RelativeSourceMode.FindAncestor; - relativeSource.AncestorType = typeof(IControl); - relativeSource.AncestorLevel = 1; - } - else if(relativeSourceMode.StartsWith("parent[")) - { - relativeSource.Mode = RelativeSourceMode.FindAncestor; - var parentConfigStart = relativeSourceMode.IndexOf('['); - if (!relativeSourceMode.EndsWith("]")) - { - throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['."); - } - var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(','); - if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0) - { - throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax"); - } - else if (parentConfigParams.Length == 1) - { - if (int.TryParse(parentConfigParams[0], out int level)) - { - relativeSource.AncestorType = typeof(IControl); - relativeSource.AncestorLevel = level + 1; - } - else - { - relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0]); - } - } - else - { - relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0]); - relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1; - } - } - else - { - throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}"); - } - } - else - { - result.Path = path; - } - - return result; - } - - private static Type LookupAncestorType(string ancestorTypeName) - { - //TODO: What is our syntax for type lookup here? - throw new NotImplementedException(); - } - - private void ValidateState(PathInfo pathInfo) - { - if (pathInfo.ElementName != null && ElementName != null) - { - throw new InvalidOperationException( - "ElementName property cannot be set when an #elementName path is provided."); - } - - if (pathInfo.RelativeSource != null && RelativeSource != null) - { - throw new InvalidOperationException( - "ElementName property cannot be set when a $self or $parent path is provided."); - } - - if ((pathInfo.ElementName != null || ElementName != null) && - (pathInfo.RelativeSource != null || RelativeSource != null)) - { - throw new InvalidOperationException( - "ElementName property cannot be set with a RelativeSource."); - } - } - private ExpressionObserver CreateDataContexObserver( IAvaloniaObject target, string path, diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 9d1e36ee78..08b81a07b3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -7,8 +7,13 @@ using System; namespace Avalonia.Markup.Xaml.MarkupExtensions { + using Avalonia.Controls; + using Avalonia.Styling; + using Portable.Xaml; + using Portable.Xaml.ComponentModel; using Portable.Xaml.Markup; using PortableXaml; + using System.ComponentModel; [MarkupExtensionReturnType(typeof(IBinding))] public class BindingExtension : MarkupExtension @@ -24,19 +29,181 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public override object ProvideValue(IServiceProvider serviceProvider) { - var b = new Binding + var descriptorContext = (ITypeDescriptorContext)serviceProvider; + + var pathInfo = ParsePath(Path, descriptorContext); + ValidateState(pathInfo); + + return XamlBinding.FromMarkupExtensionContext( + new Binding + { + Converter = Converter, + ConverterParameter = ConverterParameter, + ElementName = pathInfo.ElementName ?? ElementName, + FallbackValue = FallbackValue, + Mode = Mode, + Path = pathInfo.Path, + Priority = Priority, + RelativeSource = pathInfo.RelativeSource ?? RelativeSource, + }, + serviceProvider); + } + + private class PathInfo + { + public string Path { get; set; } + public string ElementName { get; set; } + public RelativeSource RelativeSource { get; set; } + } + + private void ValidateState(PathInfo pathInfo) + { + if (pathInfo.ElementName != null && ElementName != null) + { + throw new InvalidOperationException( + "ElementName property cannot be set when an #elementName path is provided."); + } + + if (pathInfo.RelativeSource != null && RelativeSource != null) + { + throw new InvalidOperationException( + "ElementName property cannot be set when a $self or $parent path is provided."); + } + + if ((pathInfo.ElementName != null || ElementName != null) && + (pathInfo.RelativeSource != null || RelativeSource != null)) + { + throw new InvalidOperationException( + "ElementName property cannot be set with a RelativeSource."); + } + } + + private static PathInfo ParsePath(string path, ITypeDescriptorContext context) + { + var result = new PathInfo(); + + if (string.IsNullOrWhiteSpace(path) || path == ".") { - Converter = Converter, - ConverterParameter = ConverterParameter, - ElementName = ElementName, - FallbackValue = FallbackValue, - Mode = Mode, - Path = Path, - Priority = Priority, - RelativeSource = RelativeSource - }; + result.Path = string.Empty; + } + else if (path.StartsWith("#")) + { + var dot = path.IndexOf('.'); + + if (dot != -1) + { + result.Path = path.Substring(dot + 1); + result.ElementName = path.Substring(1, dot - 1); + } + else + { + result.Path = string.Empty; + result.ElementName = path.Substring(1); + } + } + else if (path.StartsWith("$")) + { + var relativeSource = new RelativeSource(); + result.RelativeSource = relativeSource; + var dot = path.IndexOf('.'); + string relativeSourceMode; + if (dot != -1) + { + result.Path = path.Substring(dot + 1); + relativeSourceMode = path.Substring(1, dot - 1); + } + else + { + result.Path = string.Empty; + relativeSourceMode = path.Substring(1); + } + + if (relativeSourceMode == "self") + { + relativeSource.Mode = RelativeSourceMode.Self; + } + else if (relativeSourceMode == "parent") + { + relativeSource.Mode = RelativeSourceMode.FindAncestor; + relativeSource.AncestorType = typeof(IControl); + relativeSource.AncestorLevel = 1; + } + else if (relativeSourceMode.StartsWith("parent[")) + { + relativeSource.Mode = RelativeSourceMode.FindAncestor; + var parentConfigStart = relativeSourceMode.IndexOf('['); + if (!relativeSourceMode.EndsWith("]")) + { + throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['."); + } + var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';'); + if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0) + { + throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax"); + } + else if (parentConfigParams.Length == 1) + { + if (int.TryParse(parentConfigParams[0], out int level)) + { + relativeSource.AncestorType = typeof(IControl); + relativeSource.AncestorLevel = level + 1; + } + else + { + relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context); + } + } + else + { + relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context); + relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1; + } + } + else + { + throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}"); + } + } + else + { + result.Path = path; + } + + return result; + } + + private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context) + { + var parts = ancestorTypeName.Split(':'); + if (parts.Length == 0 || parts.Length > 2) + { + throw new InvalidOperationException("Invalid type name"); + } + + if (parts.Length == 1) + { + return context.ResolveType(string.Empty, parts[0]); + } + else + { + return context.ResolveType(parts[0], parts[1]); + } + } + + private static object GetDefaultAnchor(ITypeDescriptorContext context) + { + object anchor = null; + + // The target is not a control, so we need to find an anchor that will let us look + // up named controls and style resources. First look for the closest IControl in + // the context. + anchor = context.GetFirstAmbientValue(); - return XamlBinding.FromMarkupExtensionContext(b, serviceProvider); + // If a control was not found, then try to find the highest-level style as the XAML + // file could be a XAML file containing only styles. + return anchor ?? + context.GetService()?.RootObject as IStyle ?? + context.GetLastOrDefaultAmbientValue(); } public IValueConverter Converter { get; set; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index a4e4d4f5c3..e372799c1c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -127,6 +127,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Binding_To_Ancestor_Of_Type_With_Shorthand_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + +