diff --git a/src/Perspex.Base/PerspexObjectExtensions.cs b/src/Perspex.Base/PerspexObjectExtensions.cs index d7b2d5a748..c958ecbffc 100644 --- a/src/Perspex.Base/PerspexObjectExtensions.cs +++ b/src/Perspex.Base/PerspexObjectExtensions.cs @@ -103,6 +103,36 @@ namespace Perspex GetDescription(o, property)); } + /// + /// Gets a subject for a . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this IPerspexObject o, + PerspexProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + // TODO: Subject.Create is not yet in stable Rx : once it is, remove the + // AnonymousSubject classes from this file and use Subject.Create. + var output = new Subject(); + var result = new AnonymousSubject( + Observer.Create( + x => output.OnNext(x), + e => output.OnError(e), + () => output.OnCompleted()), + o.GetObservable(property)); + o.Bind(property, output, priority); + return result; + } + /// /// Gets a subject for a . /// diff --git a/src/Perspex.Styling/Perspex.Styling.csproj b/src/Perspex.Styling/Perspex.Styling.csproj index 5f79ca5820..df3c17278d 100644 --- a/src/Perspex.Styling/Perspex.Styling.csproj +++ b/src/Perspex.Styling/Perspex.Styling.csproj @@ -45,6 +45,8 @@ + + @@ -59,7 +61,7 @@ - + diff --git a/src/Perspex.Styling/Styling/ActivatedObservable.cs b/src/Perspex.Styling/Styling/ActivatedObservable.cs new file mode 100644 index 0000000000..ed313fa3a6 --- /dev/null +++ b/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 +{ + /// + /// An observable which is switched on or off according to an activator observable. + /// + /// + /// An has two inputs: an activator observable a + /// observable which produces the activated value. When the activator + /// produces true, the will produce the current activated + /// value. When the activator produces false it will produce + /// . + /// + internal class ActivatedObservable : ObservableBase, IDescription + { + /// + /// The activator. + /// + private readonly IObservable _activator; + + /// + /// Initializes a new instance of the class. + /// + /// The activator. + /// An observable that produces the activated value. + /// The binding description. + public ActivatedObservable( + IObservable activator, + IObservable source, + string description) + { + _activator = activator; + Description = description; + Source = source; + } + + /// + /// Gets a description of the binding. + /// + public string Description + { + get; + } + + /// + /// Gets an observable which produces the . + /// + public IObservable Source + { + get; + } + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The observer. + /// IDisposable object used to unsubscribe from the observable sequence. + protected override IDisposable SubscribeCore(IObserver observer) + { + Contract.Requires(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); + } + } +} diff --git a/src/Perspex.Styling/Styling/ActivatedSubject.cs b/src/Perspex.Styling/Styling/ActivatedSubject.cs new file mode 100644 index 0000000000..d99ffae105 --- /dev/null +++ b/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 +{ + /// + /// A subject which is switched on or off according to an activator observable. + /// + /// + /// An has two inputs: an activator observable and either an + /// or a observable which produces the + /// activated value. When the activator produces true, the will + /// produce the current activated value. When the activator produces false it will produce + /// . + /// + internal class ActivatedSubject : ISubject, IDescription + { + private IObservable _activator; + private bool _active; + private object _pushValue; + + /// + /// Initializes a new instance of the class. + /// + /// The activator. + /// An observable that produces the activated value. + /// The binding description. + public ActivatedSubject( + IObservable activator, + ISubject source, + string description) + { + _activator = activator; + Description = description; + Source = source; + + _activator.Skip(1).Subscribe(ActivatorChanged); + } + + /// + /// Gets a description of the binding. + /// + public string Description + { + get; + } + + /// + /// Gets the underlying subject. + /// + public ISubject Source + { + get; + } + + /// + /// Notifies all subscribed observers about the end of the sequence. + /// + public void OnCompleted() + { + if (_active) + { + Source.OnCompleted(); + } + } + + /// + /// Notifies all subscribed observers with the exception. + /// + /// The exception to send to all subscribed observers. + /// is null. + public void OnError(Exception error) + { + if (_active) + { + Source.OnError(error); + } + } + + /// + /// Notifies all subscribed observers with the value. + /// + /// The value to send to all subscribed observers. + public void OnNext(object value) + { + _pushValue = value; + + if (_active) + { + Source.OnNext(value); + } + } + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The observer. + /// IDisposable object used to unsubscribe from the observable sequence. + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(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); + } + } +} diff --git a/src/Perspex.Styling/Styling/ActivatedValue.cs b/src/Perspex.Styling/Styling/ActivatedValue.cs new file mode 100644 index 0000000000..0c02598086 --- /dev/null +++ b/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 +{ + /// + /// An value which is switched on or off according to an activator observable. + /// + /// + /// An has two inputs: an activator observable and an + /// . When the activator produces true, the + /// will produce the current value. When the activator + /// produces false it will produce . + /// + internal class ActivatedValue : ObservableBase, IDescription + { + /// + /// The activator. + /// + private readonly IObservable _activator; + + /// + /// Initializes a new instance of the class. + /// + /// The activator. + /// The activated value. + /// The binding description. + public ActivatedValue( + IObservable activator, + object value, + string description) + { + _activator = activator; + Value = value; + Description = description; + } + + /// + /// Gets the activated value. + /// + public object Value + { + get; + } + + /// + /// Gets a description of the binding. + /// + public string Description + { + get; + } + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The observer. + /// IDisposable object used to unsubscribe from the observable sequence. + protected override IDisposable SubscribeCore(IObserver observer) + { + Contract.Requires(observer != null); + + return _activator + .Select(active => active ? Value : PerspexProperty.UnsetValue) + .Subscribe(observer); + } + } +} diff --git a/src/Perspex.Styling/Styling/Setter.cs b/src/Perspex.Styling/Styling/Setter.cs index e93729cf6e..103ebb6548 100644 --- a/src/Perspex.Styling/Styling/Setter.cs +++ b/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. using System; +using System.Reactive.Subjects; using Perspex.Data; using Perspex.Metadata; @@ -77,8 +78,9 @@ namespace Perspex.Styling } else { - throw new NotSupportedException( - "Setter bindings with activators not yet supported."); + var subject = binding.CreateSubject(control, Property); + var activated = new ActivatedSubject(activator, subject, style.ToString()); + Bind(control, Property, binding, activated); } } else @@ -89,13 +91,22 @@ namespace Perspex.Styling } else { - var activated = new StyleBinding(activator, Value, style.ToString()); + var activated = new ActivatedValue(activator, Value, style.ToString()); control.Bind(Property, activated, BindingPriority.StyleTrigger); } } } 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 subject) { var mode = binding.Mode; @@ -106,7 +117,7 @@ namespace Perspex.Styling control.Bind( property, - binding.CreateSubject(control, property), + subject, mode, binding.Priority); } diff --git a/src/Perspex.Styling/Styling/StyleBinding.cs b/src/Perspex.Styling/Styling/StyleBinding.cs deleted file mode 100644 index b2ddbeee1b..0000000000 --- a/src/Perspex.Styling/Styling/StyleBinding.cs +++ /dev/null @@ -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 -{ - /// - /// Provides an observable for a style. - /// - /// - /// A has two inputs: an activator observable and either an - /// or a observable which produces the - /// activated value. When the activator produces true, the will - /// produce the current activated value. When the activator produces false it will produce - /// . - /// - internal class StyleBinding : ObservableBase, IDescription - { - /// - /// The activator. - /// - private readonly IObservable _activator; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// The activated value. - /// The binding description. - public StyleBinding( - IObservable activator, - object activatedValue, - string description) - { - _activator = activator; - ActivatedValue = activatedValue; - Description = description; - } - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public StyleBinding( - IObservable activator, - IObservable source, - string description) - { - _activator = activator; - Description = description; - Source = source; - } - - /// - /// Gets the activated value. - /// - public object ActivatedValue - { - get; - } - - /// - /// Gets a description of the binding. - /// - public string Description - { - get; - } - - /// - /// Gets an observable which produces the . - /// - public IObservable Source - { - get; - } - - /// - /// Notifies the provider that an observer is to receive notifications. - /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver observer) - { - Contract.Requires(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); - } - } - } -} diff --git a/tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs b/tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs new file mode 100644 index 0000000000..e1beb992c2 --- /dev/null +++ b/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(false); + var source = new BehaviorSubject(1); + var target = new ActivatedObservable(activator, source, string.Empty); + var result = new List(); + + 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(false); + var source = new BehaviorSubject(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(false); + var source = new BehaviorSubject(1); + var target = new ActivatedObservable(activator, source, string.Empty); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + activator.OnCompleted(); + + Assert.True(completed); + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs new file mode 100644 index 0000000000..d4654dd199 --- /dev/null +++ b/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(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 FooProperty = + PerspexProperty.Register("Foo", "foodefault"); + + public string Foo + { + get { return GetValue(FooProperty); } + set { SetValue(FooProperty, value); } + } + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs b/tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs new file mode 100644 index 0000000000..4921aeaa7b --- /dev/null +++ b/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(false); + var target = new ActivatedValue(activator, 1, string.Empty); + var result = new List(); + + 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(false); + var target = new ActivatedValue(activator, 1, string.Empty); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + activator.OnCompleted(); + + Assert.True(completed); + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj index b9e82d589d..af640ebdf4 100644 --- a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj +++ b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj @@ -80,6 +80,8 @@ + + @@ -90,7 +92,7 @@ - + diff --git a/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs b/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs deleted file mode 100644 index 82a7f66d07..0000000000 --- a/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs +++ /dev/null @@ -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(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(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(false); - var target = new StyleBinding(activator, 1, string.Empty); - var result = new List(); - - 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(false); - var source = new BehaviorSubject(1); - var target = new StyleBinding(activator, source, string.Empty); - var result = new List(); - - 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(false); - var target = new StyleBinding(activator, 1, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - activator.OnCompleted(); - - Assert.True(completed); - } - } -}