diff --git a/Perspex.UnitTests/PerspexObjectTests.cs b/Perspex.UnitTests/PerspexObjectTests.cs index b222fbc760..c9fb9515a4 100644 --- a/Perspex.UnitTests/PerspexObjectTests.cs +++ b/Perspex.UnitTests/PerspexObjectTests.cs @@ -87,6 +87,16 @@ namespace Perspex.UnitTests Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); } + [TestMethod] + public void SetValue_Sets_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.AreEqual("newvalue", target.GetValue(Class1.FooProperty)); + } + [TestMethod] public void SetValue_Raises_PropertyChanged() { @@ -296,6 +306,17 @@ namespace Perspex.UnitTests Assert.AreEqual("reset", target.GetValue(Class1.FooProperty)); } + [TestMethod] + public void Setting_UnsetValue_Reverts_To_Default_Value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + target.SetValue(Class1.FooProperty, PerspexProperty.UnsetValue); + + Assert.AreEqual("foodefault", target.GetValue(Class1.FooProperty)); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = diff --git a/Perspex/Perspex.csproj b/Perspex/Perspex.csproj index 1fa7d0b0cb..e90afe33b6 100644 --- a/Perspex/Perspex.csproj +++ b/Perspex/Perspex.csproj @@ -80,6 +80,7 @@ + diff --git a/Perspex/PerspexObject.cs b/Perspex/PerspexObject.cs index f51c57ab60..84eaf8e289 100644 --- a/Perspex/PerspexObject.cs +++ b/Perspex/PerspexObject.cs @@ -36,16 +36,10 @@ namespace Perspex private PerspexObject inheritanceParent; /// - /// The set values on this object. + /// The set values/bindings on this object. /// - private Dictionary values = - new Dictionary(); - - /// - /// The current bindings on this object. - /// - private Dictionary bindings = - new Dictionary(); + private Dictionary values = + new Dictionary(); /// /// Raised when a value changes on this object/ @@ -163,12 +157,11 @@ namespace Perspex public void ClearBinding(PerspexProperty property) { Contract.Requires(property != null); - Binding binding; + PriorityValue value; - if (this.bindings.TryGetValue(property, out binding)) + if (this.values.TryGetValue(property, out value)) { - binding.Dispose.Dispose(); - this.bindings.Remove(property); + value.ClearLocalBinding(); this.Log().Debug(string.Format( "Cleared binding on {0}.{1} (#{2:x8})", @@ -189,60 +182,6 @@ namespace Perspex this.SetValue(property, PerspexProperty.UnsetValue); } - /// - /// Clears a binding on a , returning the bound observable and - /// leaving the last bound value in place. - /// - /// The property. - public IObservable ExtractBinding(PerspexProperty property) - { - Binding binding; - - if (this.bindings.TryGetValue(property, out binding)) - { - this.bindings.Remove(property); - - this.Log().Debug(string.Format( - "Extracted binding on {0}.{1} (#{2:x8})", - this.GetType().Name, - property.Name, - this.GetHashCode())); - - return (IObservable)binding.Observable; - } - else - { - return null; - } - } - - /// - /// Clears a binding on a , returning the bound observable and - /// leaving the last bound value in place. - /// - /// The property. - public IObservable ExtractBinding(PerspexProperty property) - { - Binding binding; - - if (this.bindings.TryGetValue(property, out binding)) - { - this.bindings.Remove(property); - - this.Log().Debug(string.Format( - "Extracted binding on {0}.{1} (#{2:x8})", - this.GetType().Name, - property.Name, - this.GetHashCode())); - - return (IObservable)binding.Observable; - } - else - { - return null; - } - } - /// /// Gets an observable for a . /// @@ -334,21 +273,25 @@ namespace Perspex { Contract.Requires(property != null); - object value; + object result; - if (!this.values.TryGetValue(property, out value)) + PriorityValue value; + + if (this.values.TryGetValue(property, out value)) { - if (property.Inherits && this.inheritanceParent != null) - { - value = this.inheritanceParent.GetValue(property); - } - else - { - value = property.GetDefaultValue(this.GetType()); - } + result = value.GetEffectiveValue(); + } + else + { + result = PerspexProperty.UnsetValue; } - return value; + if (result == PerspexProperty.UnsetValue) + { + result = this.GetDefaultValue(property); + } + + return result; } /// @@ -397,48 +340,43 @@ namespace Perspex { Contract.Requires(property != null); - TypeInfo typeInfo = value.GetType().GetTypeInfo(); - Type observableType = typeInfo.ImplementedInterfaces.FirstOrDefault(x => - x.IsConstructedGenericType && - x.GetGenericTypeDefinition() == typeof(IObservable<>)); + IObservable binding = TryCastToObservable(value); - this.ClearBinding(property); + PriorityValue v; - if (observableType == null) - { - this.SetValueImpl(property, value); - } - else + if (!this.values.TryGetValue(property, out v)) { - IObservable observable = value as IObservable; - IBindingDescription bindingDescription = value as IBindingDescription; - string description = (bindingDescription != null) ? bindingDescription.Description : value.GetType().Name; - - if (observable == null) + if (value == PerspexProperty.UnsetValue) { - MethodInfo cast = typeof(PerspexObject).GetTypeInfo() - .DeclaredMethods - .FirstOrDefault(x => x.Name == "CastToObject") - .MakeGenericMethod(observableType.GenericTypeArguments[0]); - - observable = (IObservable)cast.Invoke(null, new[] { value }); + return; } - this.bindings.Add(property, new Binding + v = new PriorityValue(); + this.values.Add(property, v); + + v.Subscribe(x => { - Observable = value, - Dispose = observable.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.SetValueImpl(property, x); - }), + this.RaisePropertyChanged(property, oldValue, newValue); + } }); + } - this.Log().Debug(string.Format( - "Bound {0}.{1} (#{2:x8}) to {3}", - this.GetType().Name, - property.Name, - this.GetHashCode(), - description)); + if (binding == null) + { + v.SetLocalValue(value); + } + else + { + v.SetLocalBinding(binding); } } @@ -468,7 +406,7 @@ namespace Perspex this.SetValue((PerspexProperty)property, source); } - private static IObservable CastToObject(IObservable observable) + private static IObservable BoxObservable(IObservable observable) { return Observable.Create(observer => { @@ -479,6 +417,46 @@ namespace Perspex }); } + private static IObservable TryCastToObservable(object value) + { + Type observableType = value.GetType().GetTypeInfo() + .ImplementedInterfaces + .FirstOrDefault(x => + x.IsConstructedGenericType && + x.GetGenericTypeDefinition() == typeof(IObservable<>)); + + IObservable result = null; + + if (observableType != null) + { + result = value as IObservable; + + if (result == null) + { + MethodInfo cast = typeof(PerspexObject).GetTypeInfo() + .DeclaredMethods + .FirstOrDefault(x => x.Name == "BoxObservable") + .MakeGenericMethod(observableType.GenericTypeArguments[0]); + + result = (IObservable)cast.Invoke(null, new[] { value }); + } + } + + return result; + } + + private object GetDefaultValue(PerspexProperty property) + { + if (property.Inherits && this.inheritanceParent != null) + { + return this.inheritanceParent.GetValue(property); + } + else + { + return property.GetDefaultValue(this.GetType()); + } + } + /// /// Called when a property is changed on the current . /// @@ -515,41 +493,41 @@ 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 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 { diff --git a/Perspex/PriorityValue.cs b/Perspex/PriorityValue.cs new file mode 100644 index 0000000000..3f68ff74d1 --- /dev/null +++ b/Perspex/PriorityValue.cs @@ -0,0 +1,181 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Reactive.Disposables; + + internal class PriorityValue : IObservable> + { + private object localValue = PerspexProperty.UnsetValue; + + private IDisposable localBinding; + + private object lastValue = PerspexProperty.UnsetValue; + + private List styles = new List(); + + private List>> observers = + new List>>(); + + public object LocalValue + { + get + { + return this.localValue; + } + + set + { + if (!object.Equals(this.localValue, value)) + { + this.localValue = value; + this.Push(); + } + } + } + + public void ClearLocalBinding() + { + if (this.localBinding != null) + { + this.localBinding.Dispose(); + } + } + + public void SetLocalValue(object value) + { + if (this.localBinding != null) + { + this.localBinding.Dispose(); + } + + this.LocalValue = value; + } + + public void SetLocalBinding(IObservable binding) + { + if (this.localBinding != null) + { + this.localBinding.Dispose(); + } + + 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); + + StyleEntry entry = new StyleEntry(activator, value, this.Push, e => this.styles.Remove(e)); + + this.styles.Add(entry); + + if (this.localValue == PerspexProperty.UnsetValue) + { + this.Push(); + } + } + + public object GetEffectiveValue() + { + if (this.localValue != PerspexProperty.UnsetValue) + { + return this.localValue; + } + else + { + foreach (StyleEntry style in Enumerable.Reverse(this.styles)) + { + if (style.Active) + { + return style.Value; + } + } + } + + return PerspexProperty.UnsetValue; + } + + public IDisposable Subscribe(IObserver> observer) + { + Contract.Requires(observer != null); + + this.observers.Add(observer); + + return Disposable.Create(() => this.observers.Remove(observer)); + } + + private void Push() + { + object value = this.GetEffectiveValue(); + + if (!object.Equals(this.lastValue, value)) + { + foreach (IObserver> observer in this.observers) + { + observer.OnNext(Tuple.Create(this.lastValue, value)); + } + + this.lastValue = value; + } + } + + private class StyleEntry + { + private IObservable activator; + + public StyleEntry(object value) + { + this.Active = true; + this.Value = value; + } + + public StyleEntry( + IObservable activator, + object value, + Action activeChanged, + Action completed) + { + Contract.Requires(activator != null); + Contract.Requires(activeChanged != null); + + this.activator = activator; + this.Value = value; + + this.activator.Subscribe(x => this.Active = x, () => completed(this)); + } + + public bool Active + { + get; + private set; + } + + public object Value + { + get; + private set; + } + } + } +}