// ----------------------------------------------------------------------- // // Copyright 2013 Tricycle. All rights reserved. // // ----------------------------------------------------------------------- namespace Perspex { using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; using Splat; /// /// An object with support. /// /// /// This class is analogous to DependencyObject in WPF. /// public class PerspexObject : IEnableLogger { /// /// The registered properties by type. /// private static Dictionary> registered = new Dictionary>(); /// /// The parent object that inherited values are inherited from. /// private PerspexObject inheritanceParent; /// /// The set values/bindings on this object. /// private Dictionary values = new Dictionary(); /// /// Raised when a value changes on this object/ /// public event EventHandler PropertyChanged; /// /// Gets or sets the parent object that inherited values /// are inherited from. /// protected PerspexObject InheritanceParent { get { return this.inheritanceParent; } set { if (this.inheritanceParent != value) { if (this.inheritanceParent != null) { this.inheritanceParent.PropertyChanged -= this.ParentPropertyChanged; } var inherited = (from property in GetProperties(this.GetType()) where property.Inherits select new { Property = property, Value = this.GetValue(property), }).ToList(); this.inheritanceParent = value; foreach (var i in inherited) { object newValue = this.GetValue(i.Property); if (!object.Equals(i.Value, newValue)) { this.RaisePropertyChanged(i.Property, i.Value, newValue); } } if (this.inheritanceParent != null) { this.inheritanceParent.PropertyChanged += this.ParentPropertyChanged; } } } } /// /// Defers property updates due to style changes until /// is called. /// public void BeginDeferStyleChanges() { foreach (PriorityValue v in this.values.Values) { v.BeginDeferStyleChanges(); } this.Log().Debug(string.Format( "Defer style changes on {0} (#{1:x8})", this.GetType().Name, this.GetHashCode())); } /// /// Ends the defer of property updates due to style changes initiated by a previous call /// to . /// public void EndDeferStyleChanges() { foreach (PriorityValue v in this.values.Values) { v.EndDeferStyleChanges(); } this.Log().Debug(string.Format( "End defer style changes on {0} (#{1:x8})", this.GetType().Name, this.GetHashCode())); } /// /// Gets all s registered on a type. /// /// The type. /// A collection of definitions. public static IEnumerable GetProperties(Type type) { Contract.Requires(type != null); TypeInfo i = type.GetTypeInfo(); while (type != null) { List list; if (registered.TryGetValue(type, out list)) { foreach (PerspexProperty p in list) { yield return p; } } type = type.GetTypeInfo().BaseType; } } /// /// Registers a on a type. /// /// The type. /// The property. /// /// You won't usually want to call this method directly, instead use the /// method. /// public static void Register(Type type, PerspexProperty property) { Contract.Requires(type != null); Contract.Requires(property != null); List list; if (!registered.TryGetValue(type, out list)) { list = new List(); registered.Add(type, list); } if (!list.Contains(property)) { list.Add(property); } } /// /// Clears a binding on a , leaving the last bound value in /// place. /// /// The property. public void ClearBinding(PerspexProperty property) { Contract.Requires(property != null); PriorityValue value; if (this.values.TryGetValue(property, out value)) { value.ClearLocalBinding(); this.Log().Debug(string.Format( "Cleared binding on {0}.{1} (#{2:x8})", this.GetType().Name, property.Name, this.GetHashCode())); } } /// /// Clears a value, including its binding. /// /// The property. public void ClearValue(PerspexProperty property) { Contract.Requires(property != null); this.SetValue(property, PerspexProperty.UnsetValue); } /// /// Gets an observable for a . /// /// /// public IObservable GetObservable(PerspexProperty property) { Contract.Requires(property != null); return Observable.Create(observer => { EventHandler handler = (s, e) => { if (e.Property == property) { observer.OnNext(e.NewValue); } }; this.PropertyChanged += handler; observer.OnNext(this.GetValue(property)); return () => { this.PropertyChanged -= handler; }; }); } /// /// Gets an observable for a . /// /// /// /// public IObservable GetObservable(PerspexProperty property) { Contract.Requires(property != null); return this.GetObservable((PerspexProperty)property).Cast(); } /// /// Gets an observable for a . /// /// /// /// public IObservable GetObservable(ReadOnlyPerspexProperty property) { Contract.Requires(property != null); return this.GetObservable((PerspexProperty)property.Property); } /// /// Gets an observable for a . /// /// /// /// public IObservable> GetObservableWithHistory(PerspexProperty property) { return Observable.Create>(observer => { EventHandler handler = (s, e) => { if (e.Property == property) { observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue)); } }; this.PropertyChanged += handler; return () => { this.PropertyChanged -= handler; }; }); } /// /// Gets a value. /// /// The property. /// The value. public object GetValue(PerspexProperty property) { Contract.Requires(property != null); object result; PriorityValue value; if (this.values.TryGetValue(property, out value)) { result = value.GetEffectiveValue(); } else { result = PerspexProperty.UnsetValue; } if (result == PerspexProperty.UnsetValue) { result = this.GetDefaultValue(property); } return result; } /// /// Gets a value. /// /// The property. /// The value. public T GetValue(PerspexProperty property) { Contract.Requires(property != null); return (T)this.GetValue((PerspexProperty)property); } /// /// Gets a value. /// /// The type of the property. /// The property. /// The value. public T GetValue(ReadOnlyPerspexProperty property) { Contract.Requires(property != null); return (T)this.GetValue(property.Property); } /// /// Checks whether a is set on this object. /// /// /// public bool IsSet(PerspexProperty property) { Contract.Requires(property != null); 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. /// /// The property. /// The value. public void SetValue(PerspexProperty property, object value) { Contract.Requires(property != null); IObservable binding = TryCastToObservable(value); PriorityValue v; if (!this.values.TryGetValue(property, out v)) { if (value == PerspexProperty.UnsetValue) { return; } v = this.CreatePriorityValue(property); this.values.Add(property, v); } if (binding == null) { v.SetLocalValue(value); } else { v.SetLocalBinding(binding); this.Log().Debug(string.Format( "Bound value of {0}.{1} (#{2:x8})", this.GetType().Name, property.Name, this.GetHashCode())); } } /// /// Binds a to a style. /// /// The property. /// 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); PriorityValue v; if (!this.values.TryGetValue(property, out v)) { v = this.CreatePriorityValue(property); this.values.Add(property, v); } 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) { return Observable.Create(observer => { return observable.Subscribe(value => { observer.OnNext(value); }); }); } 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 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) { return this.inheritanceParent.GetValue(property); } else { return property.GetDefaultValue(this.GetType()); } } /// /// Called when a property is changed on the current . /// /// The event sender. /// The event args. /// /// Checks for changes in an inherited property value. /// private void ParentPropertyChanged(object sender, PerspexPropertyChangedEventArgs e) { Contract.Requires(e != null); if (e.Property.Inherits && !this.IsSet(e.Property)) { this.RaisePropertyChanged(e.Property, e.OldValue, e.NewValue); } } /// /// Raises the event. /// /// The property that has changed. /// The old property value. /// The new property value. private void RaisePropertyChanged(PerspexProperty property, object oldValue, object newValue) { Contract.Requires(property != null); if (this.PropertyChanged != null) { this.PropertyChanged( this, new PerspexPropertyChangedEventArgs(property, oldValue, newValue)); } } private class Binding { public object Observable { get; set; } public IDisposable Dispose { get; set; } } } }