Browse Source

Added ElementName bindings.

Steven Kirk 10 years ago
parent
commit
7865743819

+ 6 - 1
samples/BindingTest/MainWindow.paml

@@ -6,7 +6,7 @@
         <StackPanel Orientation="Horizontal">
           <StackPanel Margin="18" Gap="4" Width="200">
             <TextBlock FontSize="16" Text="Simple Bindings"/>
-            <TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}"/>
+            <TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
             <TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
             <TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
             <TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWayToSource}"/>
@@ -30,6 +30,11 @@
             <TextBlock Text="{Binding Path=DoubleValue}"/>
             <ProgressBar Maximum="10" Value="{Binding DoubleValue}"/>
           </StackPanel>
+          <StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
+            <TextBlock FontSize="16" Text="Binding Sources"/>
+            <TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True" 
+                     Text="{Binding Path=Text, ElementName=first, Mode=TwoWay}"/>
+          </StackPanel>
         </StackPanel>
       </StackPanel>
     </TabItem>

+ 57 - 10
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
         /// </summary>
         public IValueConverter Converter { get; set; }
 
+        /// <summary>
+        /// Gets or sets the name of the element to use as the binding source.
+        /// </summary>
+        public string ElementName { get; set; }
+
         /// <summary>
         /// Gets or sets the binding mode.
         /// </summary>
@@ -47,8 +51,11 @@ namespace Perspex.Markup.Xaml.Data
         /// <param name="property">The target property.</param>
         public void Bind(IObservablePropertyBag instance, PerspexProperty property)
         {
+            Contract.Requires<ArgumentNullException>(instance != null);
+            Contract.Requires<ArgumentNullException>(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<ArgumentNullException>(target != null);
+            Contract.Requires<ArgumentNullException>(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
         /// <param name="subject">The binding subject.</param>
         internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject<object> subject)
         {
+            Contract.Requires<ArgumentNullException>(target != null);
+            Contract.Requires<ArgumentNullException>(property != null);
+            Contract.Requires<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(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<ArgumentNullException>(target != null);
+
+            var result = new ExpressionObserver(
+                ControlLocator.Track(target, ElementName), 
+                GetExpression());
+            return result;
+        }
+
+        private IControl LookupNamedControl(IControl target)
+        {
+            Contract.Requires<ArgumentNullException>(target != null);
+
+            var nameScope = target.FindNameScope();
+
+            if (nameScope == null)
+            {
+                throw new InvalidOperationException(
+                    "Could not find name scope for ElementName binding.");
+            }
+
+            return nameScope.Find<IControl>(ElementName);
+        }
+
         private string GetExpression()
         {
             return SourcePropertyPath == null || SourcePropertyPath == "." ?

+ 2 - 0
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; }
     }

+ 3 - 1
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; }
     }

+ 67 - 14
src/Markup/Perspex.Markup/Data/ExpressionObserver.cs

@@ -26,9 +26,12 @@ namespace Perspex.Markup.Data
                 new InpcPropertyAccessorPlugin(),
             };
 
-        private Func<object> _root;
+        private readonly object _root;
+        private readonly Func<object> _rootGetter;
+        private readonly IObservable<object> _rootObservable;
+        private IDisposable _rootObserverSubscription;
         private int _count;
-        private ExpressionNode _node;
+        private readonly ExpressionNode _node;
         private ISubject<object> _empty;
 
         /// <summary>
@@ -37,21 +40,50 @@ namespace Perspex.Markup.Data
         /// <param name="root">The root object.</param>
         /// <param name="expression">The expression.</param>
         public ExpressionObserver(object root, string expression)
-            : this(() => root, expression)
         {
+            Contract.Requires<ArgumentNullException>(expression != null);
+
+            _root = root;
+
+            if (!string.IsNullOrWhiteSpace(expression))
+            {
+                _node = ExpressionNodeBuilder.Build(expression);
+            }
+
+            Expression = expression;
         }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
         /// </summary>
-        /// <param name="root">A function which gets the root object.</param>
+        /// <param name="rootObservable">An observable which provides the root object.</param>
         /// <param name="expression">The expression.</param>
-        public ExpressionObserver(Func<object> root, string expression)
+        public ExpressionObserver(IObservable<object> rootObservable, string expression)
         {
-            Contract.Requires<ArgumentNullException>(root != null);
+            Contract.Requires<ArgumentNullException>(rootObservable != null);
             Contract.Requires<ArgumentNullException>(expression != null);
 
-            _root = root;
+            _rootObservable = rootObservable;
+
+            if (!string.IsNullOrWhiteSpace(expression))
+            {
+                _node = ExpressionNodeBuilder.Build(expression);
+            }
+
+            Expression = expression;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
+        /// </summary>
+        /// <param name="rootGetter">A function which gets the root object.</param>
+        /// <param name="expression">The expression.</param>
+        public ExpressionObserver(Func<object> rootGetter, string expression)
+        {
+            Contract.Requires<ArgumentNullException>(rootGetter != null);
+            Contract.Requires<ArgumentNullException>(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
         }
 
         /// <summary>
-        /// Causes the root object to be re-read.
+        /// Causes the root object to be re-read from the root getter.
         /// </summary>
         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<object>(_root());
+                    _empty = new BehaviorSubject<object>(_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;
             }
         }

+ 1 - 1
src/Perspex.SceneGraph/NameScope.cs

@@ -17,7 +17,7 @@ namespace Perspex
         public static readonly PerspexProperty<INameScope> NameScopeProperty =
             PerspexProperty.RegisterAttached<NameScope, Visual, INameScope>("NameScope");
 
-        private Dictionary<string, object> _inner = new Dictionary<string, object>();
+        private readonly Dictionary<string, object> _inner = new Dictionary<string, object>();
 
         /// <summary>
         /// Raised when an element is registered with the name scope.

+ 27 - 1
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<object>();
+
+            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<Notification<object>> OnNext(long time, object value)
+        {
+            return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
+        }
     }
 }

+ 1 - 0
tests/Perspex.Markup.UnitTests/packages.config

@@ -5,6 +5,7 @@
   <package id="Rx-Linq" version="2.2.5" targetFramework="net46" />
   <package id="Rx-Main" version="2.2.5" targetFramework="net46" />
   <package id="Rx-PlatformServices" version="2.2.5" targetFramework="net46" />
+  <package id="Rx-Testing" version="2.2.5" targetFramework="net46" />
   <package id="xunit" version="2.0.0" targetFramework="net46" />
   <package id="xunit.abstractions" version="2.0.0" targetFramework="net46" />
   <package id="xunit.assert" version="2.0.0" targetFramework="net46" />

+ 83 - 0
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);
+        }
+    }
+}

+ 2 - 0
tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

@@ -88,6 +88,7 @@
     <Otherwise />
   </Choose>
   <ItemGroup>
+    <Compile Include="Data\BindingTests_ElementName.cs" />
     <Compile Include="Data\MultiBindingTests.cs" />
     <Compile Include="Data\BindingTests_TemplatedParent.cs" />
     <Compile Include="Data\BindingTests.cs" />
@@ -104,6 +105,7 @@
     <Compile Include="SampleModel\UserRepositoriesViewModel.cs" />
     <Compile Include="SamplePerspexObject.cs" />
     <Compile Include="Templates\DataTemplateTests.cs" />
+    <Compile Include="TestRoot.cs" />
     <Compile Include="TypeProviderMock.cs" />
     <Compile Include="ViewModelMock.cs" />
   </ItemGroup>

+ 1 - 1
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
     {

+ 61 - 0
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<NameScopeEventArgs> INameScope.Registered
+        {
+            add { _nameScope.Registered += value; ++NameScopeRegisteredSubscribers; }
+            remove { _nameScope.Registered -= value; --NameScopeRegisteredSubscribers; }
+        }
+
+        public event EventHandler<NameScopeEventArgs> 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);
+        }
+    }
+}

+ 1 - 1
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
     {

+ 1 - 1
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
     {