using System; using System.Collections.Generic; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Utilities; namespace Avalonia { /// /// Base class for avalonia properties. /// public abstract class AvaloniaProperty : IEquatable, IPropertyInfo { /// /// Represents an unset property value. /// public static readonly object UnsetValue = new UnsetValueType(); private static int s_nextId; private readonly AvaloniaPropertyMetadata _defaultMetadata; private readonly Dictionary _metadata; private readonly Dictionary _metadataCache = new Dictionary(); private bool _hasMetadataOverrides; /// /// Initializes a new instance of the class. /// /// The name of the property. /// The type of the property's value. /// The type of the class that registers the property. /// The property metadata. /// A callback. protected AvaloniaProperty( string name, Type valueType, Type ownerType, AvaloniaPropertyMetadata metadata, Action? notifying = null) { _ = name ?? throw new ArgumentNullException(nameof(name)); if (name.Contains(".")) { throw new ArgumentException("'name' may not contain periods."); } _metadata = new Dictionary(); Name = name; PropertyType = valueType ?? throw new ArgumentNullException(nameof(valueType)); OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType)); Notifying = notifying; Id = s_nextId++; _metadata.Add(ownerType, metadata ?? throw new ArgumentNullException(nameof(metadata))); _defaultMetadata = metadata; } /// /// Initializes a new instance of the class. /// /// The direct property to copy. /// The new owner type. /// Optional overridden metadata. protected AvaloniaProperty( AvaloniaProperty source, Type ownerType, AvaloniaPropertyMetadata? metadata) { _metadata = new Dictionary(); Name = source?.Name ?? throw new ArgumentNullException(nameof(source)); PropertyType = source.PropertyType; OwnerType = ownerType ?? throw new ArgumentNullException(nameof(ownerType)); Notifying = source.Notifying; Id = source.Id; _defaultMetadata = source._defaultMetadata; // Properties that have different owner can't use fast path for metadata. _hasMetadataOverrides = true; if (metadata != null) { _metadata.Add(ownerType, metadata); } } /// /// Gets the name of the property. /// public string Name { get; } /// /// Gets the type of the property's value. /// public Type PropertyType { get; } /// /// Gets the type of the class that registered the property. /// public Type OwnerType { get; } /// /// Gets a value indicating whether the property inherits its value. /// public virtual bool Inherits => false; /// /// Gets a value indicating whether this is an attached property. /// public virtual bool IsAttached => false; /// /// Gets a value indicating whether this is a direct property. /// public virtual bool IsDirect => false; /// /// Gets a value indicating whether this is a readonly property. /// public virtual bool IsReadOnly => false; /// /// Gets an observable that is fired when this property changes on any /// instance. /// /// /// An observable that is fired when this property changes on any /// instance. /// public IObservable Changed => GetChanged(); /// /// Gets a method that gets called before and after the property starts being notified on an /// object. /// /// /// When a property changes, change notifications are sent to all property subscribers; /// for example via the observable and and the /// event. If this callback is set for a property, /// then it will be called before and after these notifications take place. The bool argument /// will be true before the property change notifications are sent and false afterwards. This /// callback is intended to support Control.IsDataContextChanging. /// public Action? Notifying { get; } /// /// Gets the integer ID that represents this property. /// internal int Id { get; } /// /// Provides access to a property's binding via the /// indexer. /// /// The property. /// A describing the binding. public static IndexerDescriptor operator !(AvaloniaProperty property) { return new IndexerDescriptor { Priority = BindingPriority.LocalValue, Property = property, }; } /// /// Provides access to a property's template binding via the /// indexer. /// /// The property. /// A describing the binding. public static IndexerDescriptor operator ~(AvaloniaProperty property) { return new IndexerDescriptor { Priority = BindingPriority.TemplatedParent, Property = property, }; } /// /// Tests two s for equality. /// /// The first property. /// The second property. /// True if the properties are equal, otherwise false. public static bool operator ==(AvaloniaProperty? a, AvaloniaProperty? b) { if (object.ReferenceEquals(a, b)) { return true; } else if (a is null || b is null) { return false; } else { return a.Equals(b); } } /// /// Tests two s for inequality. /// /// The first property. /// The second property. /// True if the properties are equal, otherwise false. public static bool operator !=(AvaloniaProperty? a, AvaloniaProperty? b) { return !(a == b); } /// /// Registers a . /// /// The type of the class that is registering the property. /// The type of the property's value. /// The name of the property. /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. /// A value validation callback. /// A value coercion callback. /// /// A method that gets called before and after the property starts being notified on an /// object; the bool argument will be true before and false afterwards. This callback is /// intended to support IsDataContextChanging. /// /// A public static StyledProperty Register( string name, TValue defaultValue = default!, bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func? validate = null, Func? coerce = null, Action? notifying = null) where TOwner : IAvaloniaObject { _ = name ?? throw new ArgumentNullException(nameof(name)); var metadata = new StyledPropertyMetadata( defaultValue, defaultBindingMode: defaultBindingMode, coerce: coerce); var result = new StyledProperty( name, typeof(TOwner), metadata, inherits, validate, notifying); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } /// /// Registers an attached . /// /// The type of the class that is registering the property. /// The type of the class that the property is to be registered on. /// The type of the property's value. /// The name of the property. /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. /// A value validation callback. /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, TValue defaultValue = default!, bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func? validate = null, Func? coerce = null) where THost : IAvaloniaObject { _ = name ?? throw new ArgumentNullException(nameof(name)); var metadata = new StyledPropertyMetadata( defaultValue, defaultBindingMode: defaultBindingMode, coerce: coerce); var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(typeof(TOwner), result); registry.RegisterAttached(typeof(THost), result); return result; } /// /// Registers an attached . /// /// The type of the class that the property is to be registered on. /// The type of the property's value. /// The name of the property. /// The type of the class that is registering the property. /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. /// A value validation callback. /// A value coercion callback. /// A public static AttachedProperty RegisterAttached( string name, Type ownerType, TValue defaultValue = default!, bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func? validate = null, Func? coerce = null) where THost : IAvaloniaObject { _ = name ?? throw new ArgumentNullException(nameof(name)); var metadata = new StyledPropertyMetadata( defaultValue, defaultBindingMode: defaultBindingMode, coerce: coerce); var result = new AttachedProperty(name, ownerType, metadata, inherits, validate); var registry = AvaloniaPropertyRegistry.Instance; registry.Register(ownerType, result); registry.RegisterAttached(typeof(THost), result); return result; } /// /// Registers a direct . /// /// The type of the class that is registering the property. /// The type of the property's value. /// The name of the property. /// Gets the current value of the property. /// Sets the value of the property. /// The value to use when the property is cleared. /// The default binding mode for the property. /// /// Whether the property is interested in data validation. /// /// A public static DirectProperty RegisterDirect( string name, Func getter, Action? setter = null, TValue unsetValue = default!, BindingMode defaultBindingMode = BindingMode.OneWay, bool enableDataValidation = false) where TOwner : IAvaloniaObject { _ = name ?? throw new ArgumentNullException(nameof(name)); _ = getter ?? throw new ArgumentNullException(nameof(getter)); var metadata = new DirectPropertyMetadata( unsetValue: unsetValue, defaultBindingMode: defaultBindingMode, enableDataValidation: enableDataValidation); var result = new DirectProperty( name, getter, setter, metadata); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } /// /// Returns a binding accessor that can be passed to 's [] /// operator to initiate a binding. /// /// A . /// /// The ! and ~ operators are short forms of this. /// public IndexerDescriptor Bind() { return new IndexerDescriptor { Property = this, }; } /// public override bool Equals(object? obj) { var p = obj as AvaloniaProperty; return p is not null && Equals(p); } /// public bool Equals(AvaloniaProperty? other) { return Id == other?.Id; } /// public override int GetHashCode() { return Id; } /// /// Gets the property metadata for the specified type. /// /// The type. /// /// The property metadata. /// public AvaloniaPropertyMetadata GetMetadata() where T : IAvaloniaObject { return GetMetadata(typeof(T)); } /// /// Gets the property metadata for the specified type. /// /// The type. /// /// The property metadata. /// public AvaloniaPropertyMetadata GetMetadata(Type type) { if (!_hasMetadataOverrides) { return _defaultMetadata; } return GetMetadataWithOverrides(type); } /// /// Checks whether the is valid for the property. /// /// The value. /// True if the value is valid, otherwise false. public bool IsValidValue(object? value) { return TypeUtilities.TryConvertImplicit(PropertyType, value, out _); } /// /// Gets the string representation of the property. /// /// The property's string representation. public override string ToString() { return Name; } /// /// Uses the visitor pattern to resolve an untyped property to a typed property. /// /// The type of user data passed. /// The visitor which will accept the typed property. /// The user data to pass. public abstract void Accept(IAvaloniaPropertyVisitor visitor, ref TData data) where TData : struct; /// /// Routes an untyped ClearValue call to a typed call. /// /// The object instance. internal abstract void RouteClearValue(AvaloniaObject o); /// /// Routes an untyped GetValue call to a typed call. /// /// The object instance. internal abstract object? RouteGetValue(AvaloniaObject o); /// /// Routes an untyped GetBaseValue call to a typed call. /// /// The object instance. /// The maximum priority for the value. internal abstract object? RouteGetBaseValue(AvaloniaObject o, BindingPriority maxPriority); /// /// Routes an untyped SetValue call to a typed call. /// /// The object instance. /// The value. /// The priority. /// /// An if setting the property can be undone, otherwise null. /// internal abstract IDisposable? RouteSetValue( AvaloniaObject o, object? value, BindingPriority priority); /// /// Routes an untyped Bind call to a typed call. /// /// The object instance. /// The binding source. /// The priority. internal abstract IDisposable RouteBind( AvaloniaObject o, IObservable> source, BindingPriority priority); internal abstract void RouteInheritanceParentChanged(AvaloniaObject o, AvaloniaObject? oldParent); /// /// Overrides the metadata for the property on the specified type. /// /// The type. /// The metadata. protected void OverrideMetadata(Type type, AvaloniaPropertyMetadata metadata) { _ = type ?? throw new ArgumentNullException(nameof(type)); _ = metadata ?? throw new ArgumentNullException(nameof(metadata)); if (_metadata.ContainsKey(type)) { throw new InvalidOperationException( $"Metadata is already set for {Name} on {type}."); } var baseMetadata = GetMetadata(type); metadata.Merge(baseMetadata, this); _metadata.Add(type, metadata); _metadataCache.Clear(); _hasMetadataOverrides = true; } protected abstract IObservable GetChanged(); private AvaloniaPropertyMetadata GetMetadataWithOverrides(Type type) { if (type is null) { throw new ArgumentNullException(nameof(type)); } if (_metadataCache.TryGetValue(type, out var result)) { return result; } Type? currentType = type; while (currentType != null) { if (_metadata.TryGetValue(currentType, out result)) { _metadataCache[type] = result; return result; } currentType = currentType.BaseType; } _metadataCache[type] = _defaultMetadata; return _defaultMetadata; } bool IPropertyInfo.CanGet => true; bool IPropertyInfo.CanSet => true; object? IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this); void IPropertyInfo.Set(object target, object? value) => ((AvaloniaObject)target).SetValue(this, value); } /// /// Class representing the . /// public sealed class UnsetValueType { internal UnsetValueType() { } /// /// Returns the string representation of the . /// /// The string "(unset)". public override string ToString() => "(unset)"; } }