Browse Source

Make IBinding return an InstancedBinding.

Instead of an ISubject<object> as this was wasteful when a OneTime or
OneWay binding was required.
pull/464/head
Steven Kirk 11 years ago
parent
commit
8209a85193
  1. 17
      src/Markup/Perspex.Markup.Xaml/Context/PropertyAccessor.cs
  2. 6
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  3. 8
      src/Markup/Perspex.Markup.Xaml/Data/MultiBinding.cs
  4. 53
      src/Markup/Perspex.Markup.Xaml/Data/StyleResourceBinding.cs
  5. 71
      src/Perspex.Base/Data/BindingOperations.cs
  6. 32
      src/Perspex.Base/Data/IBinding.cs
  7. 108
      src/Perspex.Base/Data/InstancedBinding.cs
  8. 2
      src/Perspex.Base/Perspex.Base.csproj
  9. 3
      src/Perspex.Base/PerspexObject.cs
  10. 66
      src/Perspex.Base/PerspexObjectExtensions.cs
  11. 3
      src/Perspex.Styling/Styling/ActivatedObservable.cs
  12. 2
      src/Perspex.Styling/Styling/ISetter.cs
  13. 57
      src/Perspex.Styling/Styling/Setter.cs
  14. 6
      tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs
  15. 2
      tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs
  16. 26
      tests/Perspex.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  17. 3
      tests/Perspex.Styling.UnitTests/SetterTests.cs

17
src/Markup/Perspex.Markup.Xaml/Context/PropertyAccessor.cs

@ -130,12 +130,25 @@ namespace Perspex.Markup.Xaml.Context
}
else
{
IPerspexObject treeAnchor = context.TopDownValueContext.StoredInstances
// The target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the TopDownValueContext.
object anchor = context.TopDownValueContext.StoredInstances
.Select(x => x.Instance)
.OfType<IControl>()
.LastOrDefault();
((IPerspexObject)instance).Bind(property, binding, treeAnchor);
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
if (anchor == null)
{
anchor = context.TopDownValueContext.StoredInstances
.Select(x => x.Instance)
.OfType<IStyle>()
.FirstOrDefault();
}
((IPerspexObject)instance).Bind(property, binding, anchor);
}
return true;

6
src/Markup/Perspex.Markup.Xaml/Data/Binding.cs

@ -62,7 +62,7 @@ namespace Perspex.Markup.Xaml.Data
public object Source { get; set; }
/// <inheritdoc/>
public ISubject<object> CreateSubject(
public InstancedBinding Initiate(
IPerspexObject target,
PerspexProperty targetProperty,
object anchor = null)
@ -101,12 +101,14 @@ namespace Perspex.Markup.Xaml.Data
throw new NotSupportedException();
}
return new ExpressionSubject(
var subject = new ExpressionSubject(
observer,
targetProperty?.PropertyType ?? typeof(object),
Converter ?? DefaultValueConverter.Instance,
ConverterParameter,
FallbackValue);
return new InstancedBinding(subject, Mode, Priority);
}
private static PathInfo ParsePath(string path)

8
src/Markup/Perspex.Markup.Xaml/Data/MultiBinding.cs

@ -50,7 +50,7 @@ namespace Perspex.Markup.Xaml.Data
public RelativeSource RelativeSource { get; set; }
/// <inheritdoc/>
public ISubject<object> CreateSubject(
public InstancedBinding Initiate(
IPerspexObject target,
PerspexProperty targetProperty,
object anchor = null)
@ -62,10 +62,10 @@ namespace Perspex.Markup.Xaml.Data
var targetType = targetProperty?.PropertyType ?? typeof(object);
var result = new BehaviorSubject<object>(PerspexProperty.UnsetValue);
var children = Bindings.Select(x => x.CreateSubject(target, null));
var input = children.CombineLatest().Select(x => ConvertValue(x, targetType));
var children = Bindings.Select(x => x.Initiate(target, null));
var input = children.Select(x => x.Subject).CombineLatest().Select(x => ConvertValue(x, targetType));
input.Subscribe(result);
return result;
return new InstancedBinding(result, Mode, Priority);
}
/// <summary>

53
src/Markup/Perspex.Markup.Xaml/Data/StyleResourceBinding.cs

@ -34,60 +34,31 @@ namespace Perspex.Markup.Xaml.Data
public BindingPriority Priority => BindingPriority.LocalValue;
/// <inheritdoc/>
public ISubject<object> CreateSubject(
public InstancedBinding Initiate(
IPerspexObject target,
PerspexProperty targetProperty,
object anchor = null)
{
return new Subject(target, Name, anchor);
}
private class Subject : ISubject<object>
{
private IPerspexObject _target;
private string _name;
private object _anchor;
public Subject(IPerspexObject target, string name, object anchor)
{
_target = target;
_name = name;
this._anchor = anchor;
}
var host = (target as IControl) ?? (anchor as IControl);
var style = anchor as IStyle;
var resource = PerspexProperty.UnsetValue;
public void OnCompleted()
if (host != null)
{
resource = host.FindStyleResource(Name);
}
public void OnError(Exception error)
else if (style != null)
{
resource = style.FindResource(Name);
}
public void OnNext(object value)
if (resource != PerspexProperty.UnsetValue)
{
return new InstancedBinding(resource, Priority);
}
public IDisposable Subscribe(IObserver<object> observer)
else
{
var host = (_target as IControl) ?? (_anchor as IControl);
if (host != null)
{
var resource = host.FindStyleResource(_name);
if (resource != PerspexProperty.UnsetValue)
{
observer.OnNext(resource);
}
observer.OnCompleted();
}
else
{
// TODO: Log error.
}
return Disposable.Empty;
return null;
}
}
}

71
src/Perspex.Base/Data/BindingOperations.cs

@ -0,0 +1,71 @@
// 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.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
namespace Perspex.Data
{
public static class BindingOperations
{
/// <summary>
/// Applies an <see cref="InstancedBinding"/> a property on an <see cref="IPerspexObject"/>.
/// </summary>
/// <param name="target">The target object.</param>
/// <param name="property">The property to bind.</param>
/// <param name="binding">The instanced binding.</param>
/// <param name="anchor">
/// An optional anchor from which to locate required context. When binding to objects that
/// are not in the logical tree, certain types of binding need an anchor into the tree in
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provice this context.
/// </param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
public static IDisposable Apply(
IPerspexObject target,
PerspexProperty property,
InstancedBinding binding,
object anchor)
{
Contract.Requires<ArgumentNullException>(target != null);
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(binding != null);
var mode = binding.Mode;
if (mode == BindingMode.Default)
{
mode = property.GetMetadata(target.GetType()).DefaultBindingMode;
}
switch (mode)
{
case BindingMode.Default:
case BindingMode.OneWay:
return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority);
case BindingMode.TwoWay:
return new CompositeDisposable(
target.Bind(property, binding.Subject, binding.Priority),
target.GetObservable(property).Subscribe(binding.Subject));
case BindingMode.OneTime:
var source = binding.Subject ?? binding.Observable;
if (source != null)
{
return source.Take(1).Subscribe(x => target.SetValue(property, x, binding.Priority));
}
else
{
target.SetValue(property, binding.Value, binding.Priority);
return Disposable.Empty;
}
case BindingMode.OneWayToSource:
return target.GetObservable(property).Subscribe(binding.Subject);
default:
throw new ArgumentException("Invalid binding mode.");
}
}
}
}

32
src/Perspex.Base/Data/IBinding.cs

@ -1,8 +1,6 @@
// 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.Reactive.Subjects;
namespace Perspex.Data
{
/// <summary>
@ -11,28 +9,20 @@ namespace Perspex.Data
public interface IBinding
{
/// <summary>
/// Gets the binding mode.
/// </summary>
BindingMode Mode { get; }
/// <summary>
/// Gets the binding priority.
/// </summary>
BindingPriority Priority { get; }
/// <summary>
/// Creates a subject that can be used to get and set the value of the binding.
/// Initiates the binding on a target object.
/// </summary>
/// <param name="target">The target instance.</param>
/// <param name="targetProperty">The target property. May be null.</param>
/// <param name="anchor">An optional anchor from which to locate required context.</param>
/// <returns>An <see cref="ISubject{Object}"/>.</returns>
/// <remarks>
/// When binding to objects that are not in the logical tree, certain types of binding need
/// an anchor into the tree in order to locate named controls or resources. The
/// <paramref name="anchor"/> parameter can be used to provice this context.
/// </remarks>
ISubject<object> CreateSubject(
/// <param name="anchor">
/// An optional anchor from which to locate required context. When binding to objects that
/// are not in the logical tree, certain types of binding need an anchor into the tree in
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provice this context.
/// </param>
/// <returns>
/// A <see cref="InstancedBinding"/> or null if the binding could not be resolved.
/// </returns>
InstancedBinding Initiate(
IPerspexObject target,
PerspexProperty targetProperty,
object anchor = null);

108
src/Perspex.Base/Data/InstancedBinding.cs

@ -0,0 +1,108 @@
// 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.Data
{
/// <summary>
/// Holds the result of calling <see cref="IBinding.Initiate"/>.
/// </summary>
/// <remarks>
/// Whereas an <see cref="IBinding"/> holds a description of a binding such as "Bind to the X
/// property on a control's DataContext"; this class represents a binding that has been
/// *instanced* by calling <see cref="IBinding.Initiate(IPerspexObject, PerspexProperty, object)"/>
/// on a target object.
///
/// When a binding is initiated, it can return one of 3 possible sources for the binding:
/// - An <see cref="ISubject{Object}"/> which can be used for any type of binding.
/// - An <see cref="IObservable{Object}"/> which can be used for all types of bindings except
/// <see cref="BindingMode.OneWayToSource"/> and <see cref="BindingMode.TwoWay"/>.
/// - A plain object, which can only represent a <see cref="BindingMode.OneTime"/> binding.
/// </remarks>
public class InstancedBinding
{
/// <summary>
/// Initializes a new instance of the <see cref="InstancedBinding"/> class.
/// </summary>
/// <param name="value">
/// The value used for the <see cref="BindingMode.OneTime"/> binding.
/// </param>
/// <param name="priority">The binding priority.</param>
public InstancedBinding(object value, BindingPriority priority = BindingPriority.LocalValue)
{
Mode = BindingMode.OneTime;
Priority = priority;
Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="InstancedBinding"/> class.
/// </summary>
/// <param name="observable">The observable for a one-way binding.</param>
/// <param name="mode">The binding mode.</param>
/// <param name="priority">The binding priority.</param>
public InstancedBinding(
IObservable<object> observable,
BindingMode mode = BindingMode.OneWay,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(observable != null);
if (mode == BindingMode.OneWayToSource || mode == BindingMode.TwoWay)
{
throw new ArgumentException(
"Invalid BindingResult mode: OneWayToSource and TwoWay bindings" +
"require a Subject.");
}
Mode = mode;
Priority = priority;
Observable = observable;
}
/// <summary>
/// Initializes a new instance of the <see cref="InstancedBinding"/> class.
/// </summary>
/// <param name="subject">The subject for a two-way binding.</param>
/// <param name="mode">The binding mode.</param>
/// <param name="priority">The binding priority.</param>
public InstancedBinding(
ISubject<object> subject,
BindingMode mode = BindingMode.OneWay,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(subject != null);
Mode = mode;
Priority = priority;
Subject = subject;
}
/// <summary>
/// Gets the binding mode with which the binding was initiated.
/// </summary>
public BindingMode Mode { get; }
/// <summary>
/// Gets the binding priority.
/// </summary>
public BindingPriority Priority { get; }
/// <summary>
/// Gets the value used for a <see cref="BindingMode.OneTime"/> binding.
/// </summary>
public object Value { get; }
/// <summary>
/// Gets the observable for a one-way binding.
/// </summary>
public IObservable<object> Observable { get; }
/// <summary>
/// Gets the subject for a two-way binding.
/// </summary>
public ISubject<object> Subject { get; }
}
}

2
src/Perspex.Base/Perspex.Base.csproj

@ -44,6 +44,8 @@
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" />
<Compile Include="Data\InstancedBinding.cs" />
<Compile Include="Data\IBinding.cs" />
<Compile Include="Data\IndexerDescriptor.cs" />
<Compile Include="Collections\PerspexDictionary.cs" />

3
src/Perspex.Base/PerspexObject.cs

@ -188,7 +188,8 @@ namespace Perspex
break;
case BindingMode.TwoWay:
var subject = sourceBinding.Source.GetSubject(sourceBinding.Property, sourceBinding.Priority);
this.Bind(binding.Property, subject, BindingMode.TwoWay, sourceBinding.Priority);
var instanced = new InstancedBinding(subject, BindingMode.TwoWay, sourceBinding.Priority);
BindingOperations.Apply(this, binding.Property, instanced, null);
break;
}
}

66
src/Perspex.Base/PerspexObjectExtensions.cs

@ -167,73 +167,35 @@ namespace Perspex
/// <summary>
/// Binds a property on an <see cref="IPerspexObject"/> to an <see cref="IBinding"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="target">The object.</param>
/// <param name="property">The property to bind.</param>
/// <param name="binding">The binding.</param>
/// <param name="treeAnchor">
/// For `ElementName` bindings to elements that are not themselves controls, describes
/// where in the logical tree to begin searching for the named element.
/// <param name="anchor">
/// An optional anchor from which to locate required context. When binding to objects that
/// are not in the logical tree, certain types of binding need an anchor into the tree in
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provice this context.
/// </param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
public static IDisposable Bind(
this IPerspexObject o,
this IPerspexObject target,
PerspexProperty property,
IBinding binding,
IPerspexObject treeAnchor = null)
object anchor = null)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(target != null);
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(binding != null);
var mode = binding.Mode;
var result = binding.Initiate(target, property, anchor);
if (mode == BindingMode.Default)
if (result != null)
{
mode = property.GetMetadata(o.GetType()).DefaultBindingMode;
return BindingOperations.Apply(target, property, result, anchor);
}
return o.Bind(
property,
binding.CreateSubject(o, property, treeAnchor),
mode,
binding.Priority);
}
/// <summary>
/// Binds a property to a subject according to a <see cref="BindingMode"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property to bind.</param>
/// <param name="source">The binding source.</param>
/// <param name="mode">The binding mode.</param>
/// <param name="priority">The binding priority.</param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
public static IDisposable Bind(
this IPerspexObject o,
PerspexProperty property,
ISubject<object> source,
BindingMode mode,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<ArgumentNullException>(o != null);
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(source != null);
switch (mode)
else
{
case BindingMode.Default:
case BindingMode.OneWay:
return o.Bind(property, source, priority);
case BindingMode.TwoWay:
return new CompositeDisposable(
o.Bind(property, source, priority),
o.GetObservable(property).Subscribe(source));
case BindingMode.OneTime:
return source.Take(1).Subscribe(x => o.SetValue(property, x, priority));
case BindingMode.OneWayToSource:
return o.GetObservable(property).Subscribe(source);
default:
throw new ArgumentException("Invalid binding mode.");
return Disposable.Empty;
}
}

3
src/Perspex.Styling/Styling/ActivatedObservable.cs

@ -30,6 +30,9 @@ namespace Perspex.Styling
IObservable<object> source,
string description)
{
Contract.Requires<ArgumentNullException>(activator != null);
Contract.Requires<ArgumentNullException>(source != null);
Activator = activator;
Description = description;
Source = source;

2
src/Perspex.Styling/Styling/ISetter.cs

@ -11,7 +11,7 @@ namespace Perspex.Styling
public interface ISetter
{
/// <summary>
/// Applies the setter to the control.
/// Applies the setter to a control.
/// </summary>
/// <param name="style">The style that is being applied.</param>
/// <param name="control">The control.</param>

57
src/Perspex.Styling/Styling/Setter.cs

@ -58,7 +58,7 @@ namespace Perspex.Styling
}
/// <summary>
/// Applies the setter to the control.
/// Applies the setter to a control.
/// </summary>
/// <param name="style">The style that is being applied.</param>
/// <param name="control">The control.</param>
@ -76,51 +76,52 @@ namespace Perspex.Styling
var binding = Value as IBinding;
if (binding != null)
if (binding == null)
{
if (activator == null)
{
control.Bind(Property, binding);
control.SetValue(Property, Value, BindingPriority.Style);
}
else
{
var subject = binding.CreateSubject(control, Property);
var activated = new ActivatedSubject(activator, subject, description);
Bind(control, Property, binding, activated);
var activated = new ActivatedValue(activator, Value, description);
var instanced = new InstancedBinding(
activated,
BindingMode.OneWay,
BindingPriority.StyleTrigger);
BindingOperations.Apply(control, Property, instanced, null);
}
}
else
{
if (activator == null)
{
control.SetValue(Property, Value, BindingPriority.Style);
control.Bind(Property, binding);
}
else
{
var activated = new ActivatedValue(activator, Value, description);
control.Bind(Property, activated, BindingPriority.StyleTrigger);
}
}
}
var sourceInstance = binding.Initiate(control, Property);
InstancedBinding activatedInstance;
private void Bind(
IStyleable control,
PerspexProperty property,
IBinding binding,
ISubject<object> subject)
{
var mode = binding.Mode;
if (sourceInstance.Subject != null)
{
var activated = new ActivatedSubject(activator, sourceInstance.Subject, description);
activatedInstance = new InstancedBinding(activated, sourceInstance.Mode, sourceInstance.Priority);
}
else if (sourceInstance.Observable != null)
{
var activated = new ActivatedObservable(activator, sourceInstance.Observable, description);
activatedInstance = new InstancedBinding(activated, sourceInstance.Mode, sourceInstance.Priority);
}
else
{
var activated = new ActivatedValue(activator, sourceInstance.Value, description);
activatedInstance = new InstancedBinding(activated, sourceInstance.Mode, sourceInstance.Priority);
}
if (mode == BindingMode.Default)
{
mode = property.GetMetadata(control.GetType()).DefaultBindingMode;
BindingOperations.Apply(control, Property, activatedInstance, null);
}
}
control.Bind(
property,
subject,
mode,
binding.Priority);
}
}
}

6
tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs

@ -168,7 +168,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data
Path = "Foo",
};
var result = binding.CreateSubject(target, TextBox.TextProperty);
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
Assert.IsType<DefaultValueConverter>(((ExpressionSubject)result).Converter);
}
@ -184,7 +184,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data
Path = "Foo",
};
var result = binding.CreateSubject(target, TextBox.TextProperty);
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
Assert.Same(converter.Object, ((ExpressionSubject)result).Converter);
}
@ -201,7 +201,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data
Path = "Bar",
};
var result = binding.CreateSubject(target, TextBox.TextProperty);
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
Assert.Same("foo", ((ExpressionSubject)result).ConverterParameter);
}

2
tests/Perspex.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs

@ -33,7 +33,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data
var target = new Mock<IPerspexObject>();
target.Setup(x => x.GetValue(Control.DataContextProperty)).Returns(source);
var subject = binding.CreateSubject(target.Object, null);
var subject = binding.Initiate(target.Object, null).Subject;
var result = await subject.Take(1);
Assert.Equal("1,2,3", result);

26
tests/Perspex.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -172,5 +172,31 @@ namespace Perspex.Markup.Xaml.UnitTests.Xaml
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
[Fact]
public void StyleResource_Can_Be_Found_In_Sibling_Styles()
{
var xaml = @"
<Styles xmlns='https://github.com/perspex'
xmlns:mut='https://github.com/perspex/mutable'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style>
<Style.Resources>
<Color x:Key='color'>#ff506070</Color>
</Style.Resources>
</Style>
<Style>
<Style.Resources>
<mut:SolidColorBrush x:Key='brush' Color='{StyleResource color}'/>
</Style.Resources>
</Style>
</Styles>";
var loader = new PerspexXamlLoader();
var styles = (Styles)loader.Load(xaml);
var brush = (Perspex.Media.Mutable.SolidColorBrush)styles.FindResource("brush");
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
}
}

3
tests/Perspex.Styling.UnitTests/SetterTests.cs

@ -16,7 +16,8 @@ namespace Perspex.Styling.UnitTests
{
var control = new TextBlock();
var subject = new BehaviorSubject<object>("foo");
var binding = Mock.Of<IBinding>(x => x.CreateSubject(control, TextBlock.TextProperty, null) == subject);
var descriptor = new InstancedBinding(subject);
var binding = Mock.Of<IBinding>(x => x.Initiate(control, TextBlock.TextProperty, null) == descriptor);
var style = Mock.Of<IStyle>();
var setter = new Setter(TextBlock.TextProperty, binding);

Loading…
Cancel
Save