// ----------------------------------------------------------------------- // // Copyright 2013 Tricycle. All rights reserved. // // ----------------------------------------------------------------------- namespace Perspex { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Linq.Expressions; using System.Reactive; using System.Reactive.Linq; using System.Reflection; /// /// An object with support. /// /// /// This class is analogous to DependencyObject in WPF. /// public class PerspexObject { /// /// 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 on this object. /// private Dictionary values = new Dictionary(); /// /// The current bindings on this object. /// private Dictionary bindings = 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; } } } } /// /// 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); Binding binding; if (this.bindings.TryGetValue(property, out binding)) { binding.Dispose.Dispose(); this.bindings.Remove(property); } } /// /// Clears a value, including its binding. /// /// The property. public void ClearValue(PerspexProperty property) { Contract.Requires(property != null); this.ClearBinding(property); this.values.Remove(property); } /// /// 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); 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); return (IObservable)binding.Observable; } else { return null; } } /// /// 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 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()); } } return value; } /// /// 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 property. /// The value. public void SetValue(PerspexProperty property, object value) { Contract.Requires(property != null); TypeInfo typeInfo = value.GetType().GetTypeInfo(); Type observableType = typeInfo.ImplementedInterfaces.FirstOrDefault(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)); this.ClearBinding(property); if (observableType == null) { this.SetValueImpl(property, value); } else { IObservable observable = value as IObservable; if (observable == null) { MethodInfo cast = typeof(PerspexObject).GetTypeInfo() .DeclaredMethods .FirstOrDefault(x => x.Name == "CastToObject") .MakeGenericMethod(observableType.GenericTypeArguments[0]); observable = (IObservable)cast.Invoke(null, new[] { value }); } this.bindings.Add(property, new Binding { Observable = value, Dispose = observable.Subscribe(x => { this.SetValueImpl(property, x); }), }); } } /// /// 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); } private static IObservable CastToObject(IObservable observable) { return Observable.Create(observer => { return observable.Subscribe(value => { observer.OnNext(value); }); }); } /// /// 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 void SetValueImpl(PerspexProperty property, object value) { Contract.Requires(property != null); if (!property.IsValidType(value)) { throw new InvalidOperationException("Invalid value for " + property.Name); } object oldValue = this.GetValue(property); if (!object.Equals(oldValue, value)) { this.values[property] = value; this.RaisePropertyChanged(property, oldValue, value); } } private class Binding { public object Observable { get; set; } public IDisposable Dispose { get; set; } } } }