// Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reflection; using Perspex.Reactive; using Perspex.Threading; using Perspex.Utilities; using Serilog; using Serilog.Core.Enrichers; namespace Perspex { /// /// An object with support. /// /// /// This class is analogous to DependencyObject in WPF. /// public class PerspexObject : IObservablePropertyBag, INotifyPropertyChanged { /// /// The parent object that inherited values are inherited from. /// private PerspexObject _inheritanceParent; /// /// The set values/bindings on this object. /// private readonly Dictionary _values = new Dictionary(); /// /// Event handler for implementation. /// private PropertyChangedEventHandler _inpcChanged; /// /// A serilog logger for logging property events. /// private readonly ILogger _propertyLog; /// /// Initializes a new instance of the class. /// public PerspexObject() { _propertyLog = Log.ForContext(new[] { new PropertyEnricher("Area", "Property"), new PropertyEnricher("SourceContext", GetType()), new PropertyEnricher("Id", GetHashCode()), }); foreach (var property in PerspexPropertyRegistry.Instance.GetRegistered(this)) { object value = property.IsDirect ? property.Getter(this) : property.GetDefaultValue(GetType()); var e = new PerspexPropertyChangedEventArgs( this, property, PerspexProperty.UnsetValue, value, 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 { _inpcChanged += value; } remove { _inpcChanged -= value; } } /// /// Gets the object that inherited values are inherited from. /// IPropertyBag IPropertyBag.InheritanceParent => InheritanceParent; /// /// Gets or sets the parent object that inherited values /// are inherited from. /// /// /// The inheritance parent. /// protected PerspexObject InheritanceParent { get { return _inheritanceParent; } set { if (_inheritanceParent != value) { if (_inheritanceParent != null) { _inheritanceParent.PropertyChanged -= ParentPropertyChanged; } var inherited = (from property in PerspexPropertyRegistry.Instance.GetRegistered(this) where property.Inherits select new { Property = property, Value = GetValue(property), }).ToList(); _inheritanceParent = value; foreach (var i in inherited) { object newValue = GetValue(i.Property); if (!Equals(i.Value, newValue)) { RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue); } } if (_inheritanceParent != null) { _inheritanceParent.PropertyChanged += ParentPropertyChanged; } } } } /// /// Gets or sets the value of a . /// /// The property. public object this[PerspexProperty property] { get { return GetValue(property); } set { SetValue(property, value); } } /// /// Gets or sets a binding for a . /// /// The binding information. public IObservable this[BindingDescriptor binding] { get { return new BindingDescriptor { Mode = binding.Mode, Priority = binding.Priority, Property = binding.Property, Source = this, }; } set { var mode = (binding.Mode == BindingMode.Default) ? binding.Property.DefaultBindingMode : binding.Mode; var sourceBinding = value as BindingDescriptor; if (sourceBinding == null && mode > BindingMode.OneWay) { mode = BindingMode.OneWay; } switch (mode) { case BindingMode.Default: case BindingMode.OneWay: Bind(binding.Property, value, binding.Priority); break; case BindingMode.OneTime: SetValue(binding.Property, sourceBinding.Source.GetValue(sourceBinding.Property), binding.Priority); break; case BindingMode.OneWayToSource: sourceBinding.Source.Bind(sourceBinding.Property, GetObservable(binding.Property), binding.Priority); break; case BindingMode.TwoWay: BindTwoWay(binding.Property, sourceBinding.Source, sourceBinding.Property); break; } } } public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); /// /// Clears a 's local value. /// /// The property. public void ClearValue(PerspexProperty property) { Contract.Requires(property != null); 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(GetValue(property)); PropertyChanged += handler; return Disposable.Create(() => { PropertyChanged -= handler; }); }, GetDescription(property)); } /// /// Gets an observable for a . /// /// The property type. /// The property. /// An observable. public IObservable GetObservable(PerspexProperty property) { Contract.Requires(property != null); return GetObservable((PerspexProperty)property).Cast(); } /// /// Gets an observable for a . /// /// The type of the property. /// The property. /// An observable which when subscribed pushes the old and new values of the /// property each time it is changed. 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)); } }; PropertyChanged += handler; return Disposable.Create(() => { PropertyChanged -= handler; }); }, GetDescription(property)); } /// /// Gets a value. /// /// The property. /// The value. public object GetValue(PerspexProperty property) { Contract.Requires(property != null); if (property.IsDirect) { return GetRegistered(property).Getter(this); } else { object result = PerspexProperty.UnsetValue; PriorityValue value; if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } if (_values.TryGetValue(property, out value)) { result = value.Value; } if (result == PerspexProperty.UnsetValue) { result = GetDefaultValue(property); } return result; } } /// /// Gets a value. /// /// The type of the property. /// The property. /// The value. public T GetValue(PerspexProperty property) { Contract.Requires(property != null); if (property.IsDirect) { return ((PerspexProperty)GetRegistered(property)).Getter(this); } else { return (T)GetValue((PerspexProperty)property); } } /// /// 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); PriorityValue value; if (_values.TryGetValue(property, out value)) { return value.Value != PerspexProperty.UnsetValue; } 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); VerifyAccess(); if (property.IsDirect) { property = GetRegistered(property); if (property.Setter == null) { throw new ArgumentException($"The property {property.Name} is readonly."); } LogPropertySet(property, value, priority); property.Setter(this, UnsetToDefault(value, property)); } else { PriorityValue v; var originalValue = value; if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) { throw new ArgumentException(string.Format( "Invalid value for Property '{0}': '{1}' ({2})", property.Name, originalValue, originalValue?.GetType().FullName ?? "(null)")); } if (!_values.TryGetValue(property, out v)) { if (value == PerspexProperty.UnsetValue) { return; } v = CreatePriorityValue(property); _values.Add(property, v); } LogPropertySet(property, value, priority); v.SetValue(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); VerifyAccess(); if (property.IsDirect) { property = (PerspexProperty)GetRegistered(property); if (property.Setter == null) { throw new ArgumentException($"The property {property.Name} is readonly."); } LogPropertySet(property, value, priority); property.Setter(this, value); } else { SetValue((PerspexProperty)property, value, priority); } } /// /// Binds a to an observable. /// /// 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); VerifyAccess(); if (property.IsDirect) { property = GetRegistered(property); if (property.Setter == null) { throw new ArgumentException($"The property {property.Name} is readonly."); } _propertyLog.Verbose( "Bound {Property} to {Binding} with priority LocalValue", property, GetDescription(source)); return source .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType)) .Subscribe(x => SetValue(property, x)); } else { PriorityValue v; if (!PerspexPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } if (!_values.TryGetValue(property, out v)) { v = CreatePriorityValue(property); _values.Add(property, v); } _propertyLog.Verbose( "Bound {Property} to {Binding} with priority {Priority}", property, GetDescription(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); VerifyAccess(); if (property.IsDirect) { property = (PerspexProperty)GetRegistered(property); if (property.Setter == null) { throw new ArgumentException($"The property {property.Name} is readonly."); } return source.Subscribe(x => SetValue(property, x)); } else { return Bind((PerspexProperty)property, source.Select(x => (object)x), priority); } } /// /// Initiates a two-way binding 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) { VerifyAccess(); _propertyLog.Verbose( "Bound two way {Property} to {Binding} with priority {Priority}", property, source, priority); return new CompositeDisposable( Bind(property, source.GetObservable(sourceProperty)), source.Bind(sourceProperty, GetObservable(property))); } /// /// Initiates a two-way binding between a and an /// . /// /// The property on this object. /// The subject to bind to. /// 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, ISubject source, BindingPriority priority = BindingPriority.LocalValue) { VerifyAccess(); _propertyLog.Verbose( "Bound two way {Property} to {Binding} with priority {Priority}", property, GetDescription(source), priority); return new CompositeDisposable( Bind(property, source), GetObservable(property).Subscribe(source)); } /// /// Forces the specified property to be revalidated. /// /// The property. public void Revalidate(PerspexProperty property) { VerifyAccess(); PriorityValue value; if (_values.TryGetValue(property, out value)) { value.Revalidate(); } } /// bool IPropertyBag.IsRegistered(PerspexProperty property) { return PerspexPropertyRegistry.Instance.IsRegistered(this, property); } /// /// Gets all priority values set on the object. /// /// A collection of property/value tuples. internal IDictionary GetSetValues() { return _values; } /// /// Forces revalidation of properties when a property value changes. /// /// The property to that affects validation. /// The affected properties. protected static void AffectsValidation(PerspexProperty property, params PerspexProperty[] affected) { property.Changed.Subscribe(e => { foreach (var p in affected) { e.Sender.Revalidate(p); } }); } /// /// Called when a perspex property changes on the object. /// /// The event arguments. protected virtual void OnPropertyChanged(PerspexPropertyChangedEventArgs e) { } /// /// 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. protected void RaisePropertyChanged( PerspexProperty property, object oldValue, object newValue, BindingPriority priority) { Contract.Requires(property != null); VerifyAccess(); PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( this, property, oldValue, newValue, priority); if (property.Notifying != null) { property.Notifying(this, true); } try { OnPropertyChanged(e); property.NotifyChanged(e); if (PropertyChanged != null) { PropertyChanged(this, e); } if (_inpcChanged != null) { PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); _inpcChanged(this, e2); } } finally { if (property.Notifying != null) { property.Notifying(this, false); } } } /// /// Sets the backing field for a direct perspex property, raising the /// event if the value has changed. /// /// The type of the property. /// The property. /// The backing field. /// The value. /// /// True if the value changed, otherwise false. /// protected bool SetAndRaise(PerspexProperty property, ref T field, T value) { VerifyAccess(); if (!object.Equals(field, value)) { var old = field; field = value; RaisePropertyChanged(property, old, value, BindingPriority.LocalValue); return true; } else { return false; } } /// /// Converts an unset value to the default value for a property type. /// /// The value. /// The property. /// The value. private static object UnsetToDefault(object value, PerspexProperty property) { return value == PerspexProperty.UnsetValue ? TypeUtilities.Default(property.PropertyType) : value; } /// /// Creates a for a . /// /// The property. /// The . private PriorityValue CreatePriorityValue(PerspexProperty property) { Func validate = property.GetValidationFunc(GetType()); Func validate2 = null; if (validate != null) { validate2 = v => validate(this, v); } PriorityValue result = new PriorityValue(property.Name, property.PropertyType, validate2); result.Changed.Subscribe(x => { object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ? GetDefaultValue(property) : x.Item1; object newValue = (x.Item2 == PerspexProperty.UnsetValue) ? GetDefaultValue(property) : x.Item2; if (!Equals(oldValue, newValue)) { RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority); _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 && _inheritanceParent != null) { return _inheritanceParent.GetValue(property); } else { return property.GetDefaultValue(GetType()); } } /// /// Given a returns a registered perspex property that is /// equal or throws if not found. /// /// The property. /// The registered property. public PerspexProperty GetRegistered(PerspexProperty property) { var result = PerspexPropertyRegistry.Instance.FindRegistered(this, property); if (result == null) { ThrowNotRegistered(property); } return result; } /// /// 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 && !IsSet(e.Property)) { 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 GetDescription(PerspexProperty property) { return string.Format("{0}.{1}", GetType().Name, property.Name); } /// /// Gets a description of an observable that van be used in logs. /// /// The observable. /// The description. private string GetDescription(IObservable o) { var description = o as IDescription; return description?.Description ?? o.ToString(); } /// /// Logs a property set message. /// /// The property. /// The new value. /// The priority. private void LogPropertySet(PerspexProperty property, object value, BindingPriority priority) { _propertyLog.Verbose( "Set {Property} to {$Value} with priority {Priority}", property, value, priority); } /// /// Throws an exception indicating that the specified property is not registered on this /// object. /// /// The property private void ThrowNotRegistered(PerspexProperty p) { throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}"); } } }