diff --git a/Perspex.UnitTests/PerspexObjectTests.cs b/Perspex.UnitTests/PerspexObjectTests.cs index c9fb9515a4..93e286a727 100644 --- a/Perspex.UnitTests/PerspexObjectTests.cs +++ b/Perspex.UnitTests/PerspexObjectTests.cs @@ -9,6 +9,7 @@ namespace Perspex.UnitTests using System; using System.Linq; using System.Reactive.Linq; + using System.Reactive.Subjects; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] @@ -317,6 +318,92 @@ namespace Perspex.UnitTests Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); } + [TestMethod] + public void StyleBinding_Overrides_Default_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(true)); + + Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void StyleBinding_Doesnt_Override_Local_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(true)); + + Assert.AreEqual("newvalue", target.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void StyleBinding_Deactivated_Doesnt_Override_Default_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "stylevalue", Observable.Return(false)); + + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void StyleBinding_Toggles_On_Activation() + { + Class1 target = new Class1(); + + Subject source = new Subject(); + target.SetValue(Class1.FooProperty, "stylevalue", source); + + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + source.OnNext(true); + Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); + source.OnNext(false); + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void StyleBinding_Detaches_OnCompleted() + { + Class1 target = new Class1(); + + Subject source = new Subject(); + target.SetValue(Class1.FooProperty, "stylevalue", source); + + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + source.OnNext(true); + Assert.AreEqual("stylevalue", target.GetValue(Class1.FooProperty)); + source.OnCompleted(); + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + } + + [TestMethod] + public void Later_StyleBindings_Have_Precedence() + { + Class1 target = new Class1(); + + Subject source1 = new Subject(); + Subject source2 = new Subject(); + target.SetValue(Class1.FooProperty, "style1", source1); + target.SetValue(Class1.FooProperty, "style2", source2); + + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + source1.OnNext(true); + Assert.AreEqual("style1", target.GetValue(Class1.FooProperty)); + source2.OnNext(true); + Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + source1.OnNext(false); + Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + source2.OnNext(false); + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + source2.OnNext(true); + Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + source1.OnNext(true); + Assert.AreEqual("style2", target.GetValue(Class1.FooProperty)); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = diff --git a/Perspex/IBindingDescription.cs b/Perspex/IBindingDescription.cs deleted file mode 100644 index 3ade895491..0000000000 --- a/Perspex/IBindingDescription.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Perspex -{ - public interface IBindingDescription - { - string Description { get; } - } -} diff --git a/Perspex/Match.cs b/Perspex/Match.cs index ca858ad1aa..1126e41a0d 100644 --- a/Perspex/Match.cs +++ b/Perspex/Match.cs @@ -7,6 +7,9 @@ namespace Perspex { using System; + using System.Collections.Generic; + using System.Linq; + using System.Reactive.Linq; using Perspex.Controls; public class Match @@ -35,6 +38,25 @@ namespace Perspex set; } + public IObservable GetActivator() + { + List> observables = new List>(); + Match match = this; + + do + { + if (match.Observable != null) + { + observables.Add(match.Observable); + } + + match = match.Previous; + } + while (match != null); + + return System.Reactive.Linq.Observable.CombineLatest(observables).Select(x => x.All(b => b)); + } + public override string ToString() { string result = (this.Previous != null) ? this.Previous.ToString() : string.Empty; diff --git a/Perspex/Perspex.csproj b/Perspex/Perspex.csproj index e90afe33b6..720b68273a 100644 --- a/Perspex/Perspex.csproj +++ b/Perspex/Perspex.csproj @@ -78,7 +78,6 @@ - diff --git a/Perspex/PerspexObject.cs b/Perspex/PerspexObject.cs index 84eaf8e289..baa5f14728 100644 --- a/Perspex/PerspexObject.cs +++ b/Perspex/PerspexObject.cs @@ -331,6 +331,32 @@ namespace Perspex return this.values.ContainsKey(property); } + /// + /// Sets a value. + /// + /// The type of the property. + /// The property. + /// The value. + public void SetValue(PerspexProperty property, T value) + { + Contract.Requires(property != null); + + this.SetValue((PerspexProperty)property, value); + } + + /// + /// Binds a to an observable. + /// + /// The type of the property. + /// The property. + /// The observable. + public void SetValue(PerspexProperty property, IObservable source) + { + Contract.Requires(property != null); + + this.SetValue((PerspexProperty)property, source); + } + /// /// Sets a value. /// @@ -351,23 +377,8 @@ namespace Perspex return; } - v = new PriorityValue(); + v = this.CreatePriorityValue(property); this.values.Add(property, v); - - v.Subscribe(x => - { - object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ? - this.GetDefaultValue(property) : - x.Item1; - object newValue = (x.Item2 == PerspexProperty.UnsetValue) ? - this.GetDefaultValue(property) : - x.Item2; - - if (!object.Equals(oldValue, newValue)) - { - this.RaisePropertyChanged(property, oldValue, newValue); - } - }); } if (binding == null) @@ -377,33 +388,46 @@ namespace Perspex else { v.SetLocalBinding(binding); + + this.Log().Debug(string.Format( + "Bound value of {0}.{1} (#{2:x8})", + this.GetType().Name, + property.Name, + this.GetHashCode())); } } /// - /// Sets a value. + /// Binds a to an style. /// - /// The type of the property. /// The property. - /// The value. - public void SetValue(PerspexProperty property, T value) + /// The activated value. + /// An observable which activates the value. + /// + /// Style bindings have a lower precedence than local value bindings. They are toggled + /// on or off by and can be unbound by the activator + /// completing. + /// + public void SetValue(PerspexProperty property, object value, IObservable activator) { Contract.Requires(property != null); + Contract.Requires(activator != null); - this.SetValue((PerspexProperty)property, value); - } + PriorityValue v; - /// - /// Binds a to an observable. - /// - /// The type of the property. - /// The property. - /// The observable. - public void SetValue(PerspexProperty property, IObservable source) - { - Contract.Requires(property != null); + if (!this.values.TryGetValue(property, out v)) + { + v = this.CreatePriorityValue(property); + this.values.Add(property, v); + } - this.SetValue((PerspexProperty)property, source); + v.AddStyle(activator, value); + + this.Log().Debug(string.Format( + "Bound value of {0}.{1} (#{2:x8}) to style", + this.GetType().Name, + property.Name, + this.GetHashCode())); } private static IObservable BoxObservable(IObservable observable) @@ -445,6 +469,35 @@ namespace Perspex return result; } + private PriorityValue CreatePriorityValue(PerspexProperty property) + { + PriorityValue result = new PriorityValue(); + + result.Subscribe(x => + { + object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ? + this.GetDefaultValue(property) : + x.Item1; + object newValue = (x.Item2 == PerspexProperty.UnsetValue) ? + this.GetDefaultValue(property) : + x.Item2; + + if (!object.Equals(oldValue, newValue)) + { + this.RaisePropertyChanged(property, oldValue, newValue); + + this.Log().Debug(string.Format( + "Set value of {0}.{1} (#{2:x8}) to {3}", + this.GetType().Name, + property.Name, + this.GetHashCode(), + newValue)); + } + }); + + return result; + } + private object GetDefaultValue(PerspexProperty property) { if (property.Inherits && this.inheritanceParent != null) @@ -493,42 +546,6 @@ namespace Perspex } } - //private void SetValueImpl(PerspexProperty property, object value) - //{ - // Contract.Requires(property != null); - - // if (!property.IsValidValue(value)) - // { - // throw new InvalidOperationException("Invalid value for " + property.Name); - // } - - // object oldValue = this.GetValue(property); - - // if (!object.Equals(oldValue, value)) - // { - // string valueString = value.ToString(); - - // if (value == PerspexProperty.UnsetValue) - // { - // valueString = "[Unset]"; - // this.values.Remove(property); - // } - // else - // { - // this.values[property] = value; - // } - - // this.RaisePropertyChanged(property, oldValue, value); - - // this.Log().Debug(string.Format( - // "Set value of {0}.{1} (#{2:x8}) to '{3}'", - // this.GetType().Name, - // property.Name, - // this.GetHashCode(), - // valueString)); - // } - //} - private class Binding { public object Observable { get; set; } diff --git a/Perspex/PriorityValue.cs b/Perspex/PriorityValue.cs index 3f68ff74d1..d024d79fbc 100644 --- a/Perspex/PriorityValue.cs +++ b/Perspex/PriorityValue.cs @@ -70,18 +70,6 @@ namespace Perspex this.localBinding = binding.Subscribe(value => this.LocalValue = value); } - public void AddStyle(object value) - { - StyleEntry entry = new StyleEntry(value); - - this.styles.Add(entry); - - if (this.localValue == PerspexProperty.UnsetValue) - { - this.Push(); - } - } - public void AddStyle(IObservable activator, object value) { Contract.Requires(activator != null); @@ -144,12 +132,6 @@ namespace Perspex { private IObservable activator; - public StyleEntry(object value) - { - this.Active = true; - this.Value = value; - } - public StyleEntry( IObservable activator, object value, @@ -162,7 +144,12 @@ namespace Perspex this.activator = activator; this.Value = value; - this.activator.Subscribe(x => this.Active = x, () => completed(this)); + this.activator.Subscribe(x => + { + this.Active = x; + activeChanged(); + }, + () => completed(this)); } public bool Active diff --git a/Perspex/Selectors.cs b/Perspex/Selectors.cs index 061146ba8d..b95bc23617 100644 --- a/Perspex/Selectors.cs +++ b/Perspex/Selectors.cs @@ -27,6 +27,7 @@ namespace Perspex return new Match { Control = control, + Observable = Observable.Return(true), Token = typeof(T).Name, }; } diff --git a/Perspex/Setter.cs b/Perspex/Setter.cs index 872788c6d6..1bb2b7074a 100644 --- a/Perspex/Setter.cs +++ b/Perspex/Setter.cs @@ -14,8 +14,6 @@ namespace Perspex public class Setter { - private object oldValue; - public Setter() { } @@ -37,51 +35,5 @@ namespace Perspex get; set; } - - internal Subject CreateSubject(Control control, string description) - { - object oldValue = control.GetValue(this.Property); - return new Subject(control, this.Value, oldValue, description); - } - - internal class Subject : IObservable, IBindingDescription - { - private Control control; - - private object onValue; - - private object offValue; - - private List> observers; - - public Subject(Control control, object onValue, object offValue, string description) - { - this.control = control; - this.onValue = onValue; - this.offValue = offValue; - this.observers = new List>(); - this.Description = description; - } - - public string Description - { - get; - private set; - } - - public IDisposable Subscribe(IObserver observer) - { - observers.Add(observer); - return Disposable.Create(() => this.observers.Remove(observer)); - } - - public void Push(bool on) - { - foreach (IObserver o in this.observers) - { - o.OnNext(on ? this.onValue : this.offValue); - } - } - } } } diff --git a/Perspex/Style.cs b/Perspex/Style.cs index e18b852560..a93b8e1d69 100644 --- a/Perspex/Style.cs +++ b/Perspex/Style.cs @@ -45,45 +45,11 @@ namespace Perspex if (match != null) { string description = "Style " + match.ToString(); - List> o = new List>(); - - while (match != null) - { - if (match.Observable != null) - { - o.Add(match.Observable); - } - - match = match.Previous; - } - - List subjects = new List(); + IObservable activator = match.GetActivator(); foreach (Setter setter in this.Setters) { - Setter.Subject subject = setter.CreateSubject(control, description); - subjects.Add(subject); - control.SetValue(setter.Property, subject); - } - - if (o.Count == 0) - { - foreach (Setter.Subject subject in subjects) - { - subject.Push(true); - } - } - else - { - Observable.CombineLatest(o).Subscribe(x => - { - bool on = x.All(y => y); - - foreach (Setter.Subject subject in subjects) - { - subject.Push(on); - } - }); + control.SetValue(setter.Property, setter.Value, activator); } } }