using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Data;
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.PropertyStore;
using Avalonia.Threading;
namespace Avalonia
{
///
/// An object with support.
///
///
/// This class is analogous to DependencyObject in WPF.
///
public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IValueSink
{
private IAvaloniaObject _inheritanceParent;
private List _directBindings;
private PropertyChangedEventHandler _inpcChanged;
private EventHandler _propertyChanged;
private List _inheritanceChildren;
private ValueStore _values;
private ValueStore Values => _values ?? (_values = new ValueStore(this));
///
/// 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 IAvaloniaObject 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
{
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)
{
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)
{
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)
{
property = property ?? throw new ArgumentNullException(nameof(property));
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 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 type of the property.
/// The property.
public void CoerceValue(StyledPropertyBase property)
{
_values?.CoerceValue(property);
}
///
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{
_inheritanceChildren ??= new List();
_inheritanceChildren.Add(child);
}
///
void IAvaloniaObject.RemoveInheritanceChild(IAvaloniaObject child)
{
_inheritanceChildren?.Remove(child);
}
void IAvaloniaObject.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();
}
void IValueSink.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);
}
}
}
void IValueSink.Completed(
StyledPropertyBase property,
IPriorityValueEntry entry,
Optional oldValue)
{
var change = new AvaloniaPropertyChangedEventArgs(
this,
property,
oldValue,
default,
BindingPriority.Unset);
((IValueSink)this).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,
IAvaloniaObject oldParent)
{
var oldValue = oldParent switch
{
AvaloniaObject o => o.GetValueOrInheritedOrDefault(property),
null => property.GetDefaultValue(GetType()),
_ => oldParent.GetValue(property)
};
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 new binding value for the property.
protected virtual void UpdateDataValidation(
AvaloniaProperty property,
BindingValue value)
{
}
///
/// 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?.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);
}
}
///
/// 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();
}
///
/// Logs a mesage 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.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));
}
}
}
}
}