diff --git a/Perspex.UnitTests/Perspex.UnitTests.csproj b/Perspex.UnitTests/Perspex.UnitTests.csproj index c462e9c134..1e9eec4ba5 100644 --- a/Perspex.UnitTests/Perspex.UnitTests.csproj +++ b/Perspex.UnitTests/Perspex.UnitTests.csproj @@ -76,7 +76,7 @@ - + diff --git a/Perspex.UnitTests/Styling/ActivatorTests.cs b/Perspex.UnitTests/Styling/StyleActivatorTests.cs similarity index 85% rename from Perspex.UnitTests/Styling/ActivatorTests.cs rename to Perspex.UnitTests/Styling/StyleActivatorTests.cs index 14e9ab0c94..79e8eb8c73 100644 --- a/Perspex.UnitTests/Styling/ActivatorTests.cs +++ b/Perspex.UnitTests/Styling/StyleActivatorTests.cs @@ -1,4 +1,5 @@ -namespace Perspex.UnitTests.Styling +using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace Perspex.UnitTests.Styling { using System; using System.Collections.Generic; @@ -9,16 +10,15 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Perspex.Styling; - using Activator = Perspex.Styling.StyleActivator; [TestClass] - public class ActivatorTests + public class StyleActivatorTests { [TestMethod] public void Activator_And_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new Activator(inputs, ActivatorMode.And); + var target = new StyleActivator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -42,7 +42,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, ActivatorMode.And); + var target = new StyleActivator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -67,7 +67,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, ActivatorMode.And); + var target = new StyleActivator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -93,7 +93,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, ActivatorMode.And); + var target = new StyleActivator(inputs, ActivatorMode.And); var result = new TestObserver(); target.Subscribe(result); @@ -110,7 +110,7 @@ public void Activator_Or_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new Activator(inputs, ActivatorMode.Or); + var target = new StyleActivator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); @@ -134,7 +134,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, ActivatorMode.Or); + var target = new StyleActivator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); @@ -158,7 +158,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, ActivatorMode.Or); + var target = new StyleActivator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); @@ -183,7 +183,7 @@ new TestSubject(false), new TestSubject(true), }; - var target = new Activator(inputs, ActivatorMode.Or); + var target = new StyleActivator(inputs, ActivatorMode.Or); var result = new TestObserver(); target.Subscribe(result); @@ -196,5 +196,21 @@ Assert.AreEqual(1, inputs[1].SubscriberCount); Assert.AreEqual(1, inputs[2].SubscriberCount); } + + [TestMethod] + public void Completed_Activator_Should_Signal_OnCompleted() + { + var inputs = new[] + { + Observable.Return(false), + }; + + var target = new StyleActivator(inputs, ActivatorMode.Or); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + + Assert.IsTrue(completed); + } } } diff --git a/Perspex/Styling/Style.cs b/Perspex/Styling/Style.cs index f8b9fc1546..6c8999f646 100644 --- a/Perspex/Styling/Style.cs +++ b/Perspex/Styling/Style.cs @@ -43,11 +43,21 @@ namespace Perspex.Styling string description = "Style " + this.Selector.ToString(); StyleActivator activator = this.Selector.GetActivator(control); - if (!(activator.CurrentValue == false && activator.HasCompleted)) + if (activator.CurrentValue || !activator.HasCompleted) { + IObservable observable = activator; + + // If the activator has completed, then we want its value to be true forever. + // Because of this we can't pass the activator directly as it will complete + // immediately and remove the binding. + if (activator.HasCompleted) + { + observable = Observable.Never().StartWith(true); + } + foreach (Setter setter in this.Setters) { - StyleBinding binding = new StyleBinding(activator, setter.Value, description); + StyleBinding binding = new StyleBinding(observable, setter.Value, description); control.Bind(setter.Property, binding, this.Selector.Priority); } } diff --git a/Perspex/Styling/StyleActivator.cs b/Perspex/Styling/StyleActivator.cs index faa55d87d8..9bb406392a 100644 --- a/Perspex/Styling/StyleActivator.cs +++ b/Perspex/Styling/StyleActivator.cs @@ -21,26 +21,25 @@ namespace Perspex.Styling { private ActivatorMode mode; - private List values = new List(); + private bool[] values; private List subscriptions = new List(); private List> observers = new List>(); public StyleActivator( - IEnumerable> inputs, + IList> inputs, ActivatorMode mode = ActivatorMode.And) { int i = 0; this.mode = mode; + this.values = new bool[inputs.Count]; foreach (IObservable input in inputs) { int capturedIndex = i; - this.values.Add(false); - IDisposable subscription = input.Subscribe( x => this.Update(capturedIndex, x), x => this.Finish(capturedIndex), @@ -79,9 +78,18 @@ namespace Perspex.Styling { Contract.Requires(observer != null); - this.observers.Add(observer); observer.OnNext(this.CurrentValue); - return Disposable.Create(() => this.observers.Remove(observer)); + + if (this.HasCompleted) + { + observer.OnCompleted(); + return Disposable.Empty; + } + else + { + this.observers.Add(observer); + return Disposable.Create(() => this.observers.Remove(observer)); + } } private void Update(int index, bool value) @@ -111,10 +119,14 @@ namespace Perspex.Styling private void Finish(int 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]; + // We can unsubscribe from everything if the completed observable: + // - Is the only subscription. + // - Has finished on 'false' and we're in And mode + // - Has finished on 'true' and we're in Or mode + var value = this.values[i]; + var unsubscribe = + (this.values.Length == 1) || + (this.mode == ActivatorMode.And ? !value : value); if (unsubscribe) { diff --git a/Perspex/Styling/StyleBinding.cs b/Perspex/Styling/StyleBinding.cs index 365588b8de..622f667017 100644 --- a/Perspex/Styling/StyleBinding.cs +++ b/Perspex/Styling/StyleBinding.cs @@ -15,15 +15,14 @@ namespace Perspex.Styling /// /// This class takes an activator and a value. The activator is an observable which produces /// a bool. When the activator produces true, this observable will produce . - /// When the activator produces false (and before the activator returns a value) it will - /// produce . + /// When the activator produces false it will produce . /// internal class StyleBinding : IObservable, IObservableDescription { /// - /// The subject that provides the observable implementation. + /// The activator. /// - private BehaviorSubject subject = new BehaviorSubject(PerspexProperty.UnsetValue); + private IObservable activator; /// /// Initializes a new instance of the class. @@ -36,13 +35,9 @@ namespace Perspex.Styling object activatedValue, string description) { + this.activator = activator; this.ActivatedValue = activatedValue; this.Description = description; - - activator.Subscribe( - active => this.subject.OnNext(active ? this.ActivatedValue : PerspexProperty.UnsetValue), - error => this.subject.OnError(error), - () => this.subject.OnCompleted()); } /// @@ -71,7 +66,10 @@ namespace Perspex.Styling public IDisposable Subscribe(IObserver observer) { Contract.Requires(observer != null); - return this.subject.Subscribe(observer); + return this.activator.Subscribe( + active => observer.OnNext(active ? this.ActivatedValue : PerspexProperty.UnsetValue), + observer.OnError, + observer.OnCompleted); } } } diff --git a/Perspex/Styling/Styler.cs b/Perspex/Styling/Styler.cs index e56bc9e66c..1d1932f486 100644 --- a/Perspex/Styling/Styler.cs +++ b/Perspex/Styling/Styler.cs @@ -18,7 +18,12 @@ namespace Perspex.Styling { IVisual visual = control as IVisual; IStyled styleContainer = visual.GetVisualAncestorOrSelf(); - Application.Current.Styles.Attach(control); + + if (Application.Current != null) + { + Application.Current.Styles.Attach(control); + } + this.ApplyStyles(control, styleContainer); }