Browse Source

Started fixing up MultiBinding.

pull/301/head
Steven Kirk 11 years ago
parent
commit
d1acf43253
  1. 10
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs
  2. 101
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  3. 7
      src/Markup/Perspex.Markup.Xaml/Data/IBinding.cs
  4. 35
      src/Markup/Perspex.Markup.Xaml/Data/IXamlBinding.cs
  5. 73
      src/Markup/Perspex.Markup.Xaml/Data/MultiBinding.cs
  6. 2
      src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
  7. 2
      src/Markup/Perspex.Markup/FuncMultiValueConverter.cs
  8. 4
      tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs
  9. 53
      tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs
  10. 1
      tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

10
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;

101
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
/// <summary>
/// A XAML binding.
/// </summary>
public class Binding : IXamlBinding
{
private readonly ITypeConverterProvider _typeConverterProvider;
public Binding()
{
}
public Binding(ITypeConverterProvider typeConverterProvider)
{
_typeConverterProvider = typeConverterProvider;
}
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter Converter { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the relative source for the binding.
/// </summary>
public RelativeSource RelativeSource { get; set; }
/// <summary>
/// Gets or sets the binding path.
/// </summary>
public string SourcePropertyPath { get; set; }
/// <summary>
/// Applies the binding to a property on an instance.
/// </summary>
/// <param name="instance">The target instance.</param>
/// <param name="property">The target property.</param>
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<object> CreateExpressionSubject(
IObservablePropertyBag instance,
PerspexProperty property)
/// <summary>
/// Creates a subject that can be used to get and set the value of the binding.
/// </summary>
/// <param name="target">The target instance.</param>
/// <param name="targetType">The type of the target property.</param>
/// <param name="targetIsDataContext">
/// Whether the target property is the DataContext property.
/// </param>
/// <returns>An <see cref="ISubject{object}"/>.</returns>
public ISubject<object> 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);
}
/// <summary>
/// Applies a binding subject to a property on an instance.
/// </summary>
/// <param name="target">The target instance.</param>
/// <param name="property">The target property.</param>
/// <param name="subject">The binding subject.</param>
internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject<object> 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());

7
src/Markup/Perspex.Markup.Xaml/Data/IBinding.cs

@ -1,7 +0,0 @@
namespace Perspex.Markup.Xaml.Data
{
public interface IBinding
{
void Bind(IObservablePropertyBag instance, PerspexProperty property);
}
}

35
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
{
/// <summary>
/// Defines a binding that can be created in XAML markup.
/// </summary>
public interface IXamlBinding
{
/// <summary>
/// Applies the binding to a property on an instance.
/// </summary>
/// <param name="instance">The target instance.</param>
/// <param name="property">The target property.</param>
void Bind(IObservablePropertyBag instance, PerspexProperty property);
/// <summary>
/// Creates a subject that can be used to get and set the value of the binding.
/// </summary>
/// <param name="target">The target instance.</param>
/// <param name="targetType">The type of the target property.</param>
/// <param name="targetIsDataContext">
/// Whether the target property is the DataContext property.
/// </param>
/// <returns>An <see cref="ISubject{object}"/>.</returns>
ISubject<object> CreateSubject(
IObservablePropertyBag target,
Type targetType,
bool targetIsDataContext = false);
}
}

73
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
/// <summary>
/// A XAML binding that calculates an aggregate value from multiple child <see cref="Bindings"/>.
/// </summary>
public class MultiBinding : IXamlBinding
{
private readonly ITypeConverterProvider _typeConverterProvider;
public MultiBinding()
{
}
public MultiBinding(ITypeConverterProvider typeConverterProvider)
{
_typeConverterProvider = typeConverterProvider;
}
/// <summary>
/// Gets the collection of child bindings.
/// </summary>
[Content]
public IList<Binding> Bindings { get; } = new List<Binding>();
public IList<IXamlBinding> Bindings { get; set; } = new List<IXamlBinding>();
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IMultiValueConverter Converter { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the relative source for the binding.
/// </summary>
public RelativeSource RelativeSource { get; set; }
public string SourcePropertyPath { get; set; }
/// <summary>
/// Applies the binding to a property on an instance.
/// </summary>
/// <param name="instance">The target instance.</param>
/// <param name="property">The target property.</param>
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
}
}
/// <summary>
/// Creates a subject that can be used to get and set the value of the binding.
/// </summary>
/// <param name="target">The target instance.</param>
/// <param name="targetType">The type of the target property.</param>
/// <param name="targetIsDataContext">
/// Whether the target property is the DataContext property.
/// </param>
/// <returns>An <see cref="ISubject{object}"/>.</returns>
public ISubject<object> 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<object>();
var children = Bindings.Select(x => x.CreateExpressionSubject(instance, property));
var result = new BehaviorSubject<object>(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;
}
/// <summary>
/// Applies a binding subject to a property on an instance.
/// </summary>
/// <param name="target">The target instance.</param>
/// <param name="property">The target property.</param>
/// <param name="subject">The binding subject.</param>
internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject<object> subject)
{
var mode = Mode == BindingMode.Default ?

2
src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj

@ -39,7 +39,7 @@
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Converters\PerspexListTypeConverter.cs" />
<Compile Include="Data\IBinding.cs" />
<Compile Include="Data\IXamlBinding.cs" />
<Compile Include="Data\MultiBinding.cs" />
<Compile Include="Data\RelativeSource.cs" />
<Compile Include="Data\SourceBindingEndpoint.cs" />

2
src/Markup/Perspex.Markup/FuncMultiValueConverter.cs

@ -30,7 +30,7 @@ namespace Perspex.Markup
/// <inheritdoc/>
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
return _convert(values.OfType<TIn>());
return _convert(values.Cast<TIn>());
}
}
}

4
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<DefaultValueConverter>(((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);
}

53
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<IObservablePropertyBag>();
target.Setup(x => x.GetValue(Control.DataContextProperty)).Returns(source);
target.Setup(x => x.GetObservable(Control.DataContextProperty)).Returns(
Observable.Never<object>().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<object> values, Type targetType, object parameter, CultureInfo culture)
{
return string.Join(",", values);
}
}
}
}

1
tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

@ -88,6 +88,7 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="Data\MultiBindingTests.cs" />
<Compile Include="Data\BindingTests_TemplatedParent.cs" />
<Compile Include="Data\BindingTests.cs" />
<Compile Include="Converters\PerspexPropertyConverterTest.cs" />

Loading…
Cancel
Save