From 1044154279ae2f5f9c3a6e3c4475c9b7f64c47bb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 7 Nov 2015 18:20:56 +0100 Subject: [PATCH] Bind to other controls with #control syntax. Rather than specifying ElementName separately, allow binding using a "#control.Property" property path. --- .../Perspex.Markup.Xaml/Data/Binding.cs | 92 ++++++++++++++++--- .../MarkupExtensions/BindingExtension.cs | 2 +- .../TemplateBindingExtension.cs | 2 +- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- .../Perspex.Markup.Xaml.csproj | 1 - .../Templates/TreeDataTemplate.cs | 2 +- src/Perspex.Themes.Default/TextBox.paml | 4 +- .../SelectingItemsControlTests_Multiple.cs | 4 +- .../Data/BindingTests.cs | 20 ++-- .../Data/BindingTests_ElementName.cs | 4 +- .../Data/BindingTests_TemplatedParent.cs | 6 +- .../Data/MultiBindingTests.cs | 6 +- 12 files changed, 104 insertions(+), 41 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 596ad401df..56bfe07b3c 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -42,7 +42,7 @@ namespace Perspex.Markup.Xaml.Data /// /// Gets or sets the binding path. /// - public string SourcePropertyPath { get; set; } + public string Path { get; set; } /// /// Applies the binding to a property on an instance. @@ -82,19 +82,30 @@ namespace Perspex.Markup.Xaml.Data Contract.Requires(target != null); Contract.Requires(targetType != null); + var pathInfo = ParsePath(Path); + ValidateState(pathInfo); + ExpressionObserver observer; - if (ElementName != null) + if (pathInfo.ElementName != null || ElementName != null) { - observer = CreateElementSubject((IControl)target); + observer = CreateElementSubject( + (IControl)target, + pathInfo.ElementName ?? ElementName, + pathInfo.Path); } else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextSubject(target, targetIsDataContext); + observer = CreateDataContextSubject( + target, + pathInfo.Path, + targetIsDataContext); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentSubject(target); + observer = CreateTemplatedParentSubject( + target, + pathInfo.Path); } else { @@ -143,8 +154,56 @@ namespace Perspex.Markup.Xaml.Data } } + private static PathInfo ParsePath(string path) + { + var result = new PathInfo(); + + if (string.IsNullOrWhiteSpace(path) || path == ".") + { + result.Path = path; + } + 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 CreateDataContextSubject( IObservablePropertyBag target, + string path, bool targetIsDataContext) { Contract.Requires(target != null); @@ -156,7 +215,7 @@ namespace Perspex.Markup.Xaml.Data { var result = new ExpressionObserver( () => dataContextHost.GetValue(Control.DataContextProperty), - GetExpression()); + path); dataContextHost.GetObservable(Control.DataContextProperty).Subscribe(x => result.UpdateRoot()); return result; @@ -168,13 +227,15 @@ namespace Perspex.Markup.Xaml.Data } } - private ExpressionObserver CreateTemplatedParentSubject(IObservablePropertyBag target) + private ExpressionObserver CreateTemplatedParentSubject( + IObservablePropertyBag target, + string path) { Contract.Requires(target != null); var result = new ExpressionObserver( () => target.GetValue(Control.TemplatedParentProperty), - GetExpression()); + path); if (target.GetValue(Control.TemplatedParentProperty) == null) { @@ -189,13 +250,16 @@ namespace Perspex.Markup.Xaml.Data return result; } - private ExpressionObserver CreateElementSubject(IControl target) + private ExpressionObserver CreateElementSubject( + IControl target, + string elementName, + string path) { Contract.Requires(target != null); var result = new ExpressionObserver( - ControlLocator.Track(target, ElementName), - GetExpression()); + ControlLocator.Track(target, elementName), + path); return result; } @@ -214,10 +278,10 @@ namespace Perspex.Markup.Xaml.Data return nameScope.Find(ElementName); } - private string GetExpression() + private class PathInfo { - return SourcePropertyPath == null || SourcePropertyPath == "." ? - string.Empty : SourcePropertyPath; + public string Path { get; set; } + public string ElementName { get; set; } } } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 143a9f1f25..c2cdc5efd3 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -24,7 +24,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions Converter = Converter, ElementName = ElementName, Mode = Mode, - SourcePropertyPath = Path, + Path = Path, }; } diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs index da20631177..b3bd1c7c75 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs @@ -26,7 +26,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions Mode = Mode, Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), - SourcePropertyPath = Path, + Path = Path, }; } diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 55fc2abc71..85f2814e7a 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 55fc2abc715885d3686f8c8605f34714445cd72f +Subproject commit 85f2814e7a0daa76d9b1e63b1a329cb291be30a9 diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index cebb76826f..de0e4f9c82 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -185,7 +185,6 @@ - diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index 6672727014..dc9143be1b 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -35,7 +35,7 @@ namespace Perspex.Markup.Xaml.Templates { if (ItemsSource != null) { - var obs = new ExpressionObserver(item, ItemsSource.SourcePropertyPath); + var obs = new ExpressionObserver(item, ItemsSource.Path); return obs.Take(1).Wait() as IEnumerable; } diff --git a/src/Perspex.Themes.Default/TextBox.paml b/src/Perspex.Themes.Default/TextBox.paml index 8ed353c1e5..db477a6313 100644 --- a/src/Perspex.Themes.Default/TextBox.paml +++ b/src/Perspex.Themes.Default/TextBox.paml @@ -22,9 +22,9 @@ + Path="UseFloatingWatermark"/> diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index d348ab126c..d651216ca3 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -403,13 +403,13 @@ namespace Perspex.Controls.UnitTests.Primitives var itemsBinding = new Binding { - SourcePropertyPath = "Items", + Path = "Items", Mode = BindingMode.OneWay, }; var selectedItemsBinding = new Binding { - SourcePropertyPath = "SelectedItems", + Path = "SelectedItems", Mode = BindingMode.OneWay, }; diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs index 2c4441d3a3..9184b485b7 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -20,7 +20,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var target = CreateTarget(); var binding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", Mode = BindingMode.OneWay, }; @@ -38,7 +38,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var target = CreateTarget(); var binding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", Mode = BindingMode.TwoWay, }; @@ -58,7 +58,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var target = CreateTarget(dataContext: dataContext); var binding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", Mode = BindingMode.OneTime, }; @@ -87,7 +87,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var target = CreateTarget(text: textObservable.Object); var binding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", Mode = BindingMode.OneWayToSource, }; @@ -102,7 +102,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var target = CreateTarget(null); var binding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", }; binding.Bind(target.Object, TextBox.TextProperty); @@ -127,7 +127,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var binding = new Binding { - SourcePropertyPath = "Header", + Path = "Header", }; binding.Bind(parent.Child, Control.DataContextProperty); @@ -145,7 +145,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var target = CreateTarget(null); var binding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", }; var result = binding.CreateSubject(target.Object, TextBox.TextProperty.PropertyType); @@ -161,7 +161,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var binding = new Binding { Converter = converter.Object, - SourcePropertyPath = "Foo", + Path = "Foo", }; var result = binding.CreateSubject(target.Object, TextBox.TextProperty.PropertyType); @@ -189,13 +189,13 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var fooBinding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", Mode = BindingMode.TwoWay, }; var barBinding = new Binding { - SourcePropertyPath = "Bar", + Path = "Bar", Mode = BindingMode.TwoWay, }; diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs index 3efd910c74..bde7297d47 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs @@ -35,7 +35,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var binding = new Binding { ElementName = "source", - SourcePropertyPath = "Text", + Path = "Text", }; binding.Bind(target, TextBlock.TextProperty); @@ -66,7 +66,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var binding = new Binding { ElementName = "source", - SourcePropertyPath = "Text", + Path = "Text", }; binding.Bind(target, TextBlock.TextProperty); diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs index 09d3aa461f..af0b492e49 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs @@ -23,7 +23,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data Mode = BindingMode.OneWay, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), Priority = BindingPriority.TemplatedParent, - SourcePropertyPath = "Foo", + Path = "Foo", }; binding.Bind(target.Object, TextBox.TextProperty); @@ -43,7 +43,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data Mode = BindingMode.TwoWay, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), Priority = BindingPriority.TemplatedParent, - SourcePropertyPath = "Foo", + Path = "Foo", }; binding.Bind(target.Object, TextBox.TextProperty); @@ -62,7 +62,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data var target = CreateTarget(text: textObservable.Object); var binding = new Binding { - SourcePropertyPath = "Foo", + Path = "Foo", Mode = BindingMode.OneWayToSource, }; diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs index 4a86be98f7..23db6d4c39 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs @@ -25,9 +25,9 @@ namespace Perspex.Markup.Xaml.UnitTests.Data Converter = new ConcatConverter(), Bindings = new[] { - new Binding { SourcePropertyPath = "A" }, - new Binding { SourcePropertyPath = "B" }, - new Binding { SourcePropertyPath = "C" }, + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "C" }, } };