diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index eb5f87ea2a..8ba637de50 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -66,7 +66,7 @@ namespace Avalonia.Markup.Xaml.Data /// /// Gets or sets the binding path. /// - public string Path { get; set; } + public string Path { get; set; } = ""; /// /// Gets or sets the binding priority. @@ -93,53 +93,51 @@ namespace Avalonia.Markup.Xaml.Data bool enableDataValidation = false) { Contract.Requires(target != null); - anchor = anchor ?? DefaultAnchor?.Target; - - var pathInfo = ParsePath(Path); - ValidateState(pathInfo); + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - + ExpressionObserver observer; - if (pathInfo.ElementName != null || ElementName != null) + if (ElementName != null) { observer = CreateElementObserver( (target as IControl) ?? (anchor as IControl), - pathInfo.ElementName ?? 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) { observer = CreateDataContexObserver( target, - pathInfo.Path, + Path, targetProperty == Control.DataContextProperty, anchor, enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation); + observer = CreateSourceObserver(target, Path, enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver(target, pathInfo.Path); + observer = CreateTemplatedParentObserver(target, Path); } else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) { - if (RelativeSource.AncestorType == null) + if (RelativeSource.Tree == TreeType.Visual && RelativeSource.AncestorType == null) { - throw new InvalidOperationException("AncestorType must be set for RelativeSourceModel.FindAncestor."); + throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } observer = CreateFindAncestorObserver( (target as IControl) ?? (anchor as IControl), - pathInfo.Path); + RelativeSource, + Path); } else { @@ -168,53 +166,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 - { - result.Path = path; - } - - return result; - } - - 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.ElementName != null || ElementName != null) && - RelativeSource != null) - { - throw new InvalidOperationException( - "ElementName property cannot be set with a RelativeSource."); - } - } - private ExpressionObserver CreateDataContexObserver( IAvaloniaObject target, string path, @@ -271,12 +222,13 @@ namespace Avalonia.Markup.Xaml.Data private ExpressionObserver CreateFindAncestorObserver( IControl target, + RelativeSource relativeSource, string path) { Contract.Requires(target != null); return new ExpressionObserver( - ControlLocator.Track(target, RelativeSource.AncestorType, RelativeSource.AncestorLevel -1), + ControlLocator.Track(target, relativeSource.Tree, relativeSource.AncestorLevel - 1, relativeSource.AncestorType), path); } @@ -328,6 +280,7 @@ namespace Avalonia.Markup.Xaml.Data { public string Path { get; set; } public string ElementName { get; set; } + public RelativeSource RelativeSource { get; set; } } } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs index f77df6853b..825d3b8ba5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs @@ -87,5 +87,7 @@ namespace Avalonia.Markup.Xaml.Data /// Gets or sets a value that describes the type of relative source lookup. /// public RelativeSourceMode Mode { get; set; } + + public TreeType Tree { get; set; } = TreeType.Visual; } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 8984498393..c6705cbb4b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -29,20 +29,167 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public override object ProvideValue(IServiceProvider serviceProvider) { + var descriptorContext = (ITypeDescriptorContext)serviceProvider; + + var pathInfo = ParsePath(Path, descriptorContext); + ValidateState(pathInfo); + return new Binding { Converter = Converter, ConverterParameter = ConverterParameter, - ElementName = ElementName, + ElementName = pathInfo.ElementName ?? ElementName, FallbackValue = FallbackValue, Mode = Mode, - Path = Path, + Path = pathInfo.Path, Priority = Priority, - RelativeSource = RelativeSource, + RelativeSource = pathInfo.RelativeSource ?? RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)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 == ".") + { + 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 + { + Tree = TreeType.Logical + }; + 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.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 = null; + 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) { diff --git a/src/Markup/Avalonia.Markup/ControlLocator.cs b/src/Markup/Avalonia.Markup/ControlLocator.cs index de8415d6db..1a82c0a4fd 100644 --- a/src/Markup/Avalonia.Markup/ControlLocator.cs +++ b/src/Markup/Avalonia.Markup/ControlLocator.cs @@ -11,6 +11,21 @@ using Avalonia.VisualTree; namespace Avalonia.Markup { + /// + /// The type of tree via which to track a control. + /// + public enum TreeType + { + /// + /// The visual tree. + /// + Visual, + /// + /// The logical tree. + /// + Logical, + } + /// /// Locates controls relative to other controls. /// @@ -27,13 +42,13 @@ namespace Avalonia.Markup { var attached = Observable.FromEventPattern( x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree += x) + x => relativeTo.AttachedToLogicalTree -= x) .Select(x => ((IControl)x.Sender).FindNameScope()) .StartWith(relativeTo.FindNameScope()); var detached = Observable.FromEventPattern( x => relativeTo.DetachedFromLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree += x) + x => relativeTo.DetachedFromLogicalTree -= x) .Select(x => (INameScope)null); return attached.Merge(detached).Select(nameScope => @@ -68,37 +83,75 @@ namespace Avalonia.Markup /// /// The control relative from which the other control should be found. /// - /// The type of the ancestor to find. + /// The tree via which to track the control. /// /// The level of ancestor control to look for. Use 0 for the first ancestor of the /// requested type. /// - public static IObservable Track(IControl relativeTo, Type ancestorType, int ancestorLevel) + /// The type of the ancestor to find. + public static IObservable Track(IControl relativeTo, TreeType tree, int ancestorLevel, Type ancestorType = null) + { + return TrackAttachmentToTree(relativeTo, tree).Select(isAttachedToTree => + { + if (isAttachedToTree) + { + if (tree == TreeType.Visual) + { + return relativeTo.GetVisualAncestors() + .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(ancestorLevel) as IControl; + } + else + { + return relativeTo.GetLogicalAncestors() + .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(ancestorLevel) as IControl; + } + } + else + { + return null; + } + }); + } + + private static IObservable TrackAttachmentToTree(IControl relativeTo, TreeType tree) + { + return tree == TreeType.Visual ? TrackAttachmentToVisualTree(relativeTo) : TrackAttachmentToLogicalTree(relativeTo); + } + + private static IObservable TrackAttachmentToVisualTree(IControl relativeTo) { var attached = Observable.FromEventPattern( x => relativeTo.AttachedToVisualTree += x, - x => relativeTo.DetachedFromVisualTree += x) + x => relativeTo.AttachedToVisualTree -= x) .Select(x => true) .StartWith(relativeTo.IsAttachedToVisualTree); var detached = Observable.FromEventPattern( x => relativeTo.DetachedFromVisualTree += x, - x => relativeTo.DetachedFromVisualTree += x) + x => relativeTo.DetachedFromVisualTree -= x) .Select(x => false); - return attached.Merge(detached).Select(isAttachedToVisualTree => - { - if (isAttachedToVisualTree) - { - return relativeTo.GetVisualAncestors() - .Where(x => ancestorType.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo())) - .ElementAtOrDefault(ancestorLevel) as IControl; - } - else - { - return null; - } - }); + var attachmentStatus = attached.Merge(detached); + return attachmentStatus; + } + + private static IObservable TrackAttachmentToLogicalTree(IControl relativeTo) + { + var attached = Observable.FromEventPattern( + x => relativeTo.AttachedToLogicalTree += x, + x => relativeTo.AttachedToLogicalTree -= x) + .Select(x => true) + .StartWith(relativeTo.IsAttachedToLogicalTree); + + var detached = Observable.FromEventPattern( + x => relativeTo.DetachedFromLogicalTree += x, + x => relativeTo.DetachedFromLogicalTree -= x) + .Select(x => false); + + var attachmentStatus = attached.Merge(detached); + return attachmentStatus; } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 9125e1e643..095aae7742 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.UnitTests; +using System; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Xaml @@ -77,6 +78,77 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void Binding_To_First_Ancestor_Without_AncestorType_Throws_Exception() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + +