diff --git a/Perspex.UnitTests/Perspex.UnitTests.csproj b/Perspex.UnitTests/Perspex.UnitTests.csproj index 8786b08e30..854fcfd097 100644 --- a/Perspex.UnitTests/Perspex.UnitTests.csproj +++ b/Perspex.UnitTests/Perspex.UnitTests.csproj @@ -72,6 +72,7 @@ + @@ -82,6 +83,8 @@ + + diff --git a/Perspex.UnitTests/Styling/ActivatorTests.cs b/Perspex.UnitTests/Styling/ActivatorTests.cs new file mode 100644 index 0000000000..e21abe7500 --- /dev/null +++ b/Perspex.UnitTests/Styling/ActivatorTests.cs @@ -0,0 +1,200 @@ +namespace Perspex.UnitTests.Styling +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reactive.Linq; + using System.Reactive.Subjects; + using System.Text; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Perspex.Styling; + using Activator = Perspex.Styling.Activator; + + [TestClass] + public class ActivatorTests + { + [TestMethod] + public void Activator_And_Should_Follow_Single_Input() + { + var inputs = new[] { new TestSubject(false) }; + var target = new Activator(inputs, ActivatorMode.And); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + Assert.IsTrue(result.GetValue()); + inputs[0].OnNext(false); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + Assert.IsTrue(result.GetValue()); + + Assert.AreEqual(1, inputs[0].SubscriberCount); + } + + [TestMethod] + public void Activator_And_Should_AND_Multiple_Inputs() + { + var inputs = new[] + { + new TestSubject(false), + new TestSubject(false), + new TestSubject(true), + }; + var target = new Activator(inputs, ActivatorMode.And); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + inputs[1].OnNext(true); + Assert.IsTrue(result.GetValue()); + inputs[0].OnNext(false); + Assert.IsFalse(result.GetValue()); + + Assert.AreEqual(1, inputs[0].SubscriberCount); + Assert.AreEqual(1, inputs[1].SubscriberCount); + Assert.AreEqual(1, inputs[2].SubscriberCount); + } + + [TestMethod] + public void Activator_And_Should_Unsubscribe_All_When_Input_Completes_On_False() + { + var inputs = new[] + { + new TestSubject(false), + new TestSubject(false), + new TestSubject(true), + }; + var target = new Activator(inputs, ActivatorMode.And); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + inputs[1].OnNext(true); + Assert.IsTrue(result.GetValue()); + inputs[0].OnNext(false); + Assert.IsFalse(result.GetValue()); + inputs[0].OnCompleted(); + + Assert.AreEqual(0, inputs[0].SubscriberCount); + Assert.AreEqual(0, inputs[1].SubscriberCount); + Assert.AreEqual(0, inputs[2].SubscriberCount); + } + + [TestMethod] + public void Activator_And_Should_Not_Unsubscribe_All_When_Input_Completes_On_True() + { + var inputs = new[] + { + new TestSubject(false), + new TestSubject(false), + new TestSubject(true), + }; + var target = new Activator(inputs, ActivatorMode.And); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + inputs[0].OnCompleted(); + + Assert.AreEqual(1, inputs[0].SubscriberCount); + Assert.AreEqual(1, inputs[1].SubscriberCount); + Assert.AreEqual(1, inputs[2].SubscriberCount); + } + + [TestMethod] + public void Activator_Or_Should_Follow_Single_Input() + { + var inputs = new[] { new TestSubject(false) }; + var target = new Activator(inputs, ActivatorMode.Or); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + Assert.IsTrue(result.GetValue()); + inputs[0].OnNext(false); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + Assert.IsTrue(result.GetValue()); + + Assert.AreEqual(1, inputs[0].SubscriberCount); + } + + [TestMethod] + public void Activator_Or_Should_OR_Multiple_Inputs() + { + var inputs = new[] + { + new TestSubject(false), + new TestSubject(false), + new TestSubject(true), + }; + var target = new Activator(inputs, ActivatorMode.Or); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsTrue(result.GetValue()); + inputs[2].OnNext(false); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + Assert.IsTrue(result.GetValue()); + + Assert.AreEqual(1, inputs[0].SubscriberCount); + Assert.AreEqual(1, inputs[1].SubscriberCount); + Assert.AreEqual(1, inputs[2].SubscriberCount); + } + + [TestMethod] + public void Activator_Or_Should_Unsubscribe_All_When_Input_Completes_On_True() + { + var inputs = new[] + { + new TestSubject(false), + new TestSubject(false), + new TestSubject(true), + }; + var target = new Activator(inputs, ActivatorMode.Or); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsTrue(result.GetValue()); + inputs[2].OnNext(false); + Assert.IsFalse(result.GetValue()); + inputs[0].OnNext(true); + Assert.IsTrue(result.GetValue()); + inputs[0].OnCompleted(); + + Assert.AreEqual(0, inputs[0].SubscriberCount); + Assert.AreEqual(0, inputs[1].SubscriberCount); + Assert.AreEqual(0, inputs[2].SubscriberCount); + } + + [TestMethod] + public void Activator_Or_Should_Not_Unsubscribe_All_When_Input_Completes_On_False() + { + var inputs = new[] + { + new TestSubject(false), + new TestSubject(false), + new TestSubject(true), + }; + var target = new Activator(inputs, ActivatorMode.Or); + var result = new TestObserver(); + + target.Subscribe(result); + Assert.IsTrue(result.GetValue()); + inputs[2].OnNext(false); + Assert.IsFalse(result.GetValue()); + inputs[2].OnCompleted(); + + Assert.AreEqual(1, inputs[0].SubscriberCount); + Assert.AreEqual(1, inputs[1].SubscriberCount); + Assert.AreEqual(1, inputs[2].SubscriberCount); + } + } +} diff --git a/Perspex.UnitTests/TestObserver.cs b/Perspex.UnitTests/TestObserver.cs new file mode 100644 index 0000000000..aeee1d4cac --- /dev/null +++ b/Perspex.UnitTests/TestObserver.cs @@ -0,0 +1,65 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.UnitTests +{ + using System; + + internal class TestObserver : IObserver + { + private bool hasValue; + + private T value; + + public bool Completed { get; private set; } + + public Exception Error { get; private set; } + + public T GetValue() + { + if (!this.hasValue) + { + throw new Exception("Observable provided no value."); + } + + if (this.Completed) + { + throw new Exception("Observable completed unexpectedly."); + } + + if (this.Error != null) + { + throw new Exception("Observable errored unexpectedly."); + } + + this.hasValue = false; + return value; + } + + public void OnCompleted() + { + this.Completed = true; + } + + public void OnError(Exception error) + { + this.Error = error; + } + + public void OnNext(T value) + { + if (!this.hasValue) + { + this.value = value; + this.hasValue = true; + } + else + { + throw new Exception("Observable pushed more than one value."); + } + } + } +} diff --git a/Perspex.UnitTests/TestSubject.cs b/Perspex.UnitTests/TestSubject.cs new file mode 100644 index 0000000000..0a99a5df93 --- /dev/null +++ b/Perspex.UnitTests/TestSubject.cs @@ -0,0 +1,60 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.UnitTests +{ + using System; + using System.Collections.Generic; + using System.Reactive.Disposables; + + internal class TestSubject : IObserver, IObservable + { + private T initial; + + private List> subscribers = new List>(); + + public TestSubject(T initial) + { + this.initial = initial; + } + + public int SubscriberCount + { + get { return this.subscribers.Count; } + } + + public void OnCompleted() + { + foreach (IObserver subscriber in this.subscribers.ToArray()) + { + subscriber.OnCompleted(); + } + } + + public void OnError(Exception error) + { + foreach (IObserver subscriber in this.subscribers.ToArray()) + { + subscriber.OnError(error); + } + } + + public void OnNext(T value) + { + foreach (IObserver subscriber in this.subscribers.ToArray()) + { + subscriber.OnNext(value); + } + } + + public IDisposable Subscribe(IObserver observer) + { + this.subscribers.Add(observer); + observer.OnNext(initial); + return Disposable.Create(() => this.subscribers.Remove(observer)); + } + } +} diff --git a/Perspex/Styling/Activator.cs b/Perspex/Styling/Activator.cs index 860e682a68..5b4e5b88c5 100644 --- a/Perspex/Styling/Activator.cs +++ b/Perspex/Styling/Activator.cs @@ -11,8 +11,16 @@ namespace Perspex.Styling using System.Linq; using System.Reactive.Disposables; + public enum ActivatorMode + { + And, + Or, + } + public class Activator : IObservable { + ActivatorMode mode; + List values = new List(); List subscriptions = new List(); @@ -21,27 +29,24 @@ namespace Perspex.Styling bool last = false; - public Activator(Selector match, IStyleable control) + public Activator(IEnumerable> inputs, ActivatorMode mode = ActivatorMode.And) { int i = 0; - while (match != null) + this.mode = mode; + + foreach (IObservable input in inputs) { int iCaptured = i; - if (match.Observable != null) - { - this.values.Add(false); - - IDisposable subscription = match.Observable(control).Subscribe( - x => this.Update(iCaptured, x), - x => this.Finish(iCaptured), - () => this.Finish(iCaptured)); - this.subscriptions.Add(subscription); - ++i; - } + this.values.Add(false); - match = match.Previous; + IDisposable subscription = input.Subscribe( + x => this.Update(iCaptured, x), + x => this.Finish(iCaptured), + () => this.Finish(iCaptured)); + this.subscriptions.Add(subscription); + ++i; } } @@ -58,7 +63,19 @@ namespace Perspex.Styling { this.values[index] = value; - bool current = this.values.All(x => x); + bool current; + + switch (this.mode) + { + case ActivatorMode.And: + current = this.values.All(x => x); + break; + case ActivatorMode.Or: + current = this.values.Any(x => x); + break; + default: + throw new InvalidOperationException("Invalid Activator mode."); + } if (current != last) { @@ -69,10 +86,13 @@ namespace Perspex.Styling private void Finish(int i) { - if (!this.values[i]) + // If the observable has finished on 'false' and we're in And mode then it will never + // go back to true so we can unsubscribe from all the other subscriptions now. + // Similarly in Or mode; if the completed value is true then we're done. + bool unsubscribe = this.mode == ActivatorMode.And ? !this.values[i] : this.values[i]; + + if (unsubscribe) { - // If the observable has finished on 'false' then it will never go back to true - // so we can unsubscribe from all the other subscriptions now. foreach (IDisposable subscription in this.subscriptions) { subscription.Dispose(); diff --git a/Perspex/Styling/Selector.cs b/Perspex/Styling/Selector.cs index 8d1e8d2c04..0ab8095d4a 100644 --- a/Perspex/Styling/Selector.cs +++ b/Perspex/Styling/Selector.cs @@ -53,7 +53,20 @@ namespace Perspex.Styling public Activator GetActivator(IStyleable control) { - return new Activator(this, control); + List> inputs = new List>(); + Selector selector = this; + + while (selector != null) + { + if (selector.Observable != null) + { + inputs.Add(selector.Observable(control)); + } + + selector = selector.Previous; + } + + return new Activator(inputs); } public override string ToString()