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 = @"
+
+
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ Assert.Throws( () => loader.Load(xaml));
+ }
+ }
+
+ [Fact]
+ public void Binding_To_First_Ancestor_With_Shorthand_Works()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var button = window.FindControl