// Copyright (c) The Avalonia 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.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; using Avalonia.Utilities; namespace Avalonia { /// /// An object with support. /// /// /// This class is analogous to DependencyObject in WPF. /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { private IAvaloniaObject _inheritanceParent; private List _directBindings; private PropertyChangedEventHandler _inpcChanged; private EventHandler _propertyChanged; private EventHandler _inheritablePropertyChanged; private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); /// /// Initializes a new instance of the class. /// public AvaloniaObject() { VerifyAccess(); AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// /// Raised when a value changes on this object. /// public event EventHandler PropertyChanged { add { _propertyChanged += value; } remove { _propertyChanged -= value; } } /// /// Raised when a value changes on this object. /// event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged { add { _inpcChanged += value; } remove { _inpcChanged -= value; } } /// /// Raised when an inheritable value changes on this object. /// event EventHandler IAvaloniaObject.InheritablePropertyChanged { add { _inheritablePropertyChanged += value; } remove { _inheritablePropertyChanged -= value; } } /// /// Gets or sets the parent object that inherited values /// are inherited from. /// /// /// The inheritance parent. /// protected IAvaloniaObject InheritanceParent { get { return _inheritanceParent; } set { VerifyAccess(); if (_inheritanceParent != value) { if (_inheritanceParent != null) { _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; } var oldInheritanceParent = _inheritanceParent; _inheritanceParent = value; var valuestore = _values; foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) { if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue) { // if local value set there can be no change continue; } // get the value as it would have been with the previous InheritanceParent object oldValue; if (oldInheritanceParent is AvaloniaObject aobj) { oldValue = aobj.GetValueOrDefaultUnchecked(property); } else { oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); } object newValue = GetDefaultValue(property); if (!Equals(oldValue, newValue)) { RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } } if (_inheritanceParent != null) { _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged; } } } } /// /// Gets or sets the value of a . /// /// The property. public object this[AvaloniaProperty property] { get { return GetValue(property); } set { SetValue(property, value); } } /// /// Gets or sets a binding for a . /// /// The binding information. public IBinding this[IndexerDescriptor binding] { get { return new IndexerBinding(this, binding.Property, binding.Mode); } set { var sourceBinding = value as IBinding; this.Bind(binding.Property, sourceBinding); } } public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); /// /// Clears a 's local value. /// /// The property. public void ClearValue(AvaloniaProperty property) { Contract.Requires(property != null); VerifyAccess(); SetValue(property, AvaloniaProperty.UnsetValue); } /// /// Compares two objects using reference equality. /// /// The object to compare. /// /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons: /// /// - AvaloniaObjects are by their nature mutable /// - The presence of attached properties means that the semantics of equality are /// difficult to define /// /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted /// this. /// public sealed override bool Equals(object obj) => base.Equals(obj); /// /// Gets the hash code for the object. /// /// /// Overriding Equals and GetHashCode on an AvaloniaObject is disallowed for two reasons: /// /// - AvaloniaObjects are by their nature mutable /// - The presence of attached properties means that the semantics of equality are /// difficult to define /// /// See https://github.com/AvaloniaUI/Avalonia/pull/2747 for the discussion that prompted /// this. /// public sealed override int GetHashCode() => base.GetHashCode(); /// /// Gets a value. /// /// The property. /// The value. public object GetValue(AvaloniaProperty property) { Contract.Requires(property != null); VerifyAccess(); if (property.IsDirect) { return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); } else { return GetValueOrDefaultUnchecked(property); } } /// /// Gets a value. /// /// The type of the property. /// The property. /// The value. public T GetValue(AvaloniaProperty property) { Contract.Requires(property != null); return (T)GetValue((AvaloniaProperty)property); } /// /// Checks whether a is animating. /// /// The property. /// True if the property is animating, otherwise false. public bool IsAnimating(AvaloniaProperty property) { Contract.Requires(property != null); VerifyAccess(); return _values?.IsAnimating(property) ?? false; } /// /// Checks whether a is set on this object. /// /// The property. /// True if the property is set, otherwise false. /// /// Checks whether a value is assigned to the property, or that there is a binding to the /// property that is producing a value other than . /// public bool IsSet(AvaloniaProperty property) { Contract.Requires(property != null); VerifyAccess(); return _values?.IsSet(property) ?? false; } /// /// Sets a value. /// /// The property. /// The value. /// The priority of the value. public void SetValue( AvaloniaProperty property, object value, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); VerifyAccess(); if (property.IsDirect) { SetDirectValue(property, value); } else { SetStyledValue(property, value, priority); } } /// /// Sets a value. /// /// The type of the property. /// The property. /// The value. /// The priority of the value. public void SetValue( AvaloniaProperty property, T value, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); SetValue((AvaloniaProperty)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( AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); Contract.Requires(source != null); VerifyAccess(); var description = GetDescription(source); if (property.IsDirect) { if (property.IsReadOnly) { throw new ArgumentException($"The property {property.Name} is readonly."); } Logger.Verbose( LogArea.Property, this, "Bound {Property} to {Binding} with priority LocalValue", property, description); if (_directBindings == null) { _directBindings = new List(); } return new DirectBindingSubscription(this, property, source); } else { Logger.Verbose( LogArea.Property, this, "Bound {Property} to {Binding} with priority {Priority}", property, description, priority); return Values.AddBinding(property, source, 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( AvaloniaProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); return Bind(property, source.Select(x => (object)x), priority); } /// /// Forces the specified property to be revalidated. /// /// The property. public void Revalidate(AvaloniaProperty property) { VerifyAccess(); _values?.Revalidate(property); } internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue) { oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? GetDefaultValue(property) : oldValue; newValue = (newValue == AvaloniaProperty.UnsetValue) ? GetDefaultValue(property) : newValue; if (!Equals(oldValue, newValue)) { RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)priority); Logger.Verbose( LogArea.Property, this, "{Property} changed from {$Old} to {$Value} with priority {Priority}", property, oldValue, newValue, (BindingPriority)priority); } } internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { LogIfError(property, notification); UpdateDataValidation(property, notification); } /// Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() { return _propertyChanged?.GetInvocationList(); } /// /// Gets all priority values set on the object. /// /// A collection of property/value tuples. internal IDictionary GetSetValues() => Values?.GetSetValues(); /// /// Forces revalidation of properties when a property value changes. /// /// The property to that affects validation. /// The affected properties. protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected) { property.Changed.Subscribe(e => { foreach (var p in affected) { e.Sender.Revalidate(p); } }); } /// /// Logs a binding error for a property. /// /// The property that the error occurred on. /// The binding error. protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e) { Logger.Log( LogEventLevel.Warning, LogArea.Binding, this, "Error in binding to {Target}.{Property}: {Message}", this, property, e.Message); } /// /// Called to update the validation state for properties for which data validation is /// enabled. /// /// The property. /// The new validation status. protected virtual void UpdateDataValidation( AvaloniaProperty property, BindingNotification status) { } /// /// Called when a avalonia property changes on the object. /// /// The event arguments. protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs 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 internal void RaisePropertyChanged( AvaloniaProperty property, object oldValue, object newValue, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(property != null); VerifyAccess(); AvaloniaPropertyChangedEventArgs e = new AvaloniaPropertyChangedEventArgs( this, property, oldValue, newValue, priority); property.Notifying?.Invoke(this, true); try { OnPropertyChanged(e); property.NotifyChanged(e); _propertyChanged?.Invoke(this, e); if (_inpcChanged != null) { PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); _inpcChanged(this, e2); } if (property.Inherits) { _inheritablePropertyChanged?.Invoke(this, e); } } finally { property.Notifying?.Invoke(this, false); } } /// /// Sets the backing field for a direct avalonia 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(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); if (EqualityComparer.Default.Equals(field, value)) { return false; } DeferredSetter setter = Values.GetDirectDeferredSetter(property); return setter.SetAndNotify(this, property, ref field, value); } /// /// Tries to cast a value to a type, taking into account that the value may be a /// . /// /// The value. /// The type. /// The cast value, or a . private static object CastOrDefault(object value, Type type) { var notification = value as BindingNotification; if (notification == null) { return TypeUtilities.ConvertImplicitOrDefault(value, type); } else { if (notification.HasValue) { notification.SetValue(TypeUtilities.ConvertImplicitOrDefault(notification.Value, type)); } return notification; } } /// /// Gets the default value for a property. /// /// The property. /// The default value. private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) return aobj.GetValueOrDefaultUnchecked(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } /// /// Gets the value or default value for a property. /// /// The property. /// The default value. private object GetValueOrDefaultUnchecked(AvaloniaProperty property) { var aobj = this; var valuestore = aobj._values; if (valuestore != null) { var result = valuestore.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; } } if (property.Inherits) { while (aobj.InheritanceParent is AvaloniaObject parent) { aobj = parent; valuestore = aobj._values; if (valuestore != null) { var result = valuestore.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; } } } } return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); } /// /// Sets the value of a direct property. /// /// The property. /// The value. private void SetDirectValue(AvaloniaProperty property, object value) { void Set() { var notification = value as BindingNotification; if (notification != null) { LogIfError(property, notification); value = notification.Value; } if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue) { var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); var accessor = (IDirectPropertyAccessor)GetRegistered(property); var finalValue = value == AvaloniaProperty.UnsetValue ? metadata.UnsetValue : value; LogPropertySet(property, value, BindingPriority.LocalValue); accessor.SetValue(this, finalValue); } if (notification != null) { UpdateDataValidation(property, notification); } } if (Dispatcher.UIThread.CheckAccess()) { Set(); } else { Dispatcher.UIThread.Post(Set); } } /// /// Sets the value of a styled property. /// /// The property. /// The value. /// The priority of the value. private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority) { var notification = value as BindingNotification; // We currently accept BindingNotifications for non-direct properties but we just // strip them to their underlying value. if (notification != null) { if (!notification.HasValue) { return; } else { value = notification.Value; } } var originalValue = value; if (!TypeUtilities.TryConvertImplicit(property.PropertyType, value, out value)) { throw new ArgumentException(string.Format( "Invalid value for Property '{0}': '{1}' ({2})", property.Name, originalValue, originalValue?.GetType().FullName ?? "(null)")); } LogPropertySet(property, value, priority); Values.AddValue(property, value, (int)priority); } /// /// Given a direct property, returns a registered avalonia property that is equivalent or /// throws if not found. /// /// The property. /// The registered property. private AvaloniaProperty GetRegistered(AvaloniaProperty property) { var direct = property as IDirectPropertyAccessor; if (direct == null) { throw new AvaloniaInternalException( "AvaloniaObject.GetRegistered should only be called for direct properties"); } if (property.OwnerType.IsAssignableFrom(GetType())) { return property; } var result = AvaloniaPropertyRegistry.Instance.GetRegistered(this) .FirstOrDefault(x => x == property); if (result == null) { throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); } 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, AvaloniaPropertyChangedEventArgs 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 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 mesage if the notification represents a binding error. /// /// The property being bound. /// The binding notification. private void LogIfError(AvaloniaProperty property, BindingNotification notification) { if (notification.ErrorType == BindingErrorType.Error) { if (notification.Error is AggregateException aggregate) { foreach (var inner in aggregate.InnerExceptions) { LogBindingError(property, inner); } } else { LogBindingError(property, notification.Error); } } } /// /// Logs a property set message. /// /// The property. /// The new value. /// The priority. private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority) { Logger.Verbose( LogArea.Property, this, "Set {Property} to {$Value} with priority {Priority}", property, value, priority); } private class DirectBindingSubscription : IObserver, IDisposable { readonly AvaloniaObject _owner; readonly AvaloniaProperty _property; IDisposable _subscription; public DirectBindingSubscription( AvaloniaObject owner, AvaloniaProperty property, IObservable source) { _owner = owner; _property = property; _owner._directBindings.Add(this); _subscription = source.Subscribe(this); } public void Dispose() { _subscription.Dispose(); _owner._directBindings.Remove(this); } public void OnCompleted() => Dispose(); public void OnError(Exception error) => Dispose(); public void OnNext(object value) { var castValue = CastOrDefault(value, _property.PropertyType); _owner.SetDirectValue(_property, castValue); } } } }