using System; using System.Collections.Generic; using System.ComponentModel; using Avalonia.Data; using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.PropertyStore; using Avalonia.Reactive; using Avalonia.Threading; namespace Avalonia { /// /// An object with support. /// /// /// This class is analogous to DependencyObject in WPF. /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { private AvaloniaObject? _inheritanceParent; private List? _directBindings; private PropertyChangedEventHandler? _inpcChanged; private EventHandler? _propertyChanged; private List? _inheritanceChildren; private ValueStore? _values; private bool _batchUpdate; /// /// Initializes a new instance of the class. /// public AvaloniaObject() { VerifyAccess(); } /// /// 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 AvaloniaObject? InheritanceParent { get { return _inheritanceParent; } set { VerifyAccess(); if (_inheritanceParent != value) { var oldParent = _inheritanceParent; var valuestore = _values; _inheritanceParent?.RemoveInheritanceChild(this); _inheritanceParent = value; var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()); var propertiesCount = properties.Count; for (var i = 0; i < propertiesCount; i++) { var property = properties[i]; if (valuestore?.IsSet(property) == true) { // If local value set there can be no change. continue; } property.RouteInheritanceParentChanged(this, oldParent); } _inheritanceParent?.AddInheritanceChild(this); } } } /// /// 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 { this.Bind(binding.Property!, value); } } private ValueStore Values { get { if (_values is null) { _values = new ValueStore(this); if (_batchUpdate) _values.BeginBatchUpdate(); } return _values; } } public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); /// /// Clears a 's local value. /// /// The property. public void ClearValue(AvaloniaProperty property) { property = property ?? throw new ArgumentNullException(nameof(property)); property.RouteClearValue(this); } /// /// Clears a 's local value. /// /// The property. public void ClearValue(AvaloniaProperty property) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); switch (property) { case StyledPropertyBase styled: ClearValue(styled); break; case DirectPropertyBase direct: ClearValue(direct); break; default: throw new NotSupportedException("Unsupported AvaloniaProperty type."); } } /// /// Clears a 's local value. /// /// The property. public void ClearValue(StyledPropertyBase property) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); _values?.ClearLocalValue(property); } /// /// Clears a 's local value. /// /// The property. public void ClearValue(DirectPropertyBase property) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); p.InvokeSetter(this, p.GetUnsetValue(GetType())); } /// /// 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) { property = property ?? throw new ArgumentNullException(nameof(property)); return property.RouteGetValue(this); } /// /// Gets a value. /// /// The type of the property. /// The property. /// The value. public T GetValue(StyledPropertyBase property) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); return GetValueOrInheritedOrDefault(property); } /// /// Gets a value. /// /// The type of the property. /// The property. /// The value. public T GetValue(DirectPropertyBase property) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); var registered = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); return registered.InvokeGetter(this); } /// public Optional GetBaseValue(StyledPropertyBase property, BindingPriority maxPriority) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); if (_values is object && _values.TryGetValue(property, maxPriority, out var value)) { return value; } return default; } /// /// Checks whether a is animating. /// /// The property. /// True if the property is animating, otherwise false. public bool IsAnimating(AvaloniaProperty property) { property = property ?? throw new ArgumentNullException(nameof(property)); 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) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); return _values?.IsSet(property) ?? false; } /// /// Sets a value. /// /// The property. /// The value. /// The priority of the value. public IDisposable? SetValue( AvaloniaProperty property, object? value, BindingPriority priority = BindingPriority.LocalValue) { property = property ?? throw new ArgumentNullException(nameof(property)); return property.RouteSetValue(this, value, priority); } /// /// Sets a value. /// /// The type of the property. /// The property. /// The value. /// The priority of the value. /// /// An if setting the property can be undone, otherwise null. /// public IDisposable? SetValue( StyledPropertyBase property, T value, BindingPriority priority = BindingPriority.LocalValue) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); LogPropertySet(property, value, priority); if (value is UnsetValueType) { if (priority == BindingPriority.LocalValue) { Values.ClearLocalValue(property); } else { throw new NotSupportedException( "Cannot set property to Unset at non-local value priority."); } } else if (!(value is DoNothingType)) { return Values.SetValue(property, value, priority); } return null; } /// /// Sets a value. /// /// The type of the property. /// The property. /// The value. public void SetValue(DirectPropertyBase property, T value) { property = property ?? throw new ArgumentNullException(nameof(property)); VerifyAccess(); LogPropertySet(property, value, BindingPriority.LocalValue); SetDirectValueUnchecked(property, value); } /// /// 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) { property = property ?? throw new ArgumentNullException(nameof(property)); source = source ?? throw new ArgumentNullException(nameof(source)); return property.RouteBind(this, source.ToBindingValue(), 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( StyledPropertyBase property, IObservable> source, BindingPriority priority = BindingPriority.LocalValue) { property = property ?? throw new ArgumentNullException(nameof(property)); source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); return Values.AddBinding(property, source, priority); } /// /// Binds a to an observable. /// /// The type of the property. /// The property. /// The observable. /// /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( DirectPropertyBase property, IObservable> source) { property = property ?? throw new ArgumentNullException(nameof(property)); source = source ?? throw new ArgumentNullException(nameof(source)); VerifyAccess(); property = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); if (property.IsReadOnly) { throw new ArgumentException($"The property {property.Name} is readonly."); } Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( this, "Bound {Property} to {Binding} with priority LocalValue", property, GetDescription(source)); _directBindings ??= new List(); return new DirectBindingSubscription(this, property, source); } /// /// Coerces the specified . /// /// The property. public void CoerceValue(AvaloniaProperty property) { _values?.CoerceValue(property); } public void BeginBatchUpdate() { if (_batchUpdate) { throw new InvalidOperationException("Batch update already in progress."); } _batchUpdate = true; _values?.BeginBatchUpdate(); } public void EndBatchUpdate() { if (!_batchUpdate) { throw new InvalidOperationException("No batch update in progress."); } _batchUpdate = false; _values?.EndBatchUpdate(); } /// internal void AddInheritanceChild(AvaloniaObject child) { _inheritanceChildren ??= new List(); _inheritanceChildren.Add(child); } /// internal void RemoveInheritanceChild(AvaloniaObject child) { _inheritanceChildren?.Remove(child); } internal void InheritedPropertyChanged( AvaloniaProperty property, Optional oldValue, Optional newValue) { if (property.Inherits && (_values == null || !_values.IsSet(property))) { RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } } /// Delegate[]? IAvaloniaObjectDebug.GetPropertyChangedSubscribers() { return _propertyChanged?.GetInvocationList(); } internal void ValueChanged(AvaloniaPropertyChangedEventArgs change) { var property = (StyledPropertyBase)change.Property; LogIfError(property, change.NewValue); // If the change is to the effective value of the property and no old/new value is set // then fill in the old/new value from property inheritance/default value. We don't do // this for non-effective value changes because these are only needed for property // transitions, where knowing e.g. that an inherited value is active at an arbitrary // priority isn't of any use and would introduce overhead. if (change.IsEffectiveValueChange && !change.OldValue.HasValue) { change.SetOldValue(GetInheritedOrDefault(property)); } if (change.IsEffectiveValueChange && !change.NewValue.HasValue) { change.SetNewValue(GetInheritedOrDefault(property)); } if (!change.IsEffectiveValueChange || !EqualityComparer.Default.Equals(change.OldValue.Value, change.NewValue.Value)) { RaisePropertyChanged(change); if (change.IsEffectiveValueChange) { Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( this, "{Property} changed from {$Old} to {$Value} with priority {Priority}", property, change.OldValue, change.NewValue, change.Priority); } } } internal void Completed( StyledPropertyBase property, IPriorityValueEntry entry, Optional oldValue) { var change = new AvaloniaPropertyChangedEventArgs( this, property, oldValue, default, BindingPriority.Unset); ValueChanged(change); } /// /// Called for each inherited property when the changes. /// /// The type of the property value. /// The property. /// The old inheritance parent. internal void InheritanceParentChanged( StyledPropertyBase property, AvaloniaObject? oldParent) { var oldValue = oldParent is not null ? oldParent.GetValueOrInheritedOrDefault(property) : property.GetDefaultValue(GetType()); var newValue = GetInheritedOrDefault(property); if (!EqualityComparer.Default.Equals(oldValue, newValue)) { RaisePropertyChanged(property, oldValue, newValue); } } internal AvaloniaPropertyValue GetDiagnosticInternal(AvaloniaProperty property) { if (property.IsDirect) { return new AvaloniaPropertyValue( property, GetValue(property), BindingPriority.Unset, "Local Value"); } else if (_values != null) { var result = _values.GetDiagnostic(property); if (result != null) { return result; } } return new AvaloniaPropertyValue( property, GetValue(property), BindingPriority.Unset, "Unset"); } /// /// 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.TryGet(LogEventLevel.Warning, LogArea.Binding)?.Log( 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 current data binding state. /// The current data binding error, if any. protected virtual void UpdateDataValidation( AvaloniaProperty property, BindingValueType state, Exception? error) { } /// /// Called when a avalonia property changes on the object. /// /// The property change details. protected virtual void OnPropertyChangedCore(AvaloniaPropertyChangedEventArgs change) { if (change.IsEffectiveValueChange) { OnPropertyChanged(change); } } /// /// Called when a avalonia property changes on the object. /// /// The property change details. protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { } /// /// 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, Optional oldValue, BindingValue newValue, BindingPriority priority = BindingPriority.LocalValue) { RaisePropertyChanged(new AvaloniaPropertyChangedEventArgs( this, property, oldValue, newValue, priority)); } /// /// 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; } var old = field; field = value; RaisePropertyChanged(property, old, value); return true; } private T GetInheritedOrDefault(StyledPropertyBase property) { if (property.Inherits && InheritanceParent is AvaloniaObject o) { return o.GetValueOrInheritedOrDefault(property); } return property.GetDefaultValue(GetType()); } private T GetValueOrInheritedOrDefault( StyledPropertyBase property, BindingPriority maxPriority = BindingPriority.Animation) { var o = this; var inherits = property.Inherits; var value = default(T); while (o != null) { var values = o._values; if (values != null && values.TryGetValue(property, maxPriority, out value) == true) { return value; } if (!inherits) { break; } o = o.InheritanceParent as AvaloniaObject; } return property.GetDefaultValue(GetType()); } protected internal void RaisePropertyChanged(AvaloniaPropertyChangedEventArgs change) { VerifyAccess(); if (change.IsEffectiveValueChange) { change.Property.Notifying?.Invoke(this, true); } try { OnPropertyChangedCore(change); if (change.IsEffectiveValueChange) { change.Property.NotifyChanged(change); _propertyChanged?.Invoke(this, change); if (_inpcChanged != null) { var inpce = new PropertyChangedEventArgs(change.Property.Name); _inpcChanged(this, inpce); } if (change.Property.Inherits && _inheritanceChildren != null) { foreach (var child in _inheritanceChildren) { child.InheritedPropertyChanged( change.Property, change.OldValue, change.NewValue.ToOptional()); } } } } finally { if (change.IsEffectiveValueChange) { change.Property.Notifying?.Invoke(this, false); } } } /// /// Sets the value of a direct property. /// /// The property. /// The value. private void SetDirectValueUnchecked(DirectPropertyBase property, T value) { var p = AvaloniaPropertyRegistry.Instance.GetRegisteredDirect(this, property); if (value is UnsetValueType) { p.InvokeSetter(this, p.GetUnsetValue(GetType())); } else if (!(value is DoNothingType)) { p.InvokeSetter(this, value); } } /// /// Sets the value of a direct property. /// /// The property. /// The value. private void SetDirectValueUnchecked(DirectPropertyBase property, BindingValue value) { var p = AvaloniaPropertyRegistry.Instance.FindRegisteredDirect(this, property); if (p == null) { throw new ArgumentException($"Property '{property.Name} not registered on '{this.GetType()}"); } LogIfError(property, value); switch (value.Type) { case BindingValueType.UnsetValue: case BindingValueType.BindingError: var fallback = value.HasValue ? value : value.WithValue(property.GetUnsetValue(GetType())); property.InvokeSetter(this, fallback); break; case BindingValueType.DataValidationError: property.InvokeSetter(this, value); break; case BindingValueType.Value: case BindingValueType.BindingErrorWithFallback: case BindingValueType.DataValidationErrorWithFallback: property.InvokeSetter(this, value); break; } var metadata = p.GetMetadata(GetType()); if (metadata.EnableDataValidation == true) { UpdateDataValidation(property, value.Type, value.Error); } } /// /// Gets a description of an observable that van be used in logs. /// /// The observable. /// The description. private string GetDescription(object o) { var description = o as IDescription; return description?.Description ?? o.ToString() ?? o.GetType().Name; } /// /// Logs a message if the notification represents a binding error. /// /// The property being bound. /// The binding notification. private void LogIfError(AvaloniaProperty property, BindingValue value) { if (value.HasError) { if (value.Error is AggregateException aggregate) { foreach (var inner in aggregate.InnerExceptions) { LogBindingError(property, inner); } } else { LogBindingError(property, value.Error!); } } } /// /// Logs a property set message. /// /// The property. /// The new value. /// The priority. private void LogPropertySet(AvaloniaProperty property, T value, BindingPriority priority) { Logger.TryGet(LogEventLevel.Verbose, LogArea.Property)?.Log( this, "Set {Property} to {$Value} with priority {Priority}", property, value, priority); } private class DirectBindingSubscription : IObserver>, IDisposable { private readonly AvaloniaObject _owner; private readonly DirectPropertyBase _property; private readonly IDisposable _subscription; public DirectBindingSubscription( AvaloniaObject owner, DirectPropertyBase property, IObservable> source) { _owner = owner; _property = property; _owner._directBindings!.Add(this); _subscription = source.Subscribe(this); } public void Dispose() { // _subscription can be null, if Subscribe failed with an exception. _subscription?.Dispose(); _owner._directBindings!.Remove(this); } public void OnCompleted() => Dispose(); public void OnError(Exception error) => Dispose(); public void OnNext(BindingValue value) { if (Dispatcher.UIThread.CheckAccess()) { _owner.SetDirectValueUnchecked(_property, value); } else { // To avoid allocating closure in the outer scope we need to capture variables // locally. This allows us to skip most of the allocations when on UI thread. var instance = _owner; var property = _property; var newValue = value; Dispatcher.UIThread.Post(() => instance.SetDirectValueUnchecked(property, newValue)); } } } } }