| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820 |
- // 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
- {
- /// <summary>
- /// An object with <see cref="AvaloniaProperty"/> support.
- /// </summary>
- /// <remarks>
- /// This class is analogous to DependencyObject in WPF.
- /// </remarks>
- public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
- {
- /// <summary>
- /// The parent object that inherited values are inherited from.
- /// </summary>
- private IAvaloniaObject _inheritanceParent;
- /// <summary>
- /// The set values/bindings on this object.
- /// </summary>
- private readonly Dictionary<AvaloniaProperty, PriorityValue> _values =
- new Dictionary<AvaloniaProperty, PriorityValue>();
- /// <summary>
- /// Maintains a list of direct property binding subscriptions so that the binding source
- /// doesn't get collected.
- /// </summary>
- private List<IDisposable> _directBindings;
- /// <summary>
- /// Event handler for <see cref="INotifyPropertyChanged"/> implementation.
- /// </summary>
- private PropertyChangedEventHandler _inpcChanged;
- /// <summary>
- /// Event handler for <see cref="PropertyChanged"/> implementation.
- /// </summary>
- private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
- /// <summary>
- /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
- /// </summary>
- 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);
- }
- }
- /// <summary>
- /// Raised when a <see cref="AvaloniaProperty"/> value changes on this object.
- /// </summary>
- public event EventHandler<AvaloniaPropertyChangedEventArgs> PropertyChanged
- {
- add { _propertyChanged += value; }
- remove { _propertyChanged -= value; }
- }
- /// <summary>
- /// Raised when a <see cref="AvaloniaProperty"/> value changes on this object.
- /// </summary>
- event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
- {
- add { _inpcChanged += value; }
- remove { _inpcChanged -= value; }
- }
- /// <summary>
- /// Gets or sets the parent object that inherited <see cref="AvaloniaProperty"/> values
- /// are inherited from.
- /// </summary>
- /// <value>
- /// The inheritance parent.
- /// </value>
- 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;
- }
- }
- }
- }
- /// <summary>
- /// Gets or sets the value of a <see cref="AvaloniaProperty"/>.
- /// </summary>
- /// <param name="property">The property.</param>
- public object this[AvaloniaProperty property]
- {
- get { return GetValue(property); }
- set { SetValue(property, value); }
- }
- /// <summary>
- /// Gets or sets a binding for a <see cref="AvaloniaProperty"/>.
- /// </summary>
- /// <param name="binding">The binding information.</param>
- 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();
- /// <summary>
- /// Clears a <see cref="AvaloniaProperty"/>'s local value.
- /// </summary>
- /// <param name="property">The property.</param>
- public void ClearValue(AvaloniaProperty property)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- VerifyAccess();
- SetValue(property, AvaloniaProperty.UnsetValue);
- }
- /// <summary>
- /// Gets a <see cref="AvaloniaProperty"/> value.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <returns>The value.</returns>
- public object GetValue(AvaloniaProperty property)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- VerifyAccess();
- if (property.IsDirect)
- {
- return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this);
- }
- else
- {
- if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property))
- {
- ThrowNotRegistered(property);
- }
- return GetValueInternal(property);
- }
- }
- /// <summary>
- /// Gets a <see cref="AvaloniaProperty"/> value.
- /// </summary>
- /// <typeparam name="T">The type of the property.</typeparam>
- /// <param name="property">The property.</param>
- /// <returns>The value.</returns>
- public T GetValue<T>(AvaloniaProperty<T> property)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- return (T)GetValue((AvaloniaProperty)property);
- }
- /// <summary>
- /// Checks whether a <see cref="AvaloniaProperty"/> is set on this object.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <returns>True if the property is set, otherwise false.</returns>
- /// <remarks>
- /// 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 <see cref="AvaloniaProperty.UnsetValue"/>.
- /// </remarks>
- public bool IsSet(AvaloniaProperty property)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- VerifyAccess();
- PriorityValue value;
- if (_values.TryGetValue(property, out value))
- {
- return value.Value != AvaloniaProperty.UnsetValue;
- }
- return false;
- }
- /// <summary>
- /// Sets a <see cref="AvaloniaProperty"/> value.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <param name="value">The value.</param>
- /// <param name="priority">The priority of the value.</param>
- public void SetValue(
- AvaloniaProperty property,
- object value,
- BindingPriority priority = BindingPriority.LocalValue)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- VerifyAccess();
- if (property.IsDirect)
- {
- SetDirectValue(property, value);
- }
- else
- {
- SetStyledValue(property, value, priority);
- }
- }
- /// <summary>
- /// Sets a <see cref="AvaloniaProperty"/> value.
- /// </summary>
- /// <typeparam name="T">The type of the property.</typeparam>
- /// <param name="property">The property.</param>
- /// <param name="value">The value.</param>
- /// <param name="priority">The priority of the value.</param>
- public void SetValue<T>(
- AvaloniaProperty<T> property,
- T value,
- BindingPriority priority = BindingPriority.LocalValue)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- SetValue((AvaloniaProperty)property, value, priority);
- }
- /// <summary>
- /// Binds a <see cref="AvaloniaProperty"/> to an observable.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <param name="source">The observable.</param>
- /// <param name="priority">The priority of the binding.</param>
- /// <returns>
- /// A disposable which can be used to terminate the binding.
- /// </returns>
- public IDisposable Bind(
- AvaloniaProperty property,
- IObservable<object> source,
- BindingPriority priority = BindingPriority.LocalValue)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- Contract.Requires<ArgumentNullException>(source != null);
- VerifyAccess();
- var description = GetDescription(source);
- var scheduler = AvaloniaLocator.Current.GetService<IScheduler>() ?? 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<IDisposable>();
- }
- 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);
- }
- }
- /// <summary>
- /// Binds a <see cref="AvaloniaProperty"/> to an observable.
- /// </summary>
- /// <typeparam name="T">The type of the property.</typeparam>
- /// <param name="property">The property.</param>
- /// <param name="source">The observable.</param>
- /// <param name="priority">The priority of the binding.</param>
- /// <returns>
- /// A disposable which can be used to terminate the binding.
- /// </returns>
- public IDisposable Bind<T>(
- AvaloniaProperty<T> property,
- IObservable<T> source,
- BindingPriority priority = BindingPriority.LocalValue)
- {
- Contract.Requires<ArgumentNullException>(property != null);
- return Bind(property, source.Select(x => (object)x), priority);
- }
- /// <summary>
- /// Forces the specified property to be revalidated.
- /// </summary>
- /// <param name="property">The property.</param>
- public void Revalidate(AvaloniaProperty property)
- {
- VerifyAccess();
- PriorityValue value;
- if (_values.TryGetValue(property, out value))
- {
- value.Revalidate();
- }
- }
- /// <inheritdoc/>
- 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);
- }
- }
- /// <inheritdoc/>
- void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification)
- {
- UpdateDataValidation(sender.Property, notification);
- }
- /// <inheritdoc/>
- Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
- {
- return _propertyChanged?.GetInvocationList();
- }
- /// <summary>
- /// Gets all priority values set on the object.
- /// </summary>
- /// <returns>A collection of property/value tuples.</returns>
- internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues()
- {
- return _values;
- }
- /// <summary>
- /// Forces revalidation of properties when a property value changes.
- /// </summary>
- /// <param name="property">The property to that affects validation.</param>
- /// <param name="affected">The affected properties.</param>
- protected static void AffectsValidation(AvaloniaProperty property, params AvaloniaProperty[] affected)
- {
- property.Changed.Subscribe(e =>
- {
- foreach (var p in affected)
- {
- e.Sender.Revalidate(p);
- }
- });
- }
- /// <summary>
- /// Called to update the validation state for properties for which data validation is
- /// enabled.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <param name="status">The new validation status.</param>
- protected virtual void UpdateDataValidation(
- AvaloniaProperty property,
- BindingNotification status)
- {
- }
- /// <summary>
- /// Called when a avalonia property changes on the object.
- /// </summary>
- /// <param name="e">The event arguments.</param>
- protected virtual void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
- {
- }
- /// <summary>
- /// Raises the <see cref="PropertyChanged"/> event.
- /// </summary>
- /// <param name="property">The property that has changed.</param>
- /// <param name="oldValue">The old property value.</param>
- /// <param name="newValue">The new property value.</param>
- /// <param name="priority">The priority of the binding that produced the value.</param>
- protected void RaisePropertyChanged(
- AvaloniaProperty property,
- object oldValue,
- object newValue,
- BindingPriority priority = BindingPriority.LocalValue)
- {
- Contract.Requires<ArgumentNullException>(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);
- }
- }
- /// <summary>
- /// Sets the backing field for a direct avalonia property, raising the
- /// <see cref="PropertyChanged"/> event if the value has changed.
- /// </summary>
- /// <typeparam name="T">The type of the property.</typeparam>
- /// <param name="property">The property.</param>
- /// <param name="field">The backing field.</param>
- /// <param name="value">The value.</param>
- /// <returns>
- /// True if the value changed, otherwise false.
- /// </returns>
- protected bool SetAndRaise<T>(AvaloniaProperty<T> 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;
- }
- }
- /// <summary>
- /// Tries to cast a value to a type, taking into account that the value may be a
- /// <see cref="BindingNotification"/>.
- /// </summary>
- /// <param name="value">The value.</param>
- /// <param name="type">The type.</param>
- /// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
- 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;
- }
- }
- /// <summary>
- /// Creates a <see cref="PriorityValue"/> for a <see cref="AvaloniaProperty"/>.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <returns>The <see cref="PriorityValue"/>.</returns>
- private PriorityValue CreatePriorityValue(AvaloniaProperty property)
- {
- var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(GetType());
- Func<object, object> validate2 = null;
- if (validate != null)
- {
- validate2 = v => validate(this, v);
- }
- PriorityValue result = new PriorityValue(
- this,
- property,
- property.PropertyType,
- validate2);
- return result;
- }
- /// <summary>
- /// Gets the default value for a property.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <returns>The default value.</returns>
- private object GetDefaultValue(AvaloniaProperty property)
- {
- if (property.Inherits && _inheritanceParent is AvaloniaObject aobj)
- return aobj.GetValueInternal(property);
- return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType());
- }
- /// <summary>
- /// Gets a <see cref="AvaloniaProperty"/> 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
- /// </summary>
- /// <param name="property">The property.</param>
- /// <returns>The value.</returns>
- 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;
- }
- /// <summary>
- /// Sets the value of a direct property.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <param name="value">The value.</param>
- 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);
- }
- }
- /// <summary>
- /// Sets the value of a styled property.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <param name="value">The value.</param>
- /// <param name="priority">The priority of the value.</param>
- 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);
- }
- /// <summary>
- /// Given a <see cref="AvaloniaProperty"/> returns a registered avalonia property that is
- /// equal or throws if not found.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <returns>The registered property.</returns>
- public AvaloniaProperty GetRegistered(AvaloniaProperty property)
- {
- var result = AvaloniaPropertyRegistry.Instance.FindRegistered(this, property);
- if (result == null)
- {
- ThrowNotRegistered(property);
- }
- return result;
- }
- /// <summary>
- /// Called when a property is changed on the current <see cref="InheritanceParent"/>.
- /// </summary>
- /// <param name="sender">The event sender.</param>
- /// <param name="e">The event args.</param>
- /// <remarks>
- /// Checks for changes in an inherited property value.
- /// </remarks>
- private void ParentPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
- {
- Contract.Requires<ArgumentNullException>(e != null);
- if (e.Property.Inherits && !IsSet(e.Property))
- {
- RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue);
- }
- }
- /// <summary>
- /// Gets a description of an observable that van be used in logs.
- /// </summary>
- /// <param name="o">The observable.</param>
- /// <returns>The description.</returns>
- private string GetDescription(IObservable<object> o)
- {
- var description = o as IDescription;
- return description?.Description ?? o.ToString();
- }
- /// <summary>
- /// Logs a property set message.
- /// </summary>
- /// <param name="property">The property.</param>
- /// <param name="value">The new value.</param>
- /// <param name="priority">The priority.</param>
- private void LogPropertySet(AvaloniaProperty property, object value, BindingPriority priority)
- {
- Logger.Verbose(
- LogArea.Property,
- this,
- "Set {Property} to {$Value} with priority {Priority}",
- property,
- value,
- priority);
- }
- /// <summary>
- /// Throws an exception indicating that the specified property is not registered on this
- /// object.
- /// </summary>
- /// <param name="p">The property</param>
- private void ThrowNotRegistered(AvaloniaProperty p)
- {
- throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}");
- }
- }
- }
|