// 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.Disposables; using System.Reactive.Linq; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Threading; using Avalonia.Utilities; using System.Reactive.Concurrency; namespace Avalonia { /// /// An object with support. /// /// /// This class is analogous to DependencyObject in WPF. /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner { /// /// The parent object that inherited values are inherited from. /// private IAvaloniaObject _inheritanceParent; /// /// The set values/bindings on this object. /// private readonly Dictionary _values = new Dictionary(); /// /// Maintains a list of direct property binding subscriptions so that the binding source /// doesn't get collected. /// private List _directBindings; /// /// Event handler for implementation. /// private PropertyChangedEventHandler _inpcChanged; /// /// Event handler for implementation. /// private EventHandler _propertyChanged; /// /// Initializes a new instance of the class. /// public AvaloniaObject() { foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(this)) { object value = property.IsDirect ? ((IDirectPropertyAccessor)property).GetValue(this) : ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); var e = new AvaloniaPropertyChangedEventArgs( this, property, AvaloniaProperty.UnsetValue, value, BindingPriority.Unset); property.NotifyInitialized(e); } } /// /// 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; } } /// /// Gets or sets the parent object that inherited values /// are inherited from. /// /// /// The inheritance parent. /// protected IAvaloniaObject InheritanceParent { get { return _inheritanceParent; } set { if (_inheritanceParent != value) { if (_inheritanceParent != null) { _inheritanceParent.PropertyChanged -= ParentPropertyChanged; } var inherited = (from property in AvaloniaPropertyRegistry.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[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); } /// /// 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 { if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } return GetValueInternal(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 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(); PriorityValue value; if (_values.TryGetValue(property, out value)) { return value.Value != AvaloniaProperty.UnsetValue; } return 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); var scheduler = AvaloniaLocator.Current.GetService() ?? ImmediateScheduler.Instance; source = source.ObserveOn(scheduler); 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); IDisposable subscription = null; if (_directBindings == null) { _directBindings = new List(); } subscription = source .Select(x => CastOrDefault(x, property.PropertyType)) .Do(_ => { }, () => _directBindings.Remove(subscription)) .Subscribe(x => SetDirectValue(property, x)); _directBindings.Add(subscription); return Disposable.Create(() => { subscription.Dispose(); _directBindings.Remove(subscription); }); } else { PriorityValue v; if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } if (!_values.TryGetValue(property, out v)) { v = CreatePriorityValue(property); _values.Add(property, v); } Logger.Verbose( LogArea.Property, this, "Bound {Property} to {Binding} with priority {Priority}", property, description, 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( 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(); PriorityValue value; if (_values.TryGetValue(property, out value)) { value.Revalidate(); } } /// void IPriorityValueOwner.Changed(PriorityValue sender, object oldValue, object newValue) { var property = sender.Property; var priority = (BindingPriority)sender.ValuePriority; oldValue = (oldValue == AvaloniaProperty.UnsetValue) ? GetDefaultValue(property) : oldValue; newValue = (newValue == AvaloniaProperty.UnsetValue) ? GetDefaultValue(property) : newValue; if (!Equals(oldValue, newValue)) { RaisePropertyChanged(property, oldValue, newValue, priority); Logger.Verbose( LogArea.Property, this, "{Property} changed from {$Old} to {$Value} with priority {Priority}", property, oldValue, newValue, priority); } } /// void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification) { UpdateDataValidation(sender.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() { return _values; } /// /// 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); } }); } /// /// 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 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); } } 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 (!object.Equals(field, value)) { var old = field; field = value; RaisePropertyChanged(property, old, value, BindingPriority.LocalValue); return true; } else { return false; } } /// /// 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; } } /// /// Creates a for a . /// /// The property. /// The . private PriorityValue CreatePriorityValue(AvaloniaProperty property) { var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(GetType()); Func validate2 = null; if (validate != null) { validate2 = v => validate(this, v); } PriorityValue result = new PriorityValue( this, property, property.PropertyType, validate2); return result; } /// /// 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.GetValueInternal(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } /// /// Gets a value /// without check for registered as this can slow getting the value /// this method is intended for internal usage in AvaloniaObject only /// it's called only after check the property is registered /// /// The property. /// The value. private object GetValueInternal(AvaloniaProperty property) { object result = AvaloniaProperty.UnsetValue; PriorityValue value; if (_values.TryGetValue(property, out value)) { result = value.Value; } if (result == AvaloniaProperty.UnsetValue) { result = GetDefaultValue(property); } return result; } /// /// Sets the value of a direct property. /// /// The property. /// The value. private void SetDirectValue(AvaloniaProperty property, object value) { var notification = value as BindingNotification; if (notification != null) { notification.LogIfError(this, property); 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); } } /// /// 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 (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) { ThrowNotRegistered(property); } 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)")); } PriorityValue v; if (!_values.TryGetValue(property, out v)) { if (value == AvaloniaProperty.UnsetValue) { return; } v = CreatePriorityValue(property); _values.Add(property, v); } LogPropertySet(property, value, priority); v.SetValue(value, (int)priority); } /// /// Given a returns a registered avalonia property that is /// equal or throws if not found. /// /// The property. /// The registered property. public AvaloniaProperty GetRegistered(AvaloniaProperty property) { var result = AvaloniaPropertyRegistry.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, 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 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); } /// /// Throws an exception indicating that the specified property is not registered on this /// object. /// /// The property private void ThrowNotRegistered(AvaloniaProperty p) { throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}"); } } }