From d1acf4325389ff85684f56f15ac2b805139260c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 31 Oct 2015 15:42:50 +0100 Subject: [PATCH] Started fixing up MultiBinding. --- .../Context/PerspexXamlMemberValuePlugin.cs | 10 +- .../Perspex.Markup.Xaml/Data/Binding.cs | 101 ++++++++++++------ .../Perspex.Markup.Xaml/Data/IBinding.cs | 7 -- .../Perspex.Markup.Xaml/Data/IXamlBinding.cs | 35 ++++++ .../Perspex.Markup.Xaml/Data/MultiBinding.cs | 73 +++++++++---- .../Perspex.Markup.Xaml.csproj | 2 +- .../Perspex.Markup/FuncMultiValueConverter.cs | 2 +- .../Data/BindingTests.cs | 4 +- .../Data/MultiBindingTests.cs | 53 +++++++++ .../Perspex.Markup.Xaml.UnitTests.csproj | 1 + 10 files changed, 217 insertions(+), 71 deletions(-) delete mode 100644 src/Markup/Perspex.Markup.Xaml/Data/IBinding.cs create mode 100644 src/Markup/Perspex.Markup.Xaml/Data/IXamlBinding.cs create mode 100644 tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index c16c77af8d..f6a33aa026 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -39,9 +39,9 @@ namespace Perspex.Markup.Xaml.Context public override void SetValue(object instance, object value) { - if (value is IBinding) + if (value is IXamlBinding) { - HandleBinding(instance, (IBinding)value); + HandleBinding(instance, (IXamlBinding)value); } else if (IsPerspexProperty) { @@ -68,9 +68,9 @@ namespace Perspex.Markup.Xaml.Context po.SetValue(pp, value); } - private void HandleBinding(object instance, IBinding binding) + private void HandleBinding(object instance, IXamlBinding binding) { - if (typeof(IBinding).GetTypeInfo().IsAssignableFrom(_xamlMember.XamlType.UnderlyingType.GetTypeInfo())) + if (typeof(IXamlBinding).GetTypeInfo().IsAssignableFrom(_xamlMember.XamlType.UnderlyingType.GetTypeInfo())) { var property = instance.GetType().GetRuntimeProperty(_xamlMember.Name); @@ -88,7 +88,7 @@ namespace Perspex.Markup.Xaml.Context } } - private void ApplyBinding(object instance, IBinding binding) + private void ApplyBinding(object instance, IXamlBinding binding) { var perspexObject = instance as PerspexObject; var attached = _xamlMember as PerspexAttachableXamlMember; diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 3ef83da68a..8cc5a981f5 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -10,28 +10,47 @@ using Perspex.Markup.Data; namespace Perspex.Markup.Xaml.Data { - public class Binding : IBinding + /// + /// A XAML binding. + /// + public class Binding : IXamlBinding { - private readonly ITypeConverterProvider _typeConverterProvider; - - public Binding() - { - } - - public Binding(ITypeConverterProvider typeConverterProvider) - { - _typeConverterProvider = typeConverterProvider; - } - + /// + /// Gets or sets the to use. + /// public IValueConverter Converter { get; set; } + + /// + /// Gets or sets the binding mode. + /// public BindingMode Mode { get; set; } + + /// + /// Gets or sets the binding priority. + /// public BindingPriority Priority { get; set; } + + /// + /// Gets or sets the relative source for the binding. + /// public RelativeSource RelativeSource { get; set; } + + /// + /// Gets or sets the binding path. + /// public string SourcePropertyPath { get; set; } + /// + /// Applies the binding to a property on an instance. + /// + /// The target instance. + /// The target property. public void Bind(IObservablePropertyBag instance, PerspexProperty property) { - var subject = CreateExpressionSubject(instance, property); + var subject = CreateSubject( + instance, + property.PropertyType, + property == Control.DataContextProperty); if (subject != null) { @@ -39,19 +58,29 @@ namespace Perspex.Markup.Xaml.Data } } - public ISubject CreateExpressionSubject( - IObservablePropertyBag instance, - PerspexProperty property) + /// + /// Creates a subject that can be used to get and set the value of the binding. + /// + /// The target instance. + /// The type of the target property. + /// + /// Whether the target property is the DataContext property. + /// + /// An . + public ISubject CreateSubject( + IObservablePropertyBag target, + Type targetType, + bool targetIsDataContext = false) { ExpressionObserver observer; if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextExpressionSubject(instance, property); + observer = CreateDataContextExpressionSubject(target, targetIsDataContext); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentExpressionSubject(instance, property); + observer = CreateTemplatedParentExpressionSubject(target); } else { @@ -60,10 +89,16 @@ namespace Perspex.Markup.Xaml.Data return new ExpressionSubject( observer, - property.PropertyType, + targetType, Converter ?? DefaultValueConverter.Instance); } + /// + /// Applies a binding subject to a property on an instance. + /// + /// The target instance. + /// The target property. + /// The binding subject. internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject subject) { var mode = Mode == BindingMode.Default ? @@ -90,13 +125,12 @@ namespace Perspex.Markup.Xaml.Data } } - public ExpressionObserver CreateDataContextExpressionSubject( - IObservablePropertyBag instance, - PerspexProperty property) + private ExpressionObserver CreateDataContextExpressionSubject( + IObservablePropertyBag target, + bool targetIsDataContext) { - var dataContextHost = property != Control.DataContextProperty ? - instance : - instance.InheritanceParent as IObservablePropertyBag; + var dataContextHost = targetIsDataContext ? + target.InheritanceParent as IObservablePropertyBag : target; if (dataContextHost != null) { @@ -107,23 +141,24 @@ namespace Perspex.Markup.Xaml.Data result.UpdateRoot()); return result; } - - return null; + else + { + throw new InvalidOperationException( + "Cannot bind to DataContext of object with no parent."); + } } - public ExpressionObserver CreateTemplatedParentExpressionSubject( - IObservablePropertyBag instance, - PerspexProperty property) + private ExpressionObserver CreateTemplatedParentExpressionSubject(IObservablePropertyBag target) { var result = new ExpressionObserver( - () => instance.GetValue(Control.TemplatedParentProperty), + () => target.GetValue(Control.TemplatedParentProperty), GetExpression()); - if (instance.GetValue(Control.TemplatedParentProperty) == null) + if (target.GetValue(Control.TemplatedParentProperty) == null) { // TemplatedParent should only be set once, so only listen for the first non-null // value. - instance.GetObservable(Control.TemplatedParentProperty) + target.GetObservable(Control.TemplatedParentProperty) .Where(x => x != null) .Take(1) .Subscribe(x => result.UpdateRoot()); diff --git a/src/Markup/Perspex.Markup.Xaml/Data/IBinding.cs b/src/Markup/Perspex.Markup.Xaml/Data/IBinding.cs deleted file mode 100644 index 28fc9ba1e0..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/Data/IBinding.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Perspex.Markup.Xaml.Data -{ - public interface IBinding - { - void Bind(IObservablePropertyBag instance, PerspexProperty property); - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Data/IXamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Data/IXamlBinding.cs new file mode 100644 index 0000000000..e72fb5eccc --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Data/IXamlBinding.cs @@ -0,0 +1,35 @@ +// 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 System.Reactive.Subjects; + +namespace Perspex.Markup.Xaml.Data +{ + /// + /// Defines a binding that can be created in XAML markup. + /// + public interface IXamlBinding + { + /// + /// Applies the binding to a property on an instance. + /// + /// The target instance. + /// The target property. + void Bind(IObservablePropertyBag instance, PerspexProperty property); + + /// + /// Creates a subject that can be used to get and set the value of the binding. + /// + /// The target instance. + /// The type of the target property. + /// + /// Whether the target property is the DataContext property. + /// + /// An . + ISubject CreateSubject( + IObservablePropertyBag target, + Type targetType, + bool targetIsDataContext = false); + } +} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Data/MultiBinding.cs b/src/Markup/Perspex.Markup.Xaml/Data/MultiBinding.cs index e4896de5e7..46a01a166c 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/MultiBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/MultiBinding.cs @@ -7,37 +7,50 @@ using System.Globalization; using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; -using OmniXaml.TypeConversion; using Perspex.Controls; -using Perspex.Markup.Data; using Perspex.Metadata; namespace Perspex.Markup.Xaml.Data { - public class MultiBinding : IBinding + /// + /// A XAML binding that calculates an aggregate value from multiple child . + /// + public class MultiBinding : IXamlBinding { - private readonly ITypeConverterProvider _typeConverterProvider; - - public MultiBinding() - { - } - - public MultiBinding(ITypeConverterProvider typeConverterProvider) - { - _typeConverterProvider = typeConverterProvider; - } - + /// + /// Gets the collection of child bindings. + /// [Content] - public IList Bindings { get; } = new List(); + public IList Bindings { get; set; } = new List(); + + /// + /// Gets or sets the to use. + /// public IMultiValueConverter Converter { get; set; } + + /// + /// Gets or sets the binding mode. + /// public BindingMode Mode { get; set; } + + /// + /// Gets or sets the binding priority. + /// public BindingPriority Priority { get; set; } + + /// + /// Gets or sets the relative source for the binding. + /// public RelativeSource RelativeSource { get; set; } - public string SourcePropertyPath { get; set; } + /// + /// Applies the binding to a property on an instance. + /// + /// The target instance. + /// The target property. public void Bind(IObservablePropertyBag instance, PerspexProperty property) { - var subject = CreateSubject(instance, property); + var subject = CreateSubject(instance, property.PropertyType); if (subject != null) { @@ -45,23 +58,39 @@ namespace Perspex.Markup.Xaml.Data } } + /// + /// Creates a subject that can be used to get and set the value of the binding. + /// + /// The target instance. + /// The type of the target property. + /// + /// Whether the target property is the DataContext property. + /// + /// An . public ISubject CreateSubject( - IObservablePropertyBag instance, - PerspexProperty property) + IObservablePropertyBag target, + Type targetType, + bool targetIsDataContext = false) { if (Converter == null) { throw new NotSupportedException("MultiBinding without Converter not currently supported."); } - var result = new Subject(); - var children = Bindings.Select(x => x.CreateExpressionSubject(instance, property)); + var result = new BehaviorSubject(PerspexProperty.UnsetValue); + var children = Bindings.Select(x => x.CreateSubject(target, typeof(object))); var input = Observable.CombineLatest(children).Select(x => - Converter.Convert(x, property.PropertyType, null, CultureInfo.CurrentUICulture)); + Converter.Convert(x, targetType, null, CultureInfo.CurrentUICulture)); input.Subscribe(result); return result; } + /// + /// Applies a binding subject to a property on an instance. + /// + /// The target instance. + /// The target property. + /// The binding subject. internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject subject) { var mode = Mode == BindingMode.Default ? diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 60046e001c..5029d97364 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -39,7 +39,7 @@ Properties\SharedAssemblyInfo.cs - + diff --git a/src/Markup/Perspex.Markup/FuncMultiValueConverter.cs b/src/Markup/Perspex.Markup/FuncMultiValueConverter.cs index 2113e1a084..11bb4fc079 100644 --- a/src/Markup/Perspex.Markup/FuncMultiValueConverter.cs +++ b/src/Markup/Perspex.Markup/FuncMultiValueConverter.cs @@ -30,7 +30,7 @@ namespace Perspex.Markup /// public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) { - return _convert(values.OfType()); + return _convert(values.Cast()); } } } diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs index 372effc557..2c4441d3a3 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -148,7 +148,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data SourcePropertyPath = "Foo", }; - var result = binding.CreateExpressionSubject(target.Object, TextBox.TextProperty); + var result = binding.CreateSubject(target.Object, TextBox.TextProperty.PropertyType); Assert.IsType(((ExpressionSubject)result).Converter); } @@ -164,7 +164,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data SourcePropertyPath = "Foo", }; - var result = binding.CreateExpressionSubject(target.Object, TextBox.TextProperty); + var result = binding.CreateSubject(target.Object, TextBox.TextProperty.PropertyType); Assert.Same(converter.Object, ((ExpressionSubject)result).Converter); } diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs new file mode 100644 index 0000000000..4a86be98f7 --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs @@ -0,0 +1,53 @@ +// 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 System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Moq; +using Perspex.Controls; +using Perspex.Markup.Xaml.Data; +using Xunit; + +namespace Perspex.Markup.Xaml.UnitTests.Data +{ + public class MultiBindingTests + { + [Fact] + public async void OneWay_Binding_Should_Be_Set_Up() + { + var source = new { A = 1, B = 2, C = 3 }; + var binding = new MultiBinding + { + Converter = new ConcatConverter(), + Bindings = new[] + { + new Binding { SourcePropertyPath = "A" }, + new Binding { SourcePropertyPath = "B" }, + new Binding { SourcePropertyPath = "C" }, + } + }; + + var target = new Mock(); + target.Setup(x => x.GetValue(Control.DataContextProperty)).Returns(source); + target.Setup(x => x.GetObservable(Control.DataContextProperty)).Returns( + Observable.Never().StartWith(source)); + + var subject = binding.CreateSubject(target.Object, typeof(string)); + var result = await subject.Take(1); + + Assert.Equal("1,2,3", result); + } + + private class ConcatConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return string.Join(",", values); + } + } + } +} 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 11b0441a53..6f075c8f98 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 @@ +