// ----------------------------------------------------------------------- // // Copyright 2014 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex { using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Reflection; using Splat; /// /// The priority of a binding. /// public enum BindingPriority { /// /// A local value. /// LocalValue, /// /// A triggered style binding. /// /// /// A style trigger is a selector such as .class which overrides a /// binding. In this way, a basic control can have /// for example a Background from the templated parent which changes when the /// control has the :pointerover class. /// StyleTrigger, /// /// A binding to a property on the templated parent. /// TemplatedParent, /// /// A style binding. /// Style, /// /// The binding is uninitialized. /// Unset = int.MaxValue, } /// /// An object with support. /// /// /// This class is analogous to DependencyObject in WPF. /// public class PerspexObject : INotifyPropertyChanged, 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(); /// /// Event handler for implementation. /// private PropertyChangedEventHandler inpcChanged; /// /// Raised when a value changes on this object. /// public event EventHandler PropertyChanged; /// /// Raised when a value changes on this object. /// event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged { add { this.inpcChanged += value; } remove { this.inpcChanged -= value; } } /// /// 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 or sets the value of a . /// /// The property. public object this[PerspexProperty property] { get { return this.GetValue(property); } set { this.SetValue(property, value); } } /// /// Gets or sets a binding for a . /// /// The binding information. public IObservable this[Binding binding] { get { return new Binding { Mode = binding.Mode, Priority = binding.Priority, Property = binding.Property, Source = this, }; } set { BindingMode mode = (binding.Mode == BindingMode.Default) ? binding.Property.DefaultBindingMode : binding.Mode; Binding sourceBinding = value as Binding; if (sourceBinding == null && mode != BindingMode.OneWay) { throw new InvalidOperationException("Can only bind OneWay to plain IObservable."); } switch (mode) { case BindingMode.Default: case BindingMode.OneWay: this.Bind(binding.Property, value, binding.Priority); break; case BindingMode.OneTime: this.SetValue(binding.Property, sourceBinding.Source.GetValue(sourceBinding.Property)); break; case BindingMode.OneWayToSource: sourceBinding.Source.Bind(sourceBinding.Property, this.GetObservable(binding.Property), binding.Priority); break; case BindingMode.TwoWay: this.BindTwoWay(binding.Property, sourceBinding.Source, sourceBinding.Property); break; } } } /// /// 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 's local value. /// /// The property. public void ClearValue(PerspexProperty property) { Contract.Requires(property != null); this.SetValue(property, PerspexProperty.UnsetValue); } /// /// Gets an observable for a . /// /// The property. /// An observable. 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> 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.Value; } 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 all of the values explicitly set on this object. /// public IEnumerable> GetSetValues() { foreach (var value in this.values) { yield return Tuple.Create( value.Key, value.Value.Value, (BindingPriority)value.Value.ValuePriority); } } /// /// Checks whether a is set on this object. /// /// The property. /// True if the property is set, otherwise false. public bool IsSet(PerspexProperty property) { Contract.Requires(property != null); return this.values.ContainsKey(property); } /// /// Checks whether a is registered on this class. /// /// The property. /// True if the property is registered, otherwise false. private bool IsRegistered(PerspexProperty property) { Type type = this.GetType(); while (type != null) { List list; if (registered.TryGetValue(type, out list)) { if (list.Contains(property)) { return true; } } type = type.GetTypeInfo().BaseType; } return false; } /// /// Sets a value. /// /// The property. /// The value. public void SetValue(PerspexProperty property, object value) { Contract.Requires(property != null); const int Priority = (int)BindingPriority.LocalValue; PriorityValue v; if (!this.IsRegistered(property)) { throw new InvalidOperationException(string.Format( "Property '{0}' not registered on '{1}'", property.Name, this.GetType())); } if (!PriorityValue.IsValidValue(value, property.PropertyType)) { throw new InvalidOperationException(string.Format( "Invalid value for Property '{0}': {1} ({2})", property.Name, value, value.GetType().FullName)); } if (!this.values.TryGetValue(property, out v)) { if (value == PerspexProperty.UnsetValue) { return; } v = this.CreatePriorityValue(property); this.values.Add(property, v); } this.Log().Debug( "Set local value of {0}.{1} (#{2:x8}) to {3}", this.GetType().Name, property.Name, this.GetHashCode(), value); v.Replace(Observable.Never().StartWith(value), Priority); } /// /// 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. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); PriorityValue v; IDescription description = source as IDescription; if (!this.values.TryGetValue(property, out v)) { v = this.CreatePriorityValue(property); this.values.Add(property, v); } this.Log().Debug( "Bound value of {0}.{1} (#{2:x8}) to {3}", this.GetType().Name, property.Name, this.GetHashCode(), description != null ? description.Description : "[Anonymous]"); if (priority == BindingPriority.LocalValue) { return v.Replace(source, (int)priority); } else { return v.Add(source, (int)priority); } } /// /// Binds a to an observable. /// /// The type of the property. /// The property. /// The observable. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); return this.Bind((PerspexProperty)property, (IObservable)source, priority); } /// /// Initialites a two-way bind between s. /// /// The property on this object. /// The source object. /// The property on the source object. /// /// A disposable which can be used to terminate the binding. /// /// /// The binding is first carried out from to this. Two-way /// bindings are always at the LocalValue priority. /// public void BindTwoWay( PerspexProperty property, PerspexObject source, PerspexProperty sourceProperty) { source.GetObservable(sourceProperty).Subscribe(x => this.SetValue(property, x)); this.GetObservable(property).Subscribe(x => source.SetValue(sourceProperty, x)); } private PriorityValue CreatePriorityValue(PerspexProperty property) { PriorityValue result = new PriorityValue(property.Name, property.PropertyType); result.Changed.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( "Value of {0}.{1} (#{2:x8}) changed from {3} to {4}", this.GetType().Name, property.Name, this.GetHashCode(), oldValue, newValue); } }); return result; } /// /// Gets the default value for a property. /// /// The property. /// The default value. 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) { PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(this, property, oldValue, newValue); property.NotifyChanged(e); this.PropertyChanged(this, e); } if (this.inpcChanged != null) { PropertyChangedEventArgs e = new PropertyChangedEventArgs(property.Name); this.inpcChanged(this, e); } } } }