Browse Source

Fixed a binding bug.

StyleActivator wasn't signalling OnCompleted when it was subscribed in a
completed state, causing styles to be applied to controls that would
never activate.
pull/4/head
Steven Kirk 12 years ago
parent
commit
4fb39c6d9a
  1. 2
      Perspex.UnitTests/Perspex.UnitTests.csproj
  2. 38
      Perspex.UnitTests/Styling/StyleActivatorTests.cs
  3. 14
      Perspex/Styling/Style.cs
  4. 32
      Perspex/Styling/StyleActivator.cs
  5. 18
      Perspex/Styling/StyleBinding.cs
  6. 7
      Perspex/Styling/Styler.cs

2
Perspex.UnitTests/Perspex.UnitTests.csproj

@ -76,7 +76,7 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="PerspexObjectTests.cs" /> <Compile Include="PerspexObjectTests.cs" />
<Compile Include="StyleTests.cs" /> <Compile Include="StyleTests.cs" />
<Compile Include="Styling\ActivatorTests.cs" /> <Compile Include="Styling\StyleActivatorTests.cs" />
<Compile Include="Styling\SelectorTests_Multiple.cs" /> <Compile Include="Styling\SelectorTests_Multiple.cs" />
<Compile Include="Styling\SelectorTests_Class.cs" /> <Compile Include="Styling\SelectorTests_Class.cs" />
<Compile Include="Styling\SelectorTests_Id.cs" /> <Compile Include="Styling\SelectorTests_Id.cs" />

38
Perspex.UnitTests/Styling/ActivatorTests.cs → 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;
using System.Collections.Generic; using System.Collections.Generic;
@ -9,16 +10,15 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using Perspex.Styling; using Perspex.Styling;
using Activator = Perspex.Styling.StyleActivator;
[TestClass] [TestClass]
public class ActivatorTests public class StyleActivatorTests
{ {
[TestMethod] [TestMethod]
public void Activator_And_Should_Follow_Single_Input() public void Activator_And_Should_Follow_Single_Input()
{ {
var inputs = new[] { new TestSubject<bool>(false) }; var inputs = new[] { new TestSubject<bool>(false) };
var target = new Activator(inputs, ActivatorMode.And); var target = new StyleActivator(inputs, ActivatorMode.And);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -42,7 +42,7 @@
new TestSubject<bool>(false), new TestSubject<bool>(false),
new TestSubject<bool>(true), new TestSubject<bool>(true),
}; };
var target = new Activator(inputs, ActivatorMode.And); var target = new StyleActivator(inputs, ActivatorMode.And);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -67,7 +67,7 @@
new TestSubject<bool>(false), new TestSubject<bool>(false),
new TestSubject<bool>(true), new TestSubject<bool>(true),
}; };
var target = new Activator(inputs, ActivatorMode.And); var target = new StyleActivator(inputs, ActivatorMode.And);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -93,7 +93,7 @@
new TestSubject<bool>(false), new TestSubject<bool>(false),
new TestSubject<bool>(true), new TestSubject<bool>(true),
}; };
var target = new Activator(inputs, ActivatorMode.And); var target = new StyleActivator(inputs, ActivatorMode.And);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -110,7 +110,7 @@
public void Activator_Or_Should_Follow_Single_Input() public void Activator_Or_Should_Follow_Single_Input()
{ {
var inputs = new[] { new TestSubject<bool>(false) }; var inputs = new[] { new TestSubject<bool>(false) };
var target = new Activator(inputs, ActivatorMode.Or); var target = new StyleActivator(inputs, ActivatorMode.Or);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -134,7 +134,7 @@
new TestSubject<bool>(false), new TestSubject<bool>(false),
new TestSubject<bool>(true), new TestSubject<bool>(true),
}; };
var target = new Activator(inputs, ActivatorMode.Or); var target = new StyleActivator(inputs, ActivatorMode.Or);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -158,7 +158,7 @@
new TestSubject<bool>(false), new TestSubject<bool>(false),
new TestSubject<bool>(true), new TestSubject<bool>(true),
}; };
var target = new Activator(inputs, ActivatorMode.Or); var target = new StyleActivator(inputs, ActivatorMode.Or);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -183,7 +183,7 @@
new TestSubject<bool>(false), new TestSubject<bool>(false),
new TestSubject<bool>(true), new TestSubject<bool>(true),
}; };
var target = new Activator(inputs, ActivatorMode.Or); var target = new StyleActivator(inputs, ActivatorMode.Or);
var result = new TestObserver<bool>(); var result = new TestObserver<bool>();
target.Subscribe(result); target.Subscribe(result);
@ -196,5 +196,21 @@
Assert.AreEqual(1, inputs[1].SubscriberCount); Assert.AreEqual(1, inputs[1].SubscriberCount);
Assert.AreEqual(1, inputs[2].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);
}
} }
} }

14
Perspex/Styling/Style.cs

@ -43,11 +43,21 @@ namespace Perspex.Styling
string description = "Style " + this.Selector.ToString(); string description = "Style " + this.Selector.ToString();
StyleActivator activator = this.Selector.GetActivator(control); StyleActivator activator = this.Selector.GetActivator(control);
if (!(activator.CurrentValue == false && activator.HasCompleted)) if (activator.CurrentValue || !activator.HasCompleted)
{ {
IObservable<bool> 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<bool>().StartWith(true);
}
foreach (Setter setter in this.Setters) 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); control.Bind(setter.Property, binding, this.Selector.Priority);
} }
} }

32
Perspex/Styling/StyleActivator.cs

@ -21,26 +21,25 @@ namespace Perspex.Styling
{ {
private ActivatorMode mode; private ActivatorMode mode;
private List<bool> values = new List<bool>(); private bool[] values;
private List<IDisposable> subscriptions = new List<IDisposable>(); private List<IDisposable> subscriptions = new List<IDisposable>();
private List<IObserver<bool>> observers = new List<IObserver<bool>>(); private List<IObserver<bool>> observers = new List<IObserver<bool>>();
public StyleActivator( public StyleActivator(
IEnumerable<IObservable<bool>> inputs, IList<IObservable<bool>> inputs,
ActivatorMode mode = ActivatorMode.And) ActivatorMode mode = ActivatorMode.And)
{ {
int i = 0; int i = 0;
this.mode = mode; this.mode = mode;
this.values = new bool[inputs.Count];
foreach (IObservable<bool> input in inputs) foreach (IObservable<bool> input in inputs)
{ {
int capturedIndex = i; int capturedIndex = i;
this.values.Add(false);
IDisposable subscription = input.Subscribe( IDisposable subscription = input.Subscribe(
x => this.Update(capturedIndex, x), x => this.Update(capturedIndex, x),
x => this.Finish(capturedIndex), x => this.Finish(capturedIndex),
@ -79,9 +78,18 @@ namespace Perspex.Styling
{ {
Contract.Requires<ArgumentNullException>(observer != null); Contract.Requires<ArgumentNullException>(observer != null);
this.observers.Add(observer);
observer.OnNext(this.CurrentValue); 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) private void Update(int index, bool value)
@ -111,10 +119,14 @@ namespace Perspex.Styling
private void Finish(int i) private void Finish(int i)
{ {
// If the observable has finished on 'false' and we're in And mode then it will never // We can unsubscribe from everything if the completed observable:
// go back to true so we can unsubscribe from all the other subscriptions now. // - Is the only subscription.
// Similarly in Or mode; if the completed value is true then we're done. // - Has finished on 'false' and we're in And mode
bool unsubscribe = this.mode == ActivatorMode.And ? !this.values[i] : this.values[i]; // - 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) if (unsubscribe)
{ {

18
Perspex/Styling/StyleBinding.cs

@ -15,15 +15,14 @@ namespace Perspex.Styling
/// <remarks> /// <remarks>
/// This class takes an activator and a value. The activator is an observable which produces /// 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 <see cref="Value"/>. /// a bool. When the activator produces true, this observable will produce <see cref="Value"/>.
/// When the activator produces false (and before the activator returns a value) it will /// When the activator produces false it will produce <see cref="PerspexProperty.UnsetValue"/>.
/// produce <see cref="PerspexProperty.UnsetValue"/>.
/// </remarks> /// </remarks>
internal class StyleBinding : IObservable<object>, IObservableDescription internal class StyleBinding : IObservable<object>, IObservableDescription
{ {
/// <summary> /// <summary>
/// The subject that provides the observable implementation. /// The activator.
/// </summary> /// </summary>
private BehaviorSubject<object> subject = new BehaviorSubject<object>(PerspexProperty.UnsetValue); private IObservable<bool> activator;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StyleBinding"/> class. /// Initializes a new instance of the <see cref="StyleBinding"/> class.
@ -36,13 +35,9 @@ namespace Perspex.Styling
object activatedValue, object activatedValue,
string description) string description)
{ {
this.activator = activator;
this.ActivatedValue = activatedValue; this.ActivatedValue = activatedValue;
this.Description = description; this.Description = description;
activator.Subscribe(
active => this.subject.OnNext(active ? this.ActivatedValue : PerspexProperty.UnsetValue),
error => this.subject.OnError(error),
() => this.subject.OnCompleted());
} }
/// <summary> /// <summary>
@ -71,7 +66,10 @@ namespace Perspex.Styling
public IDisposable Subscribe(IObserver<object> observer) public IDisposable Subscribe(IObserver<object> observer)
{ {
Contract.Requires<NullReferenceException>(observer != null); Contract.Requires<NullReferenceException>(observer != null);
return this.subject.Subscribe(observer); return this.activator.Subscribe(
active => observer.OnNext(active ? this.ActivatedValue : PerspexProperty.UnsetValue),
observer.OnError,
observer.OnCompleted);
} }
} }
} }

7
Perspex/Styling/Styler.cs

@ -18,7 +18,12 @@ namespace Perspex.Styling
{ {
IVisual visual = control as IVisual; IVisual visual = control as IVisual;
IStyled styleContainer = visual.GetVisualAncestorOrSelf<IStyled>(); IStyled styleContainer = visual.GetVisualAncestorOrSelf<IStyled>();
Application.Current.Styles.Attach(control);
if (Application.Current != null)
{
Application.Current.Styles.Attach(control);
}
this.ApplyStyles(control, styleContainer); this.ApplyStyles(control, styleContainer);
} }

Loading…
Cancel
Save