From 786574381970f8f0aae9ed6bd610817b86ec4869 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 7 Nov 2015 13:45:45 +0100 Subject: [PATCH] Added ElementName bindings. --- samples/BindingTest/MainWindow.paml | 7 +- .../Perspex.Markup.Xaml/Data/Binding.cs | 67 ++++++++++++--- .../MarkupExtensions/BindingExtension.cs | 2 + .../TemplateBindingExtension.cs | 4 +- .../Perspex.Markup/Data/ExpressionObserver.cs | 81 ++++++++++++++---- src/Perspex.SceneGraph/NameScope.cs | 2 +- .../Data/ExpressionObserverTests_Property.cs | 28 ++++++- .../Perspex.Markup.UnitTests/packages.config | 1 + .../Data/BindingTests_ElementName.cs | 83 +++++++++++++++++++ .../Perspex.Markup.Xaml.UnitTests.csproj | 2 + .../SamplePerspexObject.cs | 2 +- .../Perspex.Markup.Xaml.UnitTests/TestRoot.cs | 61 ++++++++++++++ .../TypeProviderMock.cs | 2 +- .../ViewModelMock.cs | 2 +- 14 files changed, 313 insertions(+), 31 deletions(-) create mode 100644 tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs create mode 100644 tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml index 3a378fcd24..57f01c729a 100644 --- a/samples/BindingTest/MainWindow.paml +++ b/samples/BindingTest/MainWindow.paml @@ -6,7 +6,7 @@ - + @@ -30,6 +30,11 @@ + + + + diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 8cc5a981f5..596ad401df 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -4,7 +4,6 @@ using System; using System.Reactive.Linq; using System.Reactive.Subjects; -using OmniXaml.TypeConversion; using Perspex.Controls; using Perspex.Markup.Data; @@ -20,6 +19,11 @@ namespace Perspex.Markup.Xaml.Data /// public IValueConverter Converter { get; set; } + /// + /// Gets or sets the name of the element to use as the binding source. + /// + public string ElementName { get; set; } + /// /// Gets or sets the binding mode. /// @@ -47,8 +51,11 @@ namespace Perspex.Markup.Xaml.Data /// The target property. public void Bind(IObservablePropertyBag instance, PerspexProperty property) { + Contract.Requires(instance != null); + Contract.Requires(property != null); + var subject = CreateSubject( - instance, + instance, property.PropertyType, property == Control.DataContextProperty); @@ -72,15 +79,22 @@ namespace Perspex.Markup.Xaml.Data Type targetType, bool targetIsDataContext = false) { + Contract.Requires(target != null); + Contract.Requires(targetType != null); + ExpressionObserver observer; - if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) + if (ElementName != null) + { + observer = CreateElementSubject((IControl)target); + } + else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextExpressionSubject(target, targetIsDataContext); + observer = CreateDataContextSubject(target, targetIsDataContext); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentExpressionSubject(target); + observer = CreateTemplatedParentSubject(target); } else { @@ -88,8 +102,8 @@ namespace Perspex.Markup.Xaml.Data } return new ExpressionSubject( - observer, - targetType, + observer, + targetType, Converter ?? DefaultValueConverter.Instance); } @@ -101,6 +115,10 @@ namespace Perspex.Markup.Xaml.Data /// The binding subject. internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject subject) { + Contract.Requires(target != null); + Contract.Requires(property != null); + Contract.Requires(subject != null); + var mode = Mode == BindingMode.Default ? property.DefaultBindingMode : Mode; @@ -117,7 +135,7 @@ namespace Perspex.Markup.Xaml.Data target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => { subject.Take(1).Subscribe(x => target.SetValue(property, x, Priority)); - }); + }); break; case BindingMode.OneWayToSource: target.GetObservable(property).Subscribe(subject); @@ -125,10 +143,12 @@ namespace Perspex.Markup.Xaml.Data } } - private ExpressionObserver CreateDataContextExpressionSubject( + private ExpressionObserver CreateDataContextSubject( IObservablePropertyBag target, bool targetIsDataContext) { + Contract.Requires(target != null); + var dataContextHost = targetIsDataContext ? target.InheritanceParent as IObservablePropertyBag : target; @@ -148,8 +168,10 @@ namespace Perspex.Markup.Xaml.Data } } - private ExpressionObserver CreateTemplatedParentExpressionSubject(IObservablePropertyBag target) + private ExpressionObserver CreateTemplatedParentSubject(IObservablePropertyBag target) { + Contract.Requires(target != null); + var result = new ExpressionObserver( () => target.GetValue(Control.TemplatedParentProperty), GetExpression()); @@ -167,6 +189,31 @@ namespace Perspex.Markup.Xaml.Data return result; } + private ExpressionObserver CreateElementSubject(IControl target) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + ControlLocator.Track(target, ElementName), + GetExpression()); + return result; + } + + private IControl LookupNamedControl(IControl target) + { + Contract.Requires(target != null); + + var nameScope = target.FindNameScope(); + + if (nameScope == null) + { + throw new InvalidOperationException( + "Could not find name scope for ElementName binding."); + } + + return nameScope.Find(ElementName); + } + private string GetExpression() { return SourcePropertyPath == null || SourcePropertyPath == "." ? diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 4c5f8bc283..143a9f1f25 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -22,12 +22,14 @@ namespace Perspex.Markup.Xaml.MarkupExtensions return new Binding { Converter = Converter, + ElementName = ElementName, Mode = Mode, SourcePropertyPath = Path, }; } public IValueConverter Converter { get; set; } + public string ElementName { get; set; } public BindingMode Mode { get; set; } public string Path { get; set; } } diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs index 0fdd5339cf..da20631177 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs @@ -19,9 +19,10 @@ namespace Perspex.Markup.Xaml.MarkupExtensions public override object ProvideValue(MarkupExtensionContext extensionContext) { - return new Data.Binding + return new Binding { Converter = Converter, + ElementName = ElementName, Mode = Mode, Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), @@ -30,6 +31,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions } public IValueConverter Converter { get; set; } + public string ElementName { get; set; } public BindingMode Mode { get; set; } public string Path { get; set; } } diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index 8f1d247e55..d669713156 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -26,9 +26,12 @@ namespace Perspex.Markup.Data new InpcPropertyAccessorPlugin(), }; - private Func _root; + private readonly object _root; + private readonly Func _rootGetter; + private readonly IObservable _rootObservable; + private IDisposable _rootObserverSubscription; private int _count; - private ExpressionNode _node; + private readonly ExpressionNode _node; private ISubject _empty; /// @@ -37,21 +40,50 @@ namespace Perspex.Markup.Data /// The root object. /// The expression. public ExpressionObserver(object root, string expression) - : this(() => root, expression) { + Contract.Requires(expression != null); + + _root = root; + + if (!string.IsNullOrWhiteSpace(expression)) + { + _node = ExpressionNodeBuilder.Build(expression); + } + + Expression = expression; } /// /// Initializes a new instance of the class. /// - /// A function which gets the root object. + /// An observable which provides the root object. /// The expression. - public ExpressionObserver(Func root, string expression) + public ExpressionObserver(IObservable rootObservable, string expression) { - Contract.Requires(root != null); + Contract.Requires(rootObservable != null); Contract.Requires(expression != null); - _root = root; + _rootObservable = rootObservable; + + if (!string.IsNullOrWhiteSpace(expression)) + { + _node = ExpressionNodeBuilder.Build(expression); + } + + Expression = expression; + } + + /// + /// Initializes a new instance of the class. + /// + /// A function which gets the root object. + /// The expression. + public ExpressionObserver(Func rootGetter, string expression) + { + Contract.Requires(rootGetter != null); + Contract.Requires(expression != null); + + _rootGetter = rootGetter; if (!string.IsNullOrWhiteSpace(expression)) { @@ -105,9 +137,13 @@ namespace Perspex.Markup.Data { return (Leaf as PropertyAccessorNode)?.PropertyType; } + else if(_rootGetter != null) + { + return _rootGetter()?.GetType(); + } else { - return _root()?.GetType(); + return _root?.GetType(); } } finally @@ -134,19 +170,19 @@ namespace Perspex.Markup.Data } /// - /// Causes the root object to be re-read. + /// Causes the root object to be re-read from the root getter. /// public void UpdateRoot() { - if (_count > 0) + if (_count > 0 && _rootGetter != null) { if (_node != null) { - _node.Target = _root(); + _node.Target = _rootGetter(); } else if (_empty != null) { - _empty.OnNext(_root()); + _empty.OnNext(_rootGetter()); } } } @@ -170,7 +206,7 @@ namespace Perspex.Markup.Data { if (_empty == null) { - _empty = new BehaviorSubject(_root()); + _empty = new BehaviorSubject(_rootGetter()); } return _empty.Subscribe(observer); @@ -181,7 +217,18 @@ namespace Perspex.Markup.Data { if (_count++ == 0 && _node != null) { - _node.Target = _root(); + if (_rootGetter != null) + { + _node.Target = _rootGetter(); + } + else if (_rootObservable != null) + { + _rootObserverSubscription = _rootObservable.Subscribe(x => _node.Target = x); + } + else + { + _node.Target = _root; + } } } @@ -189,6 +236,12 @@ namespace Perspex.Markup.Data { if (--_count == 0 && _node != null) { + if (_rootObserverSubscription != null) + { + _rootObserverSubscription.Dispose(); + _rootObserverSubscription = null; + } + _node.Target = null; } } diff --git a/src/Perspex.SceneGraph/NameScope.cs b/src/Perspex.SceneGraph/NameScope.cs index 7334b9ddb0..f4d0addc82 100644 --- a/src/Perspex.SceneGraph/NameScope.cs +++ b/src/Perspex.SceneGraph/NameScope.cs @@ -17,7 +17,7 @@ namespace Perspex public static readonly PerspexProperty NameScopeProperty = PerspexProperty.RegisterAttached("NameScope"); - private Dictionary _inner = new Dictionary(); + private readonly Dictionary _inner = new Dictionary(); /// /// Raised when an element is registered with the name scope. diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index ed8a1bfb7e..fd1583106c 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -3,8 +3,9 @@ using System; using System.Collections.Generic; +using System.Reactive; using System.Reactive.Linq; -using System.Reactive.Subjects; +using Microsoft.Reactive.Testing; using Perspex.Markup.Data; using Xunit; @@ -203,6 +204,26 @@ namespace Perspex.Markup.UnitTests.Data Assert.Equal(new[] { "foo", "bar" }, result); } + [Fact] + public void Should_Track_Property_Value_From_Observable_Root() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable( + OnNext(1, new Class1 { Foo = "foo" }), + OnNext(2, new Class1 { Foo = "bar" })); + var target = new ExpressionObserver(source, "Foo"); + var result = new List(); + + using (target.Subscribe(x => result.Add(x))) + { + scheduler.Start(); + } + + Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo", "bar" }, result); + Assert.Equal(1, source.Subscriptions.Count); + Assert.NotEqual(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + } + [Fact] public void SetValue_Should_Set_Simple_Property_Value() { @@ -333,5 +354,10 @@ namespace Perspex.Markup.UnitTests.Data private class WithoutBar : NotifyingBase, INext { } + + public Recorded> OnNext(long time, object value) + { + return new Recorded>(time, Notification.CreateOnNext(value)); + } } } diff --git a/tests/Perspex.Markup.UnitTests/packages.config b/tests/Perspex.Markup.UnitTests/packages.config index 0d1a6f02e7..2a659c426f 100644 --- a/tests/Perspex.Markup.UnitTests/packages.config +++ b/tests/Perspex.Markup.UnitTests/packages.config @@ -5,6 +5,7 @@ + diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs new file mode 100644 index 0000000000..3efd910c74 --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs @@ -0,0 +1,83 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; +using Perspex.Markup.Xaml.Data; +using Xunit; + +namespace Perspex.Markup.Xaml.UnitTests.Data +{ + public class BindingTests_ElementName + { + [Fact] + public void Should_Bind_To_Element() + { + TextBlock target; + var root = new TestRoot + { + Child = new StackPanel + { + Children = new Controls.Controls + { + new TextBlock + { + Name = "source", + Text = "foo", + }, + (target = new TextBlock + { + Name = "target", + }) + } + } + }; + + var binding = new Binding + { + ElementName = "source", + SourcePropertyPath = "Text", + }; + + binding.Bind(target, TextBlock.TextProperty); + + Assert.Equal("foo", target.Text); + } + + [Fact] + public void Should_Bind_To_Later_Added_Element() + { + TextBlock target; + StackPanel stackPanel; + + var root = new TestRoot + { + Child = stackPanel = new StackPanel + { + Children = new Controls.Controls + { + (target = new TextBlock + { + Name = "target", + }), + } + } + }; + + var binding = new Binding + { + ElementName = "source", + SourcePropertyPath = "Text", + }; + + binding.Bind(target, TextBlock.TextProperty); + + stackPanel.Children.Add(new TextBlock + { + Name = "source", + Text = "foo", + }); + + Assert.Equal("foo", target.Text); + } + } +} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index 6f075c8f98..3883b7594e 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -88,6 +88,7 @@ + @@ -104,6 +105,7 @@ + diff --git a/tests/Perspex.Markup.Xaml.UnitTests/SamplePerspexObject.cs b/tests/Perspex.Markup.Xaml.UnitTests/SamplePerspexObject.cs index 60aaf4c586..81eab06b7c 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/SamplePerspexObject.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/SamplePerspexObject.cs @@ -3,7 +3,7 @@ using System; -namespace Perspex.Xaml.Base.UnitTest +namespace Perspex.Markup.Xaml.UnitTests { internal class SamplePerspexObject : PerspexObject { diff --git a/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs b/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs new file mode 100644 index 0000000000..b9db948265 --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs @@ -0,0 +1,61 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Perspex.Controls; +using Perspex.Platform; +using Perspex.Rendering; + +namespace Perspex.Markup.Xaml.UnitTests +{ + public class TestRoot : Decorator, IRenderRoot, INameScope + { + private readonly NameScope _nameScope = new NameScope(); + + event EventHandler INameScope.Registered + { + add { _nameScope.Registered += value; ++NameScopeRegisteredSubscribers; } + remove { _nameScope.Registered -= value; --NameScopeRegisteredSubscribers; } + } + + public event EventHandler Unregistered + { + add { _nameScope.Unregistered += value; ++NameScopeUnregisteredSubscribers; } + remove { _nameScope.Unregistered -= value; --NameScopeUnregisteredSubscribers; } + } + + public int NameScopeRegisteredSubscribers { get; private set; } + + public int NameScopeUnregisteredSubscribers { get; private set; } + + public IRenderTarget RenderTarget + { + get { throw new NotImplementedException(); } + } + + public IRenderQueueManager RenderQueueManager + { + get { throw new NotImplementedException(); } + } + + public Point TranslatePointToScreen(Point p) + { + throw new NotImplementedException(); + } + + public void Register(string name, object element) + { + _nameScope.Register(name, element); + } + + public object Find(string name) + { + return _nameScope.Find(name); + } + + public void Unregister(string name) + { + _nameScope.Unregister(name); + } + } +} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/TypeProviderMock.cs b/tests/Perspex.Markup.Xaml.UnitTests/TypeProviderMock.cs index 673369f641..f483bd377a 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/TypeProviderMock.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/TypeProviderMock.cs @@ -4,7 +4,7 @@ using OmniXaml; using System; -namespace Perspex.Xaml.Base.UnitTest +namespace Perspex.Markup.Xaml.UnitTests { internal class TypeProviderMock : ITypeProvider { diff --git a/tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs b/tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs index 4bdfe94e0e..723bf53311 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs @@ -4,7 +4,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; -namespace Perspex.Xaml.Base.UnitTest +namespace Perspex.Markup.Xaml.UnitTests { internal class ViewModelMock : INotifyPropertyChanged {