Browse Source

More work on allowing bindings in setters.

Support activated ISubjects.
pull/396/head
Steven Kirk 11 years ago
parent
commit
36f2f3e1e0
  1. 30
      src/Perspex.Base/PerspexObjectExtensions.cs
  2. 4
      src/Perspex.Styling/Perspex.Styling.csproj
  3. 80
      src/Perspex.Styling/Styling/ActivatedObservable.cs
  4. 124
      src/Perspex.Styling/Styling/ActivatedSubject.cs
  5. 72
      src/Perspex.Styling/Styling/ActivatedValue.cs
  6. 19
      src/Perspex.Styling/Styling/Setter.cs
  7. 107
      src/Perspex.Styling/Styling/StyleBinding.cs
  8. 70
      tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs
  9. 46
      tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs
  10. 42
      tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs
  11. 4
      tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj
  12. 79
      tests/Perspex.Styling.UnitTests/StyleBindingTests.cs

30
src/Perspex.Base/PerspexObjectExtensions.cs

@ -103,6 +103,36 @@ namespace Perspex
GetDescription(o, property)); GetDescription(o, property));
} }
/// <summary>
/// Gets a subject for a <see cref="PerspexProperty"/>.
/// </summary>
/// <param name="o">The object.</param>
/// <param name="property">The property.</param>
/// <param name="priority">
/// The priority with which binding values are written to the object.
/// </param>
/// <returns>
/// An <see cref="ISubject{Object}"/> which can be used for two-way binding to/from the
/// property.
/// </returns>
public static ISubject<object> GetSubject(
this IPerspexObject o,
PerspexProperty property,
BindingPriority priority = BindingPriority.LocalValue)
{
// TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the
// AnonymousSubject classes from this file and use Subject.Create<T>.
var output = new Subject<object>();
var result = new AnonymousSubject<object>(
Observer.Create<object>(
x => output.OnNext(x),
e => output.OnError(e),
() => output.OnCompleted()),
o.GetObservable(property));
o.Bind(property, output, priority);
return result;
}
/// <summary> /// <summary>
/// Gets a subject for a <see cref="PerspexProperty"/>. /// Gets a subject for a <see cref="PerspexProperty"/>.
/// </summary> /// </summary>

4
src/Perspex.Styling/Perspex.Styling.csproj

@ -45,6 +45,8 @@
<Compile Include="ILogical.cs" /> <Compile Include="ILogical.cs" />
<Compile Include="LogicalTree\LogicalExtensions.cs" /> <Compile Include="LogicalTree\LogicalExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Styling\ActivatedSubject.cs" />
<Compile Include="Styling\ActivatedValue.cs" />
<Compile Include="Styling\IGlobalStyles.cs" /> <Compile Include="Styling\IGlobalStyles.cs" />
<Compile Include="Styling\ISetter.cs" /> <Compile Include="Styling\ISetter.cs" />
<Compile Include="Styling\IStyle.cs" /> <Compile Include="Styling\IStyle.cs" />
@ -59,7 +61,7 @@
<Compile Include="Styling\Setter.cs" /> <Compile Include="Styling\Setter.cs" />
<Compile Include="Styling\Style.cs" /> <Compile Include="Styling\Style.cs" />
<Compile Include="Styling\StyleActivator.cs" /> <Compile Include="Styling\StyleActivator.cs" />
<Compile Include="Styling\StyleBinding.cs" /> <Compile Include="Styling\ActivatedObservable.cs" />
<Compile Include="Styling\Styler.cs" /> <Compile Include="Styling\Styler.cs" />
<Compile Include="Styling\Styles.cs" /> <Compile Include="Styling\Styles.cs" />
</ItemGroup> </ItemGroup>

80
src/Perspex.Styling/Styling/ActivatedObservable.cs

@ -0,0 +1,80 @@
// 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;
using System.Reactive.Linq;
namespace Perspex.Styling
{
/// <summary>
/// An observable which is switched on or off according to an activator observable.
/// </summary>
/// <remarks>
/// An <see cref="ActivatedObservable"/> has two inputs: an activator observable a
/// <see cref="Source"/> observable which produces the activated value. When the activator
/// produces true, the <see cref="ActivatedObservable"/> will produce the current activated
/// value. When the activator produces false it will produce
/// <see cref="PerspexProperty.UnsetValue"/>.
/// </remarks>
internal class ActivatedObservable : ObservableBase<object>, IDescription
{
/// <summary>
/// The activator.
/// </summary>
private readonly IObservable<bool> _activator;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="source">An observable that produces the activated value.</param>
/// <param name="description">The binding description.</param>
public ActivatedObservable(
IObservable<bool> activator,
IObservable<object> source,
string description)
{
_activator = activator;
Description = description;
Source = source;
}
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
}
/// <summary>
/// Gets an observable which produces the <see cref="ActivatedValue"/>.
/// </summary>
public IObservable<object> Source
{
get;
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
var sourceCompleted = Source.TakeLast(1).Select(_ => Unit.Default);
var activatorCompleted = _activator.TakeLast(1).Select(_ => Unit.Default);
var completed = sourceCompleted.Merge(activatorCompleted);
return _activator
.CombineLatest(Source, (x, y) => new { Active = x, Value = y })
.Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue)
.DistinctUntilChanged()
.TakeUntil(completed)
.Subscribe(observer);
}
}
}

124
src/Perspex.Styling/Styling/ActivatedSubject.cs

@ -0,0 +1,124 @@
// 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;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace Perspex.Styling
{
/// <summary>
/// A subject which is switched on or off according to an activator observable.
/// </summary>
/// <remarks>
/// An <see cref="ActivatedSubject"/> has two inputs: an activator observable and either an
/// <see cref="ActivatedValue"/> or a <see cref="Source"/> observable which produces the
/// activated value. When the activator produces true, the <see cref="ActivatedObservable"/> will
/// produce the current activated value. When the activator produces false it will produce
/// <see cref="PerspexProperty.UnsetValue"/>.
/// </remarks>
internal class ActivatedSubject : ISubject<object>, IDescription
{
private IObservable<bool> _activator;
private bool _active;
private object _pushValue;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedSubject"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="source">An observable that produces the activated value.</param>
/// <param name="description">The binding description.</param>
public ActivatedSubject(
IObservable<bool> activator,
ISubject<object> source,
string description)
{
_activator = activator;
Description = description;
Source = source;
_activator.Skip(1).Subscribe(ActivatorChanged);
}
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
}
/// <summary>
/// Gets the underlying subject.
/// </summary>
public ISubject<object> Source
{
get;
}
/// <summary>
/// Notifies all subscribed observers about the end of the sequence.
/// </summary>
public void OnCompleted()
{
if (_active)
{
Source.OnCompleted();
}
}
/// <summary>
/// Notifies all subscribed observers with the exception.
/// </summary>
/// <param name="error">The exception to send to all subscribed observers.</param>
/// <exception cref="ArgumentNullException"><paramref name="error"/> is null.</exception>
public void OnError(Exception error)
{
if (_active)
{
Source.OnError(error);
}
}
/// <summary>
/// Notifies all subscribed observers with the value.
/// </summary>
/// <param name="value">The value to send to all subscribed observers.</param>
public void OnNext(object value)
{
_pushValue = value;
if (_active)
{
Source.OnNext(value);
}
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
public IDisposable Subscribe(IObserver<object> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
var completed = _activator.TakeLast(1).Select(_ => Unit.Default);
return _activator
.CombineLatest(Source, (x, y) => new { Active = x, Value = y })
.Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue)
.DistinctUntilChanged()
.TakeUntil(completed)
.Subscribe(observer);
}
private void ActivatorChanged(bool active)
{
_active = active;
Source.OnNext(active ? _pushValue : PerspexProperty.UnsetValue);
}
}
}

72
src/Perspex.Styling/Styling/ActivatedValue.cs

@ -0,0 +1,72 @@
// 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;
using System.Reactive.Linq;
namespace Perspex.Styling
{
/// <summary>
/// An value which is switched on or off according to an activator observable.
/// </summary>
/// <remarks>
/// An <see cref="ActivatedValue"/> has two inputs: an activator observable and an
/// <see cref="Value"/>. When the activator produces true, the
/// <see cref="ActivatedValue"/> will produce the current value. When the activator
/// produces false it will produce <see cref="PerspexProperty.UnsetValue"/>.
/// </remarks>
internal class ActivatedValue : ObservableBase<object>, IDescription
{
/// <summary>
/// The activator.
/// </summary>
private readonly IObservable<bool> _activator;
/// <summary>
/// Initializes a new instance of the <see cref="ActivatedObservable"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="value">The activated value.</param>
/// <param name="description">The binding description.</param>
public ActivatedValue(
IObservable<bool> activator,
object value,
string description)
{
_activator = activator;
Value = value;
Description = description;
}
/// <summary>
/// Gets the activated value.
/// </summary>
public object Value
{
get;
}
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
return _activator
.Select(active => active ? Value : PerspexProperty.UnsetValue)
.Subscribe(observer);
}
}
}

19
src/Perspex.Styling/Styling/Setter.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reactive.Subjects;
using Perspex.Data; using Perspex.Data;
using Perspex.Metadata; using Perspex.Metadata;
@ -77,8 +78,9 @@ namespace Perspex.Styling
} }
else else
{ {
throw new NotSupportedException( var subject = binding.CreateSubject(control, Property);
"Setter bindings with activators not yet supported."); var activated = new ActivatedSubject(activator, subject, style.ToString());
Bind(control, Property, binding, activated);
} }
} }
else else
@ -89,13 +91,22 @@ namespace Perspex.Styling
} }
else else
{ {
var activated = new StyleBinding(activator, Value, style.ToString()); var activated = new ActivatedValue(activator, Value, style.ToString());
control.Bind(Property, activated, BindingPriority.StyleTrigger); control.Bind(Property, activated, BindingPriority.StyleTrigger);
} }
} }
} }
private void Bind(IStyleable control, PerspexProperty property, IBinding binding) private void Bind(IStyleable control, PerspexProperty property, IBinding binding)
{
Bind(control, property, binding, binding.CreateSubject(control, property));
}
private void Bind(
IStyleable control,
PerspexProperty property,
IBinding binding,
ISubject<object> subject)
{ {
var mode = binding.Mode; var mode = binding.Mode;
@ -106,7 +117,7 @@ namespace Perspex.Styling
control.Bind( control.Bind(
property, property,
binding.CreateSubject(control, property), subject,
mode, mode,
binding.Priority); binding.Priority);
} }

107
src/Perspex.Styling/Styling/StyleBinding.cs

@ -1,107 +0,0 @@
// 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;
using System.Reactive.Linq;
namespace Perspex.Styling
{
/// <summary>
/// Provides an observable for a style.
/// </summary>
/// <remarks>
/// A <see cref="StyleBinding"/> has two inputs: an activator observable and either an
/// <see cref="ActivatedValue"/> or a <see cref="Source"/> observable which produces the
/// activated value. When the activator produces true, the <see cref="StyleBinding"/> will
/// produce the current activated value. When the activator produces false it will produce
/// <see cref="PerspexProperty.UnsetValue"/>.
/// </remarks>
internal class StyleBinding : ObservableBase<object>, IDescription
{
/// <summary>
/// The activator.
/// </summary>
private readonly IObservable<bool> _activator;
/// <summary>
/// Initializes a new instance of the <see cref="StyleBinding"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="activatedValue">The activated value.</param>
/// <param name="description">The binding description.</param>
public StyleBinding(
IObservable<bool> activator,
object activatedValue,
string description)
{
_activator = activator;
ActivatedValue = activatedValue;
Description = description;
}
/// <summary>
/// Initializes a new instance of the <see cref="StyleBinding"/> class.
/// </summary>
/// <param name="activator">The activator.</param>
/// <param name="source">An observable that produces the activated value.</param>
/// <param name="description">The binding description.</param>
public StyleBinding(
IObservable<bool> activator,
IObservable<object> source,
string description)
{
_activator = activator;
Description = description;
Source = source;
}
/// <summary>
/// Gets the activated value.
/// </summary>
public object ActivatedValue
{
get;
}
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
}
/// <summary>
/// Gets an observable which produces the <see cref="ActivatedValue"/>.
/// </summary>
public IObservable<object> Source
{
get;
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>IDisposable object used to unsubscribe from the observable sequence.</returns>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
if (Source == null)
{
return _activator
.Select(active => active ? ActivatedValue : PerspexProperty.UnsetValue)
.Subscribe(observer);
}
else
{
return _activator
.CombineLatest(Source, (x, y) => new { Active = x, Value = y })
.Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue)
.Subscribe(observer);
}
}
}
}

70
tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs

@ -0,0 +1,70 @@
// 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.Reactive.Linq;
using System.Reactive.Subjects;
using Xunit;
namespace Perspex.Styling.UnitTests
{
public class ActivatedObservableTests
{
[Fact]
public void Should_Produce_Correct_Values()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
source.OnNext(2);
activator.OnNext(false);
source.OnNext(3);
activator.OnNext(true);
Assert.Equal(
new[]
{
PerspexProperty.UnsetValue,
1,
2,
PerspexProperty.UnsetValue,
3,
},
result);
}
[Fact]
public void Should_Complete_When_Source_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
source.OnCompleted();
Assert.True(completed);
}
[Fact]
public void Should_Complete_When_Activator_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new ActivatedObservable(activator, source, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
activator.OnCompleted();
Assert.True(completed);
}
}
}

46
tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs

@ -0,0 +1,46 @@
// 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;
using Perspex.Data;
using Xunit;
namespace Perspex.Styling.UnitTests
{
public class ActivatedSubjectTests
{
[Fact]
public void Should_Set_Values()
{
var data = new Class1 { Foo = "foo" };
var activator = new BehaviorSubject<bool>(false);
var source = data.GetSubject(
(PerspexProperty)Class1.FooProperty,
BindingPriority.LocalValue);
var target = new ActivatedSubject(activator, source, string.Empty);
target.OnNext("bar");
Assert.Equal("foo", data.Foo);
activator.OnNext(true);
target.OnNext("baz");
Assert.Equal("baz", data.Foo);
activator.OnNext(false);
Assert.Equal("foo", data.Foo);
target.OnNext("bax");
activator.OnNext(true);
Assert.Equal("bax", data.Foo);
}
private class Class1 : PerspexObject
{
public static readonly PerspexProperty<string> FooProperty =
PerspexProperty.Register<Class1, string>("Foo", "foodefault");
public string Foo
{
get { return GetValue(FooProperty); }
set { SetValue(FooProperty, value); }
}
}
}
}

42
tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs

@ -0,0 +1,42 @@
// 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.Reactive.Linq;
using System.Reactive.Subjects;
using Xunit;
namespace Perspex.Styling.UnitTests
{
public class ActivatedValueTests
{
[Fact]
public void Should_Produce_Correct_Values()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
activator.OnNext(false);
Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, PerspexProperty.UnsetValue }, result);
}
[Fact]
public void Should_Complete_When_Activator_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var target = new ActivatedValue(activator, 1, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
activator.OnCompleted();
Assert.True(completed);
}
}
}

4
tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj

@ -80,6 +80,8 @@
<Otherwise /> <Otherwise />
</Choose> </Choose>
<ItemGroup> <ItemGroup>
<Compile Include="ActivatedValueTests.cs" />
<Compile Include="ActivatedSubjectTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SelectorTests_Class.cs" /> <Compile Include="SelectorTests_Class.cs" />
<Compile Include="SelectorTests_Child.cs" /> <Compile Include="SelectorTests_Child.cs" />
@ -90,7 +92,7 @@
<Compile Include="SelectorTests_OfType.cs" /> <Compile Include="SelectorTests_OfType.cs" />
<Compile Include="SelectorTests_Template.cs" /> <Compile Include="SelectorTests_Template.cs" />
<Compile Include="StyleActivatorTests.cs" /> <Compile Include="StyleActivatorTests.cs" />
<Compile Include="StyleBindingTests.cs" /> <Compile Include="ActivatedObservableTests.cs" />
<Compile Include="SetterTests.cs" /> <Compile Include="SetterTests.cs" />
<Compile Include="StyleTests.cs" /> <Compile Include="StyleTests.cs" />
<Compile Include="TestControlBase.cs" /> <Compile Include="TestControlBase.cs" />

79
tests/Perspex.Styling.UnitTests/StyleBindingTests.cs

@ -1,79 +0,0 @@
// 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.Reactive.Linq;
using System.Reactive.Subjects;
using Xunit;
namespace Perspex.Styling.UnitTests
{
public class StyleBindingTests
{
[Fact]
public async void Should_Produce_UnsetValue_On_Activator_False()
{
var activator = new BehaviorSubject<bool>(false);
var target = new StyleBinding(activator, 1, string.Empty);
var result = await target.Take(1);
Assert.Equal(PerspexProperty.UnsetValue, result);
}
[Fact]
public async void Should_Produce_Value_On_Activator_True()
{
var activator = new BehaviorSubject<bool>(true);
var target = new StyleBinding(activator, 1, string.Empty);
var result = await target.Take(1);
Assert.Equal(1, result);
}
[Fact]
public void Should_Change_Value_On_Activator_Change()
{
var activator = new BehaviorSubject<bool>(false);
var target = new StyleBinding(activator, 1, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
activator.OnNext(false);
Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, PerspexProperty.UnsetValue }, result);
}
[Fact]
public void Should_Change_Value_With_Source_Observable()
{
var activator = new BehaviorSubject<bool>(false);
var source = new BehaviorSubject<object>(1);
var target = new StyleBinding(activator, source, string.Empty);
var result = new List<object>();
target.Subscribe(x => result.Add(x));
activator.OnNext(true);
source.OnNext(2);
activator.OnNext(false);
Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, 2, PerspexProperty.UnsetValue }, result);
}
[Fact]
public void Should_Complete_When_Activator_Completes()
{
var activator = new BehaviorSubject<bool>(false);
var target = new StyleBinding(activator, 1, string.Empty);
var completed = false;
target.Subscribe(_ => { }, () => completed = true);
activator.OnCompleted();
Assert.True(completed);
}
}
}
Loading…
Cancel
Save