// ----------------------------------------------------------------------- // // Copyright 2014 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex { using Perspex.Reactive; using Serilog; using Serilog.Core.Enrichers; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; /// /// The priority of a binding. /// public enum BindingPriority { /// /// A value that comes from an animation. /// Animation = -2, /// /// A local value. /// LocalValue = 0, /// /// 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 { /// /// 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; /// /// A serilog logger for logging property events. /// private ILogger propertyLog; /// /// Initializes a new instance of the class. /// public PerspexObject() { this.propertyLog = Log.ForContext(new[] { new PropertyEnricher("Area", "Property"), new PropertyEnricher("SourceContext", this.GetType()), new PropertyEnricher("Id", this.GetHashCode()), }); foreach (var property in this.GetRegisteredProperties()) { var e = new PerspexPropertyChangedEventArgs( this, property, PerspexProperty.UnsetValue, property.GetDefaultValue(this.GetType()), BindingPriority.Unset); property.NotifyInitialized(e); } } /// /// 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, BindingPriority.LocalValue); } } 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), binding.Priority); 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 new PerspexObservable( observer => { EventHandler handler = (s, e) => { if (e.Property == property) { observer.OnNext(e.NewValue); } }; observer.OnNext(this.GetValue(property)); this.PropertyChanged += handler; return Disposable.Create(() => { this.PropertyChanged -= handler; }); }, this.GetObservableDescription(property)); } /// /// Gets an observable for a . /// /// The property type. /// The property. /// An observable. 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 new PerspexObservable>( observer => { EventHandler handler = (s, e) => { if (e.Property == property) { observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue)); } }; this.PropertyChanged += handler; return Disposable.Create(() => { this.PropertyChanged -= handler; }); }, this.GetObservableDescription(property)); } /// /// 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 properties that are registered on this object. /// /// /// A collection of objects. /// public IEnumerable GetRegisteredProperties() { Type type = this.GetType(); while (type != null) { List list; if (registered.TryGetValue(type, out list)) { foreach (var p in list) { yield return p; } } type = type.GetTypeInfo().BaseType; } } /// /// 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. public 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. /// The priority of the value. public void SetValue( PerspexProperty property, object value, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); 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.propertyLog.Verbose( "Set {Property} to {$Value} with priority {Priority}", property, value, priority); v.SetDirectValue(value, (int)priority); } /// /// Sets a value. /// /// The type of the property. /// The property. /// The value. /// The priority of the value. public void SetValue( PerspexProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); this.SetValue((PerspexProperty)property, value, 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); PriorityValue v; IDescription description = source as IDescription; if (!this.IsRegistered(property)) { throw new InvalidOperationException(string.Format( "Property '{0}' not registered on '{1}'", property.Name, this.GetType())); } if (!this.values.TryGetValue(property, out v)) { v = this.CreatePriorityValue(property); this.values.Add(property, v); } this.propertyLog.Verbose( "Bound {Property} to {Binding} with priority {Priority}", property, source, priority); 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, source.Select(x => (object)x), priority); } /// /// Initialites a two-way bind between s. /// /// The property on this object. /// The source object. /// The property on the source object. /// The priority of the binding. /// /// A disposable which can be used to terminate the binding. /// /// /// The binding is first carried out from to this. /// public IDisposable BindTwoWay( PerspexProperty property, PerspexObject source, PerspexProperty sourceProperty, BindingPriority priority = BindingPriority.LocalValue) { return new CompositeDisposable( this.Bind(property, source.GetObservable(sourceProperty)), source.Bind(sourceProperty, this.GetObservable(property))); } /// /// Forces the specified property to be re-coerced. /// /// The property. public void CoerceValue(PerspexProperty property) { PriorityValue value; if (this.values.TryGetValue(property, out value)) { value.Coerce(); } } /// /// Gets all priority values set on the object. /// /// A collection of property/value tuples. internal IDictionary GetSetValues() { return this.values; } /// /// Forces re-coercion of properties when a property value changes. /// /// The property to that affects coercion. /// The affected properties. protected static void AffectsCoercion(PerspexProperty property, params PerspexProperty[] affected) { property.Changed.Subscribe(e => { foreach (var p in affected) { e.Sender.CoerceValue(p); } }); } /// /// Called when a perspex property changes on the object. /// /// The event arguments. protected virtual void OnPropertyChanged(PerspexPropertyChangedEventArgs e) { } /// /// Creates a for a . /// /// The property. /// The . private PriorityValue CreatePriorityValue(PerspexProperty property) { Func coerce = null; if (property.Coerce != null) { coerce = v => property.Coerce(this, v); } PriorityValue result = new PriorityValue(property.Name, property.PropertyType, coerce); 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, (BindingPriority)result.ValuePriority); this.propertyLog.Verbose( "{Property} changed from {$Old} to {$Value} with priority {Priority}", property, oldValue, newValue, (BindingPriority)result.ValuePriority); } }); 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, BindingPriority.LocalValue); } } /// /// Gets a description of a property that van be used in observables. /// /// The property /// The description. private string GetObservableDescription(PerspexProperty property) { return string.Format("{0}.{1}", this.GetType().Name, property.Name); } /// /// Raises the event. /// /// The property that has changed. /// The old property value. /// The new property value. /// The priority of the binding that produced the value. private void RaisePropertyChanged( PerspexProperty property, object oldValue, object newValue, BindingPriority priority) { Contract.Requires(property != null); PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( this, property, oldValue, newValue, priority); this.OnPropertyChanged(e); property.NotifyChanged(e); if (this.PropertyChanged != null) { this.PropertyChanged(this, e); } if (this.inpcChanged != null) { PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); this.inpcChanged(this, e2); } } } }