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()