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);
- }
- }
-}