Browse Source
Major refactor of the Avalonia core to make the styled property store typed.pull/3258/head
74 changed files with 3506 additions and 2103 deletions
@ -0,0 +1,67 @@ |
|||
// 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 Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Provides information for a avalonia property change.
|
|||
/// </summary>
|
|||
public class AvaloniaPropertyChangedEventArgs<T> : AvaloniaPropertyChangedEventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AvaloniaPropertyChangedEventArgs"/> class.
|
|||
/// </summary>
|
|||
/// <param name="sender">The object that the property changed on.</param>
|
|||
/// <param name="property">The property that changed.</param>
|
|||
/// <param name="oldValue">The old value of the property.</param>
|
|||
/// <param name="newValue">The new value of the property.</param>
|
|||
/// <param name="priority">The priority of the binding that produced the value.</param>
|
|||
public AvaloniaPropertyChangedEventArgs( |
|||
IAvaloniaObject sender, |
|||
AvaloniaProperty<T> property, |
|||
Optional<T> oldValue, |
|||
BindingValue<T> newValue, |
|||
BindingPriority priority) |
|||
: base(sender, priority) |
|||
{ |
|||
Property = property; |
|||
OldValue = oldValue; |
|||
NewValue = newValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the property that changed.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The property that changed.
|
|||
/// </value>
|
|||
public new AvaloniaProperty<T> Property { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the old value of the property.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The old value of the property.
|
|||
/// </value>
|
|||
public new Optional<T> OldValue { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the new value of the property.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The new value of the property.
|
|||
/// </value>
|
|||
public new BindingValue<T> NewValue { get; private set; } |
|||
|
|||
protected override AvaloniaProperty GetProperty() => Property; |
|||
|
|||
protected override object? GetOldValue() => OldValue.ValueOrDefault(AvaloniaProperty.UnsetValue); |
|||
|
|||
protected override object? GetNewValue() => NewValue.ValueOrDefault(AvaloniaProperty.UnsetValue); |
|||
} |
|||
} |
|||
@ -0,0 +1,406 @@ |
|||
using System; |
|||
using Avalonia.Utilities; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Data |
|||
{ |
|||
/// <summary>
|
|||
/// Describes the type of a <see cref="BindingValue{T}"/>.
|
|||
/// </summary>
|
|||
public enum BindingValueType |
|||
{ |
|||
/// <summary>
|
|||
/// An unset value: the target property will revert to its unbound state until a new
|
|||
/// binding value is produced.
|
|||
/// </summary>
|
|||
UnsetValue = 0, |
|||
|
|||
/// <summary>
|
|||
/// Do nothing: the binding value will be ignored.
|
|||
/// </summary>
|
|||
DoNothing = 1, |
|||
|
|||
/// <summary>
|
|||
/// A simple value.
|
|||
/// </summary>
|
|||
Value = 2 | HasValue, |
|||
|
|||
/// <summary>
|
|||
/// A binding error, such as a missing source property.
|
|||
/// </summary>
|
|||
BindingError = 3 | HasError, |
|||
|
|||
/// <summary>
|
|||
/// A data validation error.
|
|||
/// </summary>
|
|||
DataValidationError = 4 | HasError, |
|||
|
|||
/// <summary>
|
|||
/// A binding error with a fallback value.
|
|||
/// </summary>
|
|||
BindingErrorWithFallback = BindingError | HasValue, |
|||
|
|||
/// <summary>
|
|||
/// A data validation error with a fallback value.
|
|||
/// </summary>
|
|||
DataValidationErrorWithFallback = DataValidationError | HasValue, |
|||
|
|||
TypeMask = 0x00ff, |
|||
HasValue = 0x0100, |
|||
HasError = 0x0200, |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A value passed into a binding.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <remarks>
|
|||
/// The avalonia binding system is typed, and as such additional state is stored in this
|
|||
/// structure. A binding value can be in a number of states, described by the
|
|||
/// <see cref="Type"/> property:
|
|||
///
|
|||
/// - <see cref="BindingValueType.Value"/>: a simple value
|
|||
/// - <see cref="BindingValueType.UnsetValue"/>: the target property will revert to its unbound
|
|||
/// state until a new binding value is produced. Represented by
|
|||
/// <see cref="AvaloniaProperty.UnsetValue"/> in an untyped context
|
|||
/// - <see cref="BindingValueType.DoNothing"/>: the binding value will be ignored. Represented
|
|||
/// by <see cref="BindingOperations.DoNothing"/> in an untyped context
|
|||
/// - <see cref="BindingValueType.BindingError"/>: a binding error, such as a missing source
|
|||
/// property, with an optional fallback value
|
|||
/// - <see cref="BindingValueType.DataValidationError"/>: a data validation error, with an
|
|||
/// optional fallback value
|
|||
///
|
|||
/// To create a new binding value you can:
|
|||
///
|
|||
/// - For a simple value, call the <see cref="BindingValue{T}"/> constructor or use an implicit
|
|||
/// conversion from <typeparamref name="T"/>
|
|||
/// - For an unset value, use <see cref="Unset"/> or simply `default`
|
|||
/// - For other types, call one of the static factory methods
|
|||
/// </remarks>
|
|||
public readonly struct BindingValue<T> |
|||
{ |
|||
private readonly T _value; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="BindingValue{T}"/> struct with a type of
|
|||
/// <see cref="BindingValueType.Value"/>
|
|||
/// </summary>
|
|||
/// <param name="value">The value.</param>
|
|||
public BindingValue(T value) |
|||
{ |
|||
ValidateValue(value); |
|||
_value = value; |
|||
Type = BindingValueType.Value; |
|||
Error = null; |
|||
} |
|||
|
|||
private BindingValue(BindingValueType type, T value, Exception? error) |
|||
{ |
|||
_value = value; |
|||
Type = type; |
|||
Error = error; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the binding value represents either a binding or data
|
|||
/// validation error.
|
|||
/// </summary>
|
|||
public bool HasError => Type.HasFlagCustom(BindingValueType.HasError); |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the binding value has a value.
|
|||
/// </summary>
|
|||
public bool HasValue => Type.HasFlagCustom(BindingValueType.HasValue); |
|||
|
|||
/// <summary>
|
|||
/// Gets the type of the binding value.
|
|||
/// </summary>
|
|||
public BindingValueType Type { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the binding value or fallback value.
|
|||
/// </summary>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// <see cref="HasValue"/> is false.
|
|||
/// </exception>
|
|||
public T Value => HasValue ? _value : throw new InvalidOperationException("BindingValue has no value."); |
|||
|
|||
/// <summary>
|
|||
/// Gets the binding or data validation error.
|
|||
/// </summary>
|
|||
public Exception? Error { get; } |
|||
|
|||
/// <summary>
|
|||
/// Converts the binding value to an <see cref="Optional{T}"/>.
|
|||
/// </summary>
|
|||
/// <returns></returns>
|
|||
public Optional<T> ToOptional() => HasValue ? new Optional<T>(Value) : default; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() => HasError ? $"Error: {Error!.Message}" : Value?.ToString() ?? "(null)"; |
|||
|
|||
/// <summary>
|
|||
/// Converts the value to untyped representation, using <see cref="AvaloniaProperty.UnsetValue"/>,
|
|||
/// <see cref="BindingOperations.DoNothing"/> and <see cref="BindingNotification"/> where
|
|||
/// appropriate.
|
|||
/// </summary>
|
|||
/// <returns>The untyped representation of the binding value.</returns>
|
|||
public object? ToUntyped() |
|||
{ |
|||
return Type switch |
|||
{ |
|||
BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, |
|||
BindingValueType.DoNothing => BindingOperations.DoNothing, |
|||
BindingValueType.Value => Value, |
|||
BindingValueType.BindingError => |
|||
new BindingNotification(Error, BindingErrorType.Error), |
|||
BindingValueType.BindingErrorWithFallback => |
|||
new BindingNotification(Error, BindingErrorType.Error, Value), |
|||
BindingValueType.DataValidationError => |
|||
new BindingNotification(Error, BindingErrorType.DataValidationError), |
|||
BindingValueType.DataValidationErrorWithFallback => |
|||
new BindingNotification(Error, BindingErrorType.DataValidationError, Value), |
|||
_ => throw new NotSupportedException("Invalida BindingValueType."), |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a new binding value with the specified value.
|
|||
/// </summary>
|
|||
/// <param name="value">The new value.</param>
|
|||
/// <returns>The new binding value.</returns>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// The binding type is <see cref="BindingValueType.UnsetValue"/> or
|
|||
/// <see cref="BindingValueType.DoNothing"/>.
|
|||
/// </exception>
|
|||
public BindingValue<T> WithValue(T value) |
|||
{ |
|||
if (Type == BindingValueType.DoNothing) |
|||
{ |
|||
throw new InvalidOperationException("Cannot add value to DoNothing binding value."); |
|||
} |
|||
|
|||
var type = Type == BindingValueType.UnsetValue ? BindingValueType.Value : Type; |
|||
return new BindingValue<T>(type | BindingValueType.HasValue, value, Error); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the binding value if present, otherwise a default value.
|
|||
/// </summary>
|
|||
/// <param name="defaultValue">The default value.</param>
|
|||
/// <returns>The value.</returns>
|
|||
public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the binding value if present, otherwise a default value.
|
|||
/// </summary>
|
|||
/// <param name="defaultValue">The default value.</param>
|
|||
/// <returns>
|
|||
/// The value if present and of the correct type, `default(TResult)` if the value is
|
|||
/// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
|
|||
/// value is not present.
|
|||
/// </returns>
|
|||
public TResult ValueOrDefault<TResult>(TResult defaultValue = default) |
|||
{ |
|||
return HasValue ? |
|||
Value is TResult result ? result : default |
|||
: defaultValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a <see cref="BindingValue{T}"/> from an object, handling the special values
|
|||
/// <see cref="AvaloniaProperty.UnsetValue"/> and <see cref="BindingOperations.DoNothing"/>.
|
|||
/// </summary>
|
|||
/// <param name="value">The untyped value.</param>
|
|||
/// <returns>The typed binding value.</returns>
|
|||
public static BindingValue<T> FromUntyped(object? value) |
|||
{ |
|||
return value switch |
|||
{ |
|||
UnsetValueType _ => Unset, |
|||
DoNothingType _ => DoNothing, |
|||
BindingNotification n => n.ToBindingValue().Cast<T>(), |
|||
_ => (T)value |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a binding value from an instance of the underlying value type.
|
|||
/// </summary>
|
|||
/// <param name="value">The value.</param>
|
|||
public static implicit operator BindingValue<T>(T value) => new BindingValue<T>(value); |
|||
|
|||
/// <summary>
|
|||
/// Creates a binding value from an <see cref="Optional{T}"/>.
|
|||
/// </summary>
|
|||
/// <param name="optional">The optional value.</param>
|
|||
|
|||
public static implicit operator BindingValue<T>(Optional<T> optional) |
|||
{ |
|||
return optional.HasValue ? optional.Value : Unset; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.UnsetValue"/>.
|
|||
/// </summary>
|
|||
public static BindingValue<T> Unset => new BindingValue<T>(BindingValueType.UnsetValue, default, null); |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.DoNothing"/>.
|
|||
/// </summary>
|
|||
public static BindingValue<T> DoNothing => new BindingValue<T>(BindingValueType.DoNothing, default, null); |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/>.
|
|||
/// </summary>
|
|||
/// <param name="e">The binding error.</param>
|
|||
public static BindingValue<T> BindingError(Exception e) |
|||
{ |
|||
e = e ?? throw new ArgumentNullException("e"); |
|||
|
|||
return new BindingValue<T>(BindingValueType.BindingError, default, e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.BindingErrorWithFallback"/>.
|
|||
/// </summary>
|
|||
/// <param name="e">The binding error.</param>
|
|||
/// <param name="fallbackValue">The fallback value.</param>
|
|||
public static BindingValue<T> BindingError(Exception e, T fallbackValue) |
|||
{ |
|||
e = e ?? throw new ArgumentNullException("e"); |
|||
|
|||
return new BindingValue<T>(BindingValueType.BindingErrorWithFallback, fallbackValue, e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.BindingError"/> or
|
|||
/// <see cref="BindingValueType.BindingErrorWithFallback"/>.
|
|||
/// </summary>
|
|||
/// <param name="e">The binding error.</param>
|
|||
/// <param name="fallbackValue">The fallback value.</param>
|
|||
public static BindingValue<T> BindingError(Exception e, Optional<T> fallbackValue) |
|||
{ |
|||
e = e ?? throw new ArgumentNullException("e"); |
|||
|
|||
return new BindingValue<T>( |
|||
fallbackValue.HasValue ? |
|||
BindingValueType.BindingErrorWithFallback : |
|||
BindingValueType.BindingError, |
|||
fallbackValue.HasValue ? fallbackValue.Value : default, |
|||
e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/>.
|
|||
/// </summary>
|
|||
/// <param name="e">The data validation error.</param>
|
|||
public static BindingValue<T> DataValidationError(Exception e) |
|||
{ |
|||
e = e ?? throw new ArgumentNullException("e"); |
|||
|
|||
return new BindingValue<T>(BindingValueType.DataValidationError, default, e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
|
|||
/// </summary>
|
|||
/// <param name="e">The data validation error.</param>
|
|||
/// <param name="fallbackValue">The fallback value.</param>
|
|||
public static BindingValue<T> DataValidationError(Exception e, T fallbackValue) |
|||
{ |
|||
e = e ?? throw new ArgumentNullException("e"); |
|||
|
|||
return new BindingValue<T>(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a binding value with a type of <see cref="BindingValueType.DataValidationError"/> or
|
|||
/// <see cref="BindingValueType.DataValidationErrorWithFallback"/>.
|
|||
/// </summary>
|
|||
/// <param name="e">The binding error.</param>
|
|||
/// <param name="fallbackValue">The fallback value.</param>
|
|||
public static BindingValue<T> DataValidationError(Exception e, Optional<T> fallbackValue) |
|||
{ |
|||
e = e ?? throw new ArgumentNullException("e"); |
|||
|
|||
return new BindingValue<T>( |
|||
fallbackValue.HasValue ? |
|||
BindingValueType.DataValidationError : |
|||
BindingValueType.DataValidationErrorWithFallback, |
|||
fallbackValue.HasValue ? fallbackValue.Value : default, |
|||
e); |
|||
} |
|||
|
|||
private static void ValidateValue(T value) |
|||
{ |
|||
if (value is UnsetValueType) |
|||
{ |
|||
throw new InvalidOperationException("AvaloniaValue.UnsetValue is not a valid value for BindingValue<>."); |
|||
} |
|||
|
|||
if (value is DoNothingType) |
|||
{ |
|||
throw new InvalidOperationException("BindingOperations.DoNothing is not a valid value for BindingValue<>."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public static class BindingValueExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Casts the type of a <see cref="BindingValue{T}"/> using only the C# cast operator.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The target type.</typeparam>
|
|||
/// <param name="value">The binding value.</param>
|
|||
/// <returns>The cast value.</returns>
|
|||
public static BindingValue<T> Cast<T>(this BindingValue<object> value) |
|||
{ |
|||
return value.Type switch |
|||
{ |
|||
BindingValueType.DoNothing => BindingValue<T>.DoNothing, |
|||
BindingValueType.UnsetValue => BindingValue<T>.Unset, |
|||
BindingValueType.Value => new BindingValue<T>((T)value.Value), |
|||
BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!), |
|||
BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError( |
|||
value.Error!, |
|||
(T)value.Value), |
|||
BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!), |
|||
BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError( |
|||
value.Error!, |
|||
(T)value.Value), |
|||
_ => throw new NotSupportedException("Invalid BindingValue type."), |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Casts the type of a <see cref="BindingValue{T}"/> using the implicit conversions
|
|||
/// allowed by the C# language.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The target type.</typeparam>
|
|||
/// <param name="value">The binding value.</param>
|
|||
/// <returns>The cast value.</returns>
|
|||
/// <remarks>
|
|||
/// Note that this method uses reflection and as such may be slow.
|
|||
/// </remarks>
|
|||
public static BindingValue<T> Convert<T>(this BindingValue<object> value) |
|||
{ |
|||
return value.Type switch |
|||
{ |
|||
BindingValueType.DoNothing => BindingValue<T>.DoNothing, |
|||
BindingValueType.UnsetValue => BindingValue<T>.Unset, |
|||
BindingValueType.Value => new BindingValue<T>(TypeUtilities.ConvertImplicit<T>(value.Value)), |
|||
BindingValueType.BindingError => BindingValue<T>.BindingError(value.Error!), |
|||
BindingValueType.BindingErrorWithFallback => BindingValue<T>.BindingError( |
|||
value.Error!, |
|||
TypeUtilities.ConvertImplicit<T>(value.Value)), |
|||
BindingValueType.DataValidationError => BindingValue<T>.DataValidationError(value.Error!), |
|||
BindingValueType.DataValidationErrorWithFallback => BindingValue<T>.DataValidationError( |
|||
value.Error!, |
|||
TypeUtilities.ConvertImplicit<T>(value.Value)), |
|||
_ => throw new NotSupportedException("Invalid BindingValue type."), |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Data |
|||
{ |
|||
/// <summary>
|
|||
/// An optional typed value.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <remarks>
|
|||
/// This struct is similar to <see cref="Nullable{T}"/> except it also accepts reference types:
|
|||
/// note that null is a valid value for reference types. It is also similar to
|
|||
/// <see cref="BindingValue{T}"/> but has only two states: "value present" and "value missing".
|
|||
///
|
|||
/// To create a new optional value you can:
|
|||
///
|
|||
/// - For a simple value, call the <see cref="Optional{T}"/> constructor or use an implicit
|
|||
/// conversion from <typeparamref name="T"/>
|
|||
/// - For an missing value, use <see cref="Empty"/> or simply `default`
|
|||
/// </remarks>
|
|||
public struct Optional<T> |
|||
{ |
|||
private readonly T _value; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Optional{T}"/> struct with value.
|
|||
/// </summary>
|
|||
/// <param name="value">The value.</param>
|
|||
public Optional(T value) |
|||
{ |
|||
_value = value; |
|||
HasValue = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether a value is present.
|
|||
/// </summary>
|
|||
public bool HasValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value.
|
|||
/// </summary>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// <see cref="HasValue"/> is false.
|
|||
/// </exception>
|
|||
public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value."); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool Equals(object obj) => obj is Optional<T> o && this == o; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int GetHashCode() => HasValue ? Value!.GetHashCode() : 0; |
|||
|
|||
/// <summary>
|
|||
/// Casts the value (if any) to an <see cref="object"/>.
|
|||
/// </summary>
|
|||
/// <returns>The cast optional value.</returns>
|
|||
public Optional<object> ToObject() => HasValue ? new Optional<object>(Value) : default; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override string ToString() => HasValue ? Value?.ToString() ?? "(null)" : "(empty)"; |
|||
|
|||
/// <summary>
|
|||
/// Gets the value if present, otherwise a default value.
|
|||
/// </summary>
|
|||
/// <param name="defaultValue">The default value.</param>
|
|||
/// <returns>The value.</returns>
|
|||
public T ValueOrDefault(T defaultValue = default) => HasValue ? Value : defaultValue; |
|||
|
|||
/// <summary>
|
|||
/// Gets the value if present, otherwise a default value.
|
|||
/// </summary>
|
|||
/// <param name="defaultValue">The default value.</param>
|
|||
/// <returns>
|
|||
/// The value if present and of the correct type, `default(TResult)` if the value is
|
|||
/// present but not of the correct type or null, or <paramref name="defaultValue"/> if the
|
|||
/// value is not present.
|
|||
/// </returns>
|
|||
public TResult ValueOrDefault<TResult>(TResult defaultValue = default) |
|||
{ |
|||
return HasValue ? |
|||
Value is TResult result ? result : default |
|||
: defaultValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates an <see cref="Optional{T}"/> from an instance of the underlying value type.
|
|||
/// </summary>
|
|||
/// <param name="value">The value.</param>
|
|||
public static implicit operator Optional<T>(T value) => new Optional<T>(value); |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="Optional{T}"/>s for inequality.
|
|||
/// </summary>
|
|||
/// <param name="x">The first value.</param>
|
|||
/// <param name="y">The second value.</param>
|
|||
/// <returns>True if the values are unequal; otherwise false.</returns>
|
|||
public static bool operator !=(Optional<T> x, Optional<T> y) => !(x == y); |
|||
|
|||
/// <summary>
|
|||
/// Compares two <see cref="Optional{T}"/>s for equality.
|
|||
/// </summary>
|
|||
/// <param name="x">The first value.</param>
|
|||
/// <param name="y">The second value.</param>
|
|||
/// <returns>True if the values are equal; otherwise false.</returns>
|
|||
public static bool operator==(Optional<T> x, Optional<T> y) |
|||
{ |
|||
if (!x.HasValue && !y.HasValue) |
|||
{ |
|||
return true; |
|||
} |
|||
else if (x.HasValue && y.HasValue) |
|||
{ |
|||
return EqualityComparer<T>.Default.Equals(x.Value, y.Value); |
|||
} |
|||
else |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns an <see cref="Optional{T}"/> without a value.
|
|||
/// </summary>
|
|||
public static Optional<T> Empty => default; |
|||
} |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Reactive; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Base class for direct properties.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">The type of the property's value.</typeparam>
|
|||
/// <remarks>
|
|||
/// Whereas <see cref="DirectProperty{TOwner, TValue}"/> is typed on the owner type, this base
|
|||
/// class provides a non-owner-typed interface to a direct poperty.
|
|||
/// </remarks>
|
|||
public abstract class DirectPropertyBase<TValue> : AvaloniaProperty<TValue> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DirectPropertyBase{TValue}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the property.</param>
|
|||
/// <param name="ownerType">The type of the class that registers the property.</param>
|
|||
/// <param name="metadata">The property metadata.</param>
|
|||
/// <param name="enableDataValidation">
|
|||
/// Whether the property is interested in data validation.
|
|||
/// </param>
|
|||
protected DirectPropertyBase( |
|||
string name, |
|||
Type ownerType, |
|||
PropertyMetadata metadata, |
|||
bool enableDataValidation) |
|||
: base(name, ownerType, metadata) |
|||
{ |
|||
IsDataValidationEnabled = enableDataValidation; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
|
|||
/// </summary>
|
|||
/// <param name="source">The property to copy.</param>
|
|||
/// <param name="ownerType">The new owner type.</param>
|
|||
/// <param name="metadata">Optional overridden metadata.</param>
|
|||
/// <param name="enableDataValidation">
|
|||
/// Whether the property is interested in data validation.
|
|||
/// </param>
|
|||
protected DirectPropertyBase( |
|||
AvaloniaProperty source, |
|||
Type ownerType, |
|||
PropertyMetadata metadata, |
|||
bool enableDataValidation) |
|||
: base(source, ownerType, metadata) |
|||
{ |
|||
IsDataValidationEnabled = enableDataValidation; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the type that registered the property.
|
|||
/// </summary>
|
|||
public abstract Type Owner { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value that indicates whether data validation is enabled for the property.
|
|||
/// </summary>
|
|||
public bool IsDataValidationEnabled { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the value of the property on the instance.
|
|||
/// </summary>
|
|||
/// <param name="instance">The instance.</param>
|
|||
/// <returns>The property value.</returns>
|
|||
internal abstract TValue InvokeGetter(IAvaloniaObject instance); |
|||
|
|||
/// <summary>
|
|||
/// Sets the value of the property on the instance.
|
|||
/// </summary>
|
|||
/// <param name="instance">The instance.</param>
|
|||
/// <param name="value">The value.</param>
|
|||
internal abstract void InvokeSetter(IAvaloniaObject instance, BindingValue<TValue> value); |
|||
|
|||
/// <summary>
|
|||
/// Gets the unset value for the property on the specified type.
|
|||
/// </summary>
|
|||
/// <param name="type">The type.</param>
|
|||
/// <returns>The unset value.</returns>
|
|||
public TValue GetUnsetValue(Type type) |
|||
{ |
|||
type = type ?? throw new ArgumentNullException(nameof(type)); |
|||
return GetMetadata(type).UnsetValue; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the property metadata for the specified type.
|
|||
/// </summary>
|
|||
/// <param name="type">The type.</param>
|
|||
/// <returns>
|
|||
/// The property metadata.
|
|||
/// </returns>
|
|||
public new DirectPropertyMetadata<TValue> GetMetadata(Type type) |
|||
{ |
|||
return (DirectPropertyMetadata<TValue>)base.GetMetadata(type); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
internal override void NotifyInitialized(IAvaloniaObject o) |
|||
{ |
|||
var e = new AvaloniaPropertyChangedEventArgs<TValue>( |
|||
o, |
|||
this, |
|||
default, |
|||
InvokeGetter(o), |
|||
BindingPriority.Unset); |
|||
NotifyInitialized(e); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
internal override object? RouteGetValue(IAvaloniaObject o) |
|||
{ |
|||
return o.GetValue<TValue>(this); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
internal override void RouteSetValue( |
|||
IAvaloniaObject o, |
|||
object value, |
|||
BindingPriority priority) |
|||
{ |
|||
var v = TryConvert(value); |
|||
|
|||
if (v.HasValue) |
|||
{ |
|||
o.SetValue<TValue>(this, (TValue)v.Value, priority); |
|||
} |
|||
else if (v.Type == BindingValueType.UnsetValue) |
|||
{ |
|||
o.ClearValue(this); |
|||
} |
|||
else if (v.HasError) |
|||
{ |
|||
throw v.Error!; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
internal override IDisposable RouteBind( |
|||
IAvaloniaObject o, |
|||
IObservable<BindingValue<object>> source, |
|||
BindingPriority priority) |
|||
{ |
|||
var adapter = TypedBindingAdapter<TValue>.Create(o, this, source); |
|||
return o.Bind<TValue>(this, adapter, priority); |
|||
} |
|||
|
|||
internal override void RouteInheritanceParentChanged(AvaloniaObject o, IAvaloniaObject oldParent) |
|||
{ |
|||
throw new NotSupportedException("Direct properties do not support inheritance."); |
|||
} |
|||
} |
|||
} |
|||
@ -1,51 +0,0 @@ |
|||
// 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 Avalonia.Data; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// An owner of a <see cref="PriorityValue"/>.
|
|||
/// </summary>
|
|||
internal interface IPriorityValueOwner |
|||
{ |
|||
/// <summary>
|
|||
/// Called when a <see cref="PriorityValue"/>'s value changes.
|
|||
/// </summary>
|
|||
/// <param name="property">The the property that has changed.</param>
|
|||
/// <param name="priority">The priority of the value.</param>
|
|||
/// <param name="oldValue">The old value.</param>
|
|||
/// <param name="newValue">The new value.</param>
|
|||
void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue); |
|||
|
|||
/// <summary>
|
|||
/// Called when a <see cref="BindingNotification"/> is received by a
|
|||
/// <see cref="PriorityValue"/>.
|
|||
/// </summary>
|
|||
/// <param name="property">The the property that has changed.</param>
|
|||
/// <param name="notification">The notification.</param>
|
|||
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); |
|||
|
|||
/// <summary>
|
|||
/// Returns deferred setter for given non-direct property.
|
|||
/// </summary>
|
|||
/// <param name="property">Property.</param>
|
|||
/// <returns>Deferred setter for given property.</returns>
|
|||
DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property); |
|||
|
|||
/// <summary>
|
|||
/// Logs a binding error.
|
|||
/// </summary>
|
|||
/// <param name="property">The property the error occurred on.</param>
|
|||
/// <param name="e">The binding error.</param>
|
|||
void LogError(AvaloniaProperty property, Exception e); |
|||
|
|||
/// <summary>
|
|||
/// Ensures that the current thread is the UI thread.
|
|||
/// </summary>
|
|||
void VerifyAccess(); |
|||
} |
|||
} |
|||
@ -1,160 +0,0 @@ |
|||
// 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.Runtime.ExceptionServices; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// A registered binding in a <see cref="PriorityValue"/>.
|
|||
/// </summary>
|
|||
internal class PriorityBindingEntry : IDisposable, IObserver<object> |
|||
{ |
|||
private readonly PriorityLevel _owner; |
|||
private IDisposable _subscription; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner.</param>
|
|||
/// <param name="index">
|
|||
/// The binding index. Later bindings should have higher indexes.
|
|||
/// </param>
|
|||
public PriorityBindingEntry(PriorityLevel owner, int index) |
|||
{ |
|||
_owner = owner; |
|||
Index = index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the observable associated with the entry.
|
|||
/// </summary>
|
|||
public IObservable<object> Observable { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a description of the binding.
|
|||
/// </summary>
|
|||
public string Description |
|||
{ |
|||
get; |
|||
private set; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the binding entry index. Later bindings will have higher indexes.
|
|||
/// </summary>
|
|||
public int Index |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the binding has completed.
|
|||
/// </summary>
|
|||
public bool HasCompleted { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// The current value of the binding.
|
|||
/// </summary>
|
|||
public object Value |
|||
{ |
|||
get; |
|||
private set; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Starts listening to the binding.
|
|||
/// </summary>
|
|||
/// <param name="binding">The binding.</param>
|
|||
public void Start(IObservable<object> binding) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(binding != null); |
|||
|
|||
if (_subscription != null) |
|||
{ |
|||
throw new Exception("PriorityValue.Entry.Start() called more than once."); |
|||
} |
|||
|
|||
Observable = binding; |
|||
Value = AvaloniaProperty.UnsetValue; |
|||
|
|||
if (binding is IDescription) |
|||
{ |
|||
Description = ((IDescription)binding).Description; |
|||
} |
|||
|
|||
_subscription = binding.Subscribe(this); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Ends the binding subscription.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
} |
|||
|
|||
void IObserver<object>.OnNext(object value) |
|||
{ |
|||
void Signal(PriorityBindingEntry instance, object newValue) |
|||
{ |
|||
var notification = newValue as BindingNotification; |
|||
|
|||
if (notification != null) |
|||
{ |
|||
if (notification.HasValue || notification.ErrorType == BindingErrorType.Error) |
|||
{ |
|||
instance.Value = notification.Value; |
|||
instance._owner.Changed(instance); |
|||
} |
|||
|
|||
if (notification.ErrorType != BindingErrorType.None) |
|||
{ |
|||
instance._owner.Error(instance, notification); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
instance.Value = newValue; |
|||
instance._owner.Changed(instance); |
|||
} |
|||
} |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Signal(this, 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 = this; |
|||
var newValue = value; |
|||
|
|||
Dispatcher.UIThread.Post(() => Signal(instance, newValue)); |
|||
} |
|||
} |
|||
|
|||
void IObserver<object>.OnCompleted() |
|||
{ |
|||
HasCompleted = true; |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
_owner.Completed(this); |
|||
} |
|||
else |
|||
{ |
|||
Dispatcher.UIThread.Post(() => _owner.Completed(this)); |
|||
} |
|||
} |
|||
|
|||
void IObserver<object>.OnError(Exception error) |
|||
{ |
|||
ExceptionDispatchInfo.Capture(error).Throw(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,227 +0,0 @@ |
|||
// 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.Diagnostics; |
|||
using System.Threading; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Stores bindings for a priority level in a <see cref="PriorityValue"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// Each priority level in a <see cref="PriorityValue"/> has a current <see cref="Value"/>,
|
|||
/// a list of <see cref="Bindings"/> and a <see cref="DirectValue"/>. When there are no
|
|||
/// bindings present, or all bindings return <see cref="AvaloniaProperty.UnsetValue"/> then
|
|||
/// <code>Value</code> will equal <code>DirectValue</code>.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// When there are bindings present, then the latest added binding that doesn't return
|
|||
/// <code>UnsetValue</code> will take precedence. The active binding is returned by the
|
|||
/// <see cref="ActiveBindingIndex"/> property (which refers to the active binding's
|
|||
/// <see cref="PriorityBindingEntry.Index"/> property rather than the index in
|
|||
/// <code>Bindings</code>).
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// If <code>DirectValue</code> is set while a binding is active, then it will replace the
|
|||
/// current value until the active binding fires again.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
internal class PriorityLevel |
|||
{ |
|||
private object _directValue; |
|||
private int _nextIndex; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="PriorityLevel"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner.</param>
|
|||
/// <param name="priority">The priority.</param>
|
|||
public PriorityLevel( |
|||
PriorityValue owner, |
|||
int priority) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(owner != null); |
|||
|
|||
Owner = owner; |
|||
Priority = priority; |
|||
Value = _directValue = AvaloniaProperty.UnsetValue; |
|||
ActiveBindingIndex = -1; |
|||
Bindings = new LinkedList<PriorityBindingEntry>(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the owner of the level.
|
|||
/// </summary>
|
|||
public PriorityValue Owner { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the priority of this level.
|
|||
/// </summary>
|
|||
public int Priority { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the direct value for this priority level.
|
|||
/// </summary>
|
|||
public object DirectValue |
|||
{ |
|||
get |
|||
{ |
|||
return _directValue; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
Value = _directValue = value; |
|||
Owner.LevelValueChanged(this); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the current binding for the priority level.
|
|||
/// </summary>
|
|||
public object Value { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="PriorityBindingEntry.Index"/> value of the active binding, or -1
|
|||
/// if no binding is active.
|
|||
/// </summary>
|
|||
public int ActiveBindingIndex { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the bindings for the priority level.
|
|||
/// </summary>
|
|||
public LinkedList<PriorityBindingEntry> Bindings { get; } |
|||
|
|||
/// <summary>
|
|||
/// Adds a binding.
|
|||
/// </summary>
|
|||
/// <param name="binding">The binding to add.</param>
|
|||
/// <returns>A disposable used to remove the binding.</returns>
|
|||
public IDisposable Add(IObservable<object> binding) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(binding != null); |
|||
|
|||
var entry = new PriorityBindingEntry(this, _nextIndex++); |
|||
var node = Bindings.AddFirst(entry); |
|||
|
|||
entry.Start(binding); |
|||
|
|||
return new RemoveBindingDisposable(node, Bindings, this); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invoked when an entry in <see cref="Bindings"/> changes value.
|
|||
/// </summary>
|
|||
/// <param name="entry">The entry that changed.</param>
|
|||
public void Changed(PriorityBindingEntry entry) |
|||
{ |
|||
if (entry.Index >= ActiveBindingIndex) |
|||
{ |
|||
if (entry.Value != AvaloniaProperty.UnsetValue) |
|||
{ |
|||
Value = entry.Value; |
|||
ActiveBindingIndex = entry.Index; |
|||
Owner.LevelValueChanged(this); |
|||
} |
|||
else |
|||
{ |
|||
ActivateFirstBinding(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invoked when an entry in <see cref="Bindings"/> completes.
|
|||
/// </summary>
|
|||
/// <param name="entry">The entry that completed.</param>
|
|||
public void Completed(PriorityBindingEntry entry) |
|||
{ |
|||
Bindings.Remove(entry); |
|||
|
|||
if (entry.Index >= ActiveBindingIndex) |
|||
{ |
|||
ActivateFirstBinding(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Invoked when an entry in <see cref="Bindings"/> encounters a recoverable error.
|
|||
/// </summary>
|
|||
/// <param name="entry">The entry that completed.</param>
|
|||
/// <param name="error">The error.</param>
|
|||
public void Error(PriorityBindingEntry entry, BindingNotification error) |
|||
{ |
|||
Owner.LevelError(this, error); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Activates the first binding that has a value.
|
|||
/// </summary>
|
|||
private void ActivateFirstBinding() |
|||
{ |
|||
foreach (var binding in Bindings) |
|||
{ |
|||
if (binding.Value != AvaloniaProperty.UnsetValue) |
|||
{ |
|||
Value = binding.Value; |
|||
ActiveBindingIndex = binding.Index; |
|||
Owner.LevelValueChanged(this); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
Value = DirectValue; |
|||
ActiveBindingIndex = -1; |
|||
Owner.LevelValueChanged(this); |
|||
} |
|||
|
|||
private sealed class RemoveBindingDisposable : IDisposable |
|||
{ |
|||
private readonly LinkedList<PriorityBindingEntry> _bindings; |
|||
private readonly PriorityLevel _priorityLevel; |
|||
private LinkedListNode<PriorityBindingEntry> _binding; |
|||
|
|||
public RemoveBindingDisposable( |
|||
LinkedListNode<PriorityBindingEntry> binding, |
|||
LinkedList<PriorityBindingEntry> bindings, |
|||
PriorityLevel priorityLevel) |
|||
{ |
|||
_binding = binding; |
|||
_bindings = bindings; |
|||
_priorityLevel = priorityLevel; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
LinkedListNode<PriorityBindingEntry> binding = Interlocked.Exchange(ref _binding, null); |
|||
|
|||
if (binding == null) |
|||
{ |
|||
// Some system is trying to remove binding twice.
|
|||
Debug.Assert(false); |
|||
|
|||
return; |
|||
} |
|||
|
|||
PriorityBindingEntry entry = binding.Value; |
|||
|
|||
if (!entry.HasCompleted) |
|||
{ |
|||
_bindings.Remove(binding); |
|||
|
|||
entry.Dispose(); |
|||
|
|||
if (entry.Index >= _priorityLevel.ActiveBindingIndex) |
|||
{ |
|||
_priorityLevel.ActivateFirstBinding(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,315 +0,0 @@ |
|||
// 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.Linq; |
|||
using System.Text; |
|||
using Avalonia.Data; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Maintains a list of prioritized bindings together with a current value.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Bindings, in the form of <see cref="IObservable{Object}"/>s are added to the object using
|
|||
/// the <see cref="Add"/> method. With the observable is passed a priority, where lower values
|
|||
/// represent higher priorities. The current <see cref="Value"/> is selected from the highest
|
|||
/// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
|
|||
/// are multiple bindings registered with the same priority, the most recently added binding
|
|||
/// has a higher priority. Each time the value changes, the
|
|||
/// <see cref="IPriorityValueOwner.Changed"/> method on the
|
|||
/// owner object is fired with the old and new values.
|
|||
/// </remarks>
|
|||
internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)> |
|||
{ |
|||
private readonly Type _valueType; |
|||
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>(); |
|||
private readonly Func<object, object> _validate; |
|||
private (object value, int priority) _value; |
|||
private DeferredSetter<object> _setter; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
|
|||
/// </summary>
|
|||
/// <param name="owner">The owner of the object.</param>
|
|||
/// <param name="property">The property that the value represents.</param>
|
|||
/// <param name="valueType">The value type.</param>
|
|||
/// <param name="validate">An optional validation function.</param>
|
|||
public PriorityValue( |
|||
IPriorityValueOwner owner, |
|||
AvaloniaProperty property, |
|||
Type valueType, |
|||
Func<object, object> validate = null) |
|||
{ |
|||
Owner = owner; |
|||
Property = property; |
|||
_valueType = valueType; |
|||
_value = (AvaloniaProperty.UnsetValue, int.MaxValue); |
|||
_validate = validate; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the property is animating.
|
|||
/// </summary>
|
|||
public bool IsAnimating |
|||
{ |
|||
get |
|||
{ |
|||
return ValuePriority <= (int)BindingPriority.Animation && |
|||
GetLevel(ValuePriority).ActiveBindingIndex != -1; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the owner of the value.
|
|||
/// </summary>
|
|||
public IPriorityValueOwner Owner { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the property that the value represents.
|
|||
/// </summary>
|
|||
public AvaloniaProperty Property { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the current value.
|
|||
/// </summary>
|
|||
public object Value => _value.value; |
|||
|
|||
/// <summary>
|
|||
/// Gets the priority of the binding that is currently active.
|
|||
/// </summary>
|
|||
public int ValuePriority => _value.priority; |
|||
|
|||
/// <summary>
|
|||
/// Adds a new binding.
|
|||
/// </summary>
|
|||
/// <param name="binding">The binding.</param>
|
|||
/// <param name="priority">The binding priority.</param>
|
|||
/// <returns>
|
|||
/// A disposable that will remove the binding.
|
|||
/// </returns>
|
|||
public IDisposable Add(IObservable<object> binding, int priority) |
|||
{ |
|||
return GetLevel(priority).Add(binding); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the value for a specified priority.
|
|||
/// </summary>
|
|||
/// <param name="value">The value.</param>
|
|||
/// <param name="priority">The priority</param>
|
|||
public void SetValue(object value, int priority) |
|||
{ |
|||
GetLevel(priority).DirectValue = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the currently active bindings on this object.
|
|||
/// </summary>
|
|||
/// <returns>An enumerable collection of bindings.</returns>
|
|||
public IEnumerable<PriorityBindingEntry> GetBindings() |
|||
{ |
|||
foreach (var level in _levels) |
|||
{ |
|||
foreach (var binding in level.Value.Bindings) |
|||
{ |
|||
yield return binding; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns diagnostic string that can help the user debug the bindings in effect on
|
|||
/// this object.
|
|||
/// </summary>
|
|||
/// <returns>A diagnostic string.</returns>
|
|||
public string GetDiagnostic() |
|||
{ |
|||
var b = new StringBuilder(); |
|||
var first = true; |
|||
|
|||
foreach (var level in _levels) |
|||
{ |
|||
if (!first) |
|||
{ |
|||
b.AppendLine(); |
|||
} |
|||
|
|||
b.Append(ValuePriority == level.Key ? "*" : string.Empty); |
|||
b.Append("Priority "); |
|||
b.Append(level.Key); |
|||
b.Append(": "); |
|||
b.AppendLine(level.Value.Value?.ToString() ?? "(null)"); |
|||
b.AppendLine("--------"); |
|||
b.Append("Direct: "); |
|||
b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)"); |
|||
|
|||
foreach (var binding in level.Value.Bindings) |
|||
{ |
|||
b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty); |
|||
b.Append(binding.Description ?? binding.Observable.GetType().Name); |
|||
b.Append(": "); |
|||
b.AppendLine(binding.Value?.ToString() ?? "(null)"); |
|||
} |
|||
|
|||
first = false; |
|||
} |
|||
|
|||
return b.ToString(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when the value for a priority level changes.
|
|||
/// </summary>
|
|||
/// <param name="level">The priority level of the changed entry.</param>
|
|||
public void LevelValueChanged(PriorityLevel level) |
|||
{ |
|||
if (level.Priority <= ValuePriority) |
|||
{ |
|||
if (level.Value != AvaloniaProperty.UnsetValue) |
|||
{ |
|||
UpdateValue(level.Value, level.Priority); |
|||
} |
|||
else |
|||
{ |
|||
foreach (var i in _levels.Values.OrderBy(x => x.Priority)) |
|||
{ |
|||
if (i.Value != AvaloniaProperty.UnsetValue) |
|||
{ |
|||
UpdateValue(i.Value, i.Priority); |
|||
return; |
|||
} |
|||
} |
|||
|
|||
UpdateValue(AvaloniaProperty.UnsetValue, int.MaxValue); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called when a priority level encounters an error.
|
|||
/// </summary>
|
|||
/// <param name="level">The priority level of the changed entry.</param>
|
|||
/// <param name="error">The binding error.</param>
|
|||
public void LevelError(PriorityLevel level, BindingNotification error) |
|||
{ |
|||
Owner.LogError(Property, error.Error); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Causes a revalidation of the value.
|
|||
/// </summary>
|
|||
public void Revalidate() |
|||
{ |
|||
if (_validate != null) |
|||
{ |
|||
PriorityLevel level; |
|||
|
|||
if (_levels.TryGetValue(ValuePriority, out level)) |
|||
{ |
|||
UpdateValue(level.Value, level.Priority); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the <see cref="PriorityLevel"/> with the specified priority, creating it if it
|
|||
/// doesn't already exist.
|
|||
/// </summary>
|
|||
/// <param name="priority">The priority.</param>
|
|||
/// <returns>The priority level.</returns>
|
|||
private PriorityLevel GetLevel(int priority) |
|||
{ |
|||
PriorityLevel result; |
|||
|
|||
if (!_levels.TryGetValue(priority, out result)) |
|||
{ |
|||
result = new PriorityLevel(this, priority); |
|||
_levels.Add(priority, result); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the current <see cref="Value"/> and notifies all subscribers.
|
|||
/// </summary>
|
|||
/// <param name="value">The value to set.</param>
|
|||
/// <param name="priority">The priority level that the value came from.</param>
|
|||
private void UpdateValue(object value, int priority) |
|||
{ |
|||
var newValue = (value, priority); |
|||
|
|||
if (newValue == _value) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (_setter == null) |
|||
{ |
|||
_setter = Owner.GetNonDirectDeferredSetter(Property); |
|||
} |
|||
|
|||
_setter.SetAndNotifyCallback(Property, this, ref _value, newValue); |
|||
} |
|||
|
|||
void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value) |
|||
{ |
|||
SetAndNotify(ref backing, value); |
|||
} |
|||
|
|||
private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update) |
|||
{ |
|||
var val = update.value; |
|||
var notification = val as BindingNotification; |
|||
object castValue; |
|||
|
|||
if (notification != null) |
|||
{ |
|||
val = (notification.HasValue) ? notification.Value : null; |
|||
} |
|||
|
|||
if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue)) |
|||
{ |
|||
var old = backing.value; |
|||
|
|||
if (_validate != null && castValue != AvaloniaProperty.UnsetValue) |
|||
{ |
|||
castValue = _validate(castValue); |
|||
} |
|||
|
|||
backing = (castValue, update.priority); |
|||
|
|||
if (notification?.HasValue == true) |
|||
{ |
|||
notification.SetValue(castValue); |
|||
} |
|||
|
|||
if (notification == null || notification.HasValue) |
|||
{ |
|||
Owner?.Changed(Property, ValuePriority, old, Value); |
|||
} |
|||
|
|||
if (notification != null) |
|||
{ |
|||
Owner?.BindingNotificationReceived(Property, notification); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Error)?.Log( |
|||
LogArea.Binding, |
|||
Owner, |
|||
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", |
|||
Property.Name, |
|||
_valueType, |
|||
val, |
|||
val?.GetType()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal interface IBindingEntry : IPriorityValueEntry, IDisposable |
|||
{ |
|||
} |
|||
|
|||
internal class BindingEntry<T> : IBindingEntry, IPriorityValueEntry<T>, IObserver<BindingValue<T>> |
|||
{ |
|||
private readonly IAvaloniaObject _owner; |
|||
private IValueSink _sink; |
|||
private IDisposable? _subscription; |
|||
|
|||
public BindingEntry( |
|||
IAvaloniaObject owner, |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source, |
|||
BindingPriority priority, |
|||
IValueSink sink) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
Source = source; |
|||
Priority = priority; |
|||
_sink = sink; |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
public BindingPriority Priority { get; } |
|||
public IObservable<BindingValue<T>> Source { get; } |
|||
public Optional<T> Value { get; private set; } |
|||
Optional<object> IValue.Value => Value.ToObject(); |
|||
BindingPriority IValue.ValuePriority => Priority; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
_sink.Completed(Property, this); |
|||
} |
|||
|
|||
public void OnCompleted() => _sink.Completed(Property, this); |
|||
|
|||
public void OnError(Exception error) |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
|
|||
public void OnNext(BindingValue<T> value) |
|||
{ |
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
UpdateValue(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 = this; |
|||
var newValue = value; |
|||
|
|||
Dispatcher.UIThread.Post(() => instance.UpdateValue(newValue)); |
|||
} |
|||
} |
|||
|
|||
public void Start() |
|||
{ |
|||
_subscription = Source.Subscribe(this); |
|||
} |
|||
|
|||
public void Reparent(IValueSink sink) => _sink = sink; |
|||
|
|||
private void UpdateValue(BindingValue<T> value) |
|||
{ |
|||
if (value.Type == BindingValueType.DoNothing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var old = Value; |
|||
|
|||
if (value.Type != BindingValueType.DataValidationError) |
|||
{ |
|||
Value = value.ToOptional(); |
|||
} |
|||
|
|||
_sink.ValueChanged(Property, Priority, old, value); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class ConstantValueEntry<T> : IPriorityValueEntry<T> |
|||
{ |
|||
public ConstantValueEntry( |
|||
StyledPropertyBase<T> property, |
|||
T value, |
|||
BindingPriority priority) |
|||
{ |
|||
Property = property; |
|||
Value = value; |
|||
Priority = priority; |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
public BindingPriority Priority { get; } |
|||
public Optional<T> Value { get; private set; } |
|||
Optional<object> IValue.Value => Value.ToObject(); |
|||
BindingPriority IValue.ValuePriority => Priority; |
|||
|
|||
public void Reparent(IValueSink sink) { } |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal interface IPriorityValueEntry : IValue |
|||
{ |
|||
BindingPriority Priority { get; } |
|||
|
|||
void Reparent(IValueSink sink); |
|||
} |
|||
|
|||
internal interface IPriorityValueEntry<T> : IPriorityValueEntry, IValue<T> |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal interface IValue |
|||
{ |
|||
Optional<object> Value { get; } |
|||
BindingPriority ValuePriority { get; } |
|||
} |
|||
|
|||
internal interface IValue<T> : IValue |
|||
{ |
|||
new Optional<T> Value { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal interface IValueSink |
|||
{ |
|||
void ValueChanged<T>( |
|||
StyledPropertyBase<T> property, |
|||
BindingPriority priority, |
|||
Optional<T> oldValue, |
|||
BindingValue<T> newValue); |
|||
|
|||
void Completed(AvaloniaProperty property, IPriorityValueEntry entry); |
|||
} |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class PriorityValue<T> : IValue<T>, IValueSink |
|||
{ |
|||
private readonly IAvaloniaObject _owner; |
|||
private readonly IValueSink _sink; |
|||
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>(); |
|||
private Optional<T> _localValue; |
|||
|
|||
public PriorityValue( |
|||
IAvaloniaObject owner, |
|||
StyledPropertyBase<T> property, |
|||
IValueSink sink) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
_sink = sink; |
|||
} |
|||
|
|||
public PriorityValue( |
|||
IAvaloniaObject owner, |
|||
StyledPropertyBase<T> property, |
|||
IValueSink sink, |
|||
IPriorityValueEntry<T> existing) |
|||
: this(owner, property, sink) |
|||
{ |
|||
existing.Reparent(this); |
|||
_entries.Add(existing); |
|||
|
|||
if (existing.Value.HasValue) |
|||
{ |
|||
Value = existing.Value; |
|||
ValuePriority = existing.Priority; |
|||
} |
|||
} |
|||
|
|||
public StyledPropertyBase<T> Property { get; } |
|||
public Optional<T> Value { get; private set; } |
|||
public BindingPriority ValuePriority { get; private set; } |
|||
public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries; |
|||
Optional<object> IValue.Value => Value.ToObject(); |
|||
|
|||
public void ClearLocalValue() => UpdateEffectiveValue(); |
|||
|
|||
public void SetValue(T value, BindingPriority priority) |
|||
{ |
|||
if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
_localValue = value; |
|||
} |
|||
else |
|||
{ |
|||
var insert = FindInsertPoint(priority); |
|||
_entries.Insert(insert, new ConstantValueEntry<T>(Property, value, priority)); |
|||
} |
|||
|
|||
UpdateEffectiveValue(); |
|||
} |
|||
|
|||
public BindingEntry<T> AddBinding(IObservable<BindingValue<T>> source, BindingPriority priority) |
|||
{ |
|||
var binding = new BindingEntry<T>(_owner, Property, source, priority, this); |
|||
var insert = FindInsertPoint(binding.Priority); |
|||
_entries.Insert(insert, binding); |
|||
return binding; |
|||
} |
|||
|
|||
void IValueSink.ValueChanged<TValue>( |
|||
StyledPropertyBase<TValue> property, |
|||
BindingPriority priority, |
|||
Optional<TValue> oldValue, |
|||
BindingValue<TValue> newValue) |
|||
{ |
|||
_localValue = default; |
|||
UpdateEffectiveValue(); |
|||
} |
|||
|
|||
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) |
|||
{ |
|||
_entries.Remove((IPriorityValueEntry<T>)entry); |
|||
UpdateEffectiveValue(); |
|||
} |
|||
|
|||
private int FindInsertPoint(BindingPriority priority) |
|||
{ |
|||
var result = _entries.Count; |
|||
|
|||
for (var i = 0; i < _entries.Count; ++i) |
|||
{ |
|||
if (_entries[i].Priority < priority) |
|||
{ |
|||
result = i; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private void UpdateEffectiveValue() |
|||
{ |
|||
var reachedLocalValues = false; |
|||
var value = default(Optional<T>); |
|||
|
|||
for (var i = _entries.Count - 1; i >= 0; --i) |
|||
{ |
|||
var entry = _entries[i]; |
|||
|
|||
if (!reachedLocalValues && entry.Priority >= BindingPriority.LocalValue) |
|||
{ |
|||
reachedLocalValues = true; |
|||
|
|||
if (_localValue.HasValue) |
|||
{ |
|||
value = _localValue; |
|||
ValuePriority = BindingPriority.LocalValue; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (entry.Value.HasValue) |
|||
{ |
|||
value = entry.Value; |
|||
ValuePriority = entry.Priority; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (value != Value) |
|||
{ |
|||
var old = Value; |
|||
Value = value; |
|||
_sink.ValueChanged(Property, ValuePriority, old, value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
internal class AvaloniaPropertyBindingObservable<T> : LightweightObservableBase<BindingValue<T>>, IDescription |
|||
{ |
|||
private readonly WeakReference<IAvaloniaObject> _target; |
|||
private readonly AvaloniaProperty _property; |
|||
private T _value; |
|||
|
|||
public AvaloniaPropertyBindingObservable( |
|||
IAvaloniaObject target, |
|||
AvaloniaProperty property) |
|||
{ |
|||
_target = new WeakReference<IAvaloniaObject>(target); |
|||
_property = property; |
|||
} |
|||
|
|||
public string Description => $"{_target.GetType().Name}.{_property.Name}"; |
|||
|
|||
protected override void Initialize() |
|||
{ |
|||
if (_target.TryGetTarget(out var target)) |
|||
{ |
|||
_value = (T)target.GetValue(_property); |
|||
target.PropertyChanged += PropertyChanged; |
|||
} |
|||
} |
|||
|
|||
protected override void Deinitialize() |
|||
{ |
|||
if (_target.TryGetTarget(out var target)) |
|||
{ |
|||
target.PropertyChanged -= PropertyChanged; |
|||
} |
|||
} |
|||
|
|||
protected override void Subscribed(IObserver<BindingValue<T>> observer, bool first) |
|||
{ |
|||
observer.OnNext(new BindingValue<T>(_value)); |
|||
} |
|||
|
|||
private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) |
|||
{ |
|||
if (e.Property == _property) |
|||
{ |
|||
_value = (T)e.NewValue; |
|||
PublishNext(new BindingValue<T>(_value)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
internal class BindingValueAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
IObserver<T> |
|||
{ |
|||
private readonly IObservable<T> _source; |
|||
private IDisposable? _subscription; |
|||
|
|||
public BindingValueAdapter(IObservable<T> source) => _source = source; |
|||
public void OnCompleted() => PublishCompleted(); |
|||
public void OnError(Exception error) => PublishError(error); |
|||
public void OnNext(T value) => PublishNext(BindingValue<T>.FromUntyped(value)); |
|||
protected override void Subscribed() => _subscription = _source.Subscribe(this); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
} |
|||
|
|||
internal class BindingValueSubjectAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
ISubject<BindingValue<T>> |
|||
{ |
|||
private readonly ISubject<T> _source; |
|||
private readonly Inner _inner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public BindingValueSubjectAdapter(ISubject<T> source) |
|||
{ |
|||
_source = source; |
|||
_inner = new Inner(this); |
|||
} |
|||
|
|||
public void OnCompleted() => _source.OnCompleted(); |
|||
public void OnError(Exception error) => _source.OnError(error); |
|||
|
|||
public void OnNext(BindingValue<T> value) |
|||
{ |
|||
if (value.HasValue) |
|||
{ |
|||
_source.OnNext(value.Value); |
|||
} |
|||
} |
|||
|
|||
protected override void Subscribed() => _subscription = _source.Subscribe(_inner); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
|
|||
private class Inner : IObserver<T> |
|||
{ |
|||
private readonly BindingValueSubjectAdapter<T> _owner; |
|||
|
|||
public Inner(BindingValueSubjectAdapter<T> owner) => _owner = owner; |
|||
|
|||
public void OnCompleted() => _owner.PublishCompleted(); |
|||
public void OnError(Exception error) => _owner.PublishError(error); |
|||
public void OnNext(T value) => _owner.PublishNext(BindingValue<T>.FromUntyped(value)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
public static class BindingValueExtensions |
|||
{ |
|||
public static IObservable<BindingValue<T>> ToBindingValue<T>(this IObservable<T> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new BindingValueAdapter<T>(source); |
|||
} |
|||
|
|||
public static ISubject<BindingValue<T>> ToBindingValue<T>(this ISubject<T> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new BindingValueSubjectAdapter<T>(source); |
|||
} |
|||
|
|||
public static IObservable<object> ToUntyped<T>(this IObservable<BindingValue<T>> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new UntypedBindingAdapter<T>(source); |
|||
} |
|||
|
|||
public static ISubject<object> ToUntyped<T>(this ISubject<BindingValue<T>> source) |
|||
{ |
|||
source = source ?? throw new ArgumentNullException(nameof(source)); |
|||
return new UntypedBindingSubjectAdapter<T>(source); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Logging; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
internal class TypedBindingAdapter<T> : SingleSubscriberObservableBase<BindingValue<T>>, |
|||
IObserver<BindingValue<object>> |
|||
{ |
|||
private readonly IAvaloniaObject _target; |
|||
private readonly AvaloniaProperty<T> _property; |
|||
private readonly IObservable<BindingValue<object>> _source; |
|||
private IDisposable? _subscription; |
|||
|
|||
public TypedBindingAdapter( |
|||
IAvaloniaObject target, |
|||
AvaloniaProperty<T> property, |
|||
IObservable<BindingValue<object>> source) |
|||
{ |
|||
_target = target; |
|||
_property = property; |
|||
_source = source; |
|||
} |
|||
|
|||
public void OnNext(BindingValue<object> value) |
|||
{ |
|||
try |
|||
{ |
|||
PublishNext(value.Convert<T>()); |
|||
} |
|||
catch (InvalidCastException e) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Error)?.Log( |
|||
LogArea.Binding, |
|||
_target, |
|||
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", |
|||
_property.Name, |
|||
_property.PropertyType, |
|||
value.Value, |
|||
value.Value?.GetType()); |
|||
PublishNext(BindingValue<T>.BindingError(e)); |
|||
} |
|||
} |
|||
|
|||
public void OnCompleted() => PublishCompleted(); |
|||
public void OnError(Exception error) => PublishError(error); |
|||
|
|||
public static IObservable<BindingValue<T>> Create( |
|||
IAvaloniaObject target, |
|||
AvaloniaProperty<T> property, |
|||
IObservable<BindingValue<object>> source) |
|||
{ |
|||
return source is IObservable<BindingValue<T>> result ? |
|||
result : |
|||
new TypedBindingAdapter<T>(target, property, source); |
|||
} |
|||
|
|||
protected override void Subscribed() => _subscription = _source.Subscribe(this); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using System; |
|||
using System.Reactive.Subjects; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Reactive |
|||
{ |
|||
internal class UntypedBindingAdapter<T> : SingleSubscriberObservableBase<object?>, |
|||
IObserver<BindingValue<T>> |
|||
{ |
|||
private readonly IObservable<BindingValue<T>> _source; |
|||
private IDisposable? _subscription; |
|||
|
|||
public UntypedBindingAdapter(IObservable<BindingValue<T>> source) => _source = source; |
|||
public void OnCompleted() => PublishCompleted(); |
|||
public void OnError(Exception error) => PublishError(error); |
|||
public void OnNext(BindingValue<T> value) => value.ToUntyped(); |
|||
protected override void Subscribed() => _subscription = _source.Subscribe(this); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
} |
|||
|
|||
internal class UntypedBindingSubjectAdapter<T> : SingleSubscriberObservableBase<object?>, |
|||
ISubject<object?> |
|||
{ |
|||
private readonly ISubject<BindingValue<T>> _source; |
|||
private readonly Inner _inner; |
|||
private IDisposable? _subscription; |
|||
|
|||
public UntypedBindingSubjectAdapter(ISubject<BindingValue<T>> source) |
|||
{ |
|||
_source = source; |
|||
_inner = new Inner(this); |
|||
} |
|||
|
|||
public void OnCompleted() => _source.OnCompleted(); |
|||
public void OnError(Exception error) => _source.OnError(error); |
|||
public void OnNext(object? value) |
|||
{ |
|||
_source.OnNext(BindingValue<T>.FromUntyped(value)); |
|||
} |
|||
|
|||
protected override void Subscribed() => _subscription = _source.Subscribe(_inner); |
|||
protected override void Unsubscribed() => _subscription?.Dispose(); |
|||
|
|||
private class Inner : IObserver<BindingValue<T>> |
|||
{ |
|||
private readonly UntypedBindingSubjectAdapter<T> _owner; |
|||
|
|||
public Inner(UntypedBindingSubjectAdapter<T> owner) => _owner = owner; |
|||
|
|||
public void OnCompleted() => _owner.PublishCompleted(); |
|||
public void OnError(Exception error) => _owner.PublishError(error); |
|||
public void OnNext(BindingValue<T> value) => _owner.PublishNext(value.ToUntyped()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,205 +1,231 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Data; |
|||
using Avalonia.PropertyStore; |
|||
using Avalonia.Utilities; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
internal class ValueStore : IPriorityValueOwner |
|||
internal class ValueStore : IValueSink |
|||
{ |
|||
private readonly AvaloniaPropertyValueStore<object> _propertyValues; |
|||
private readonly AvaloniaPropertyValueStore<object> _deferredSetters; |
|||
private readonly AvaloniaObject _owner; |
|||
private readonly IValueSink _sink; |
|||
private readonly AvaloniaPropertyValueStore<object> _values; |
|||
|
|||
public ValueStore(AvaloniaObject owner) |
|||
{ |
|||
_owner = owner; |
|||
_propertyValues = new AvaloniaPropertyValueStore<object>(); |
|||
_deferredSetters = new AvaloniaPropertyValueStore<object>(); |
|||
_sink = _owner = owner; |
|||
_values = new AvaloniaPropertyValueStore<object>(); |
|||
} |
|||
|
|||
public IDisposable AddBinding( |
|||
AvaloniaProperty property, |
|||
IObservable<object> source, |
|||
BindingPriority priority) |
|||
public bool IsAnimating(AvaloniaProperty property) |
|||
{ |
|||
PriorityValue priorityValue; |
|||
|
|||
if (_propertyValues.TryGetValue(property, out var v)) |
|||
if (_values.TryGetValue(property, out var slot)) |
|||
{ |
|||
priorityValue = v as PriorityValue; |
|||
|
|||
if (priorityValue == null) |
|||
if (slot is IValue v) |
|||
{ |
|||
priorityValue = CreatePriorityValue(property); |
|||
priorityValue.SetValue(v, (int)BindingPriority.LocalValue); |
|||
_propertyValues.SetValue(property, priorityValue); |
|||
return v.ValuePriority < BindingPriority.LocalValue; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
priorityValue = CreatePriorityValue(property); |
|||
_propertyValues.AddValue(property, priorityValue); |
|||
} |
|||
|
|||
return priorityValue.Add(source, (int)priority); |
|||
return false; |
|||
} |
|||
|
|||
public void AddValue(AvaloniaProperty property, object value, int priority) |
|||
public bool IsSet(AvaloniaProperty property) |
|||
{ |
|||
PriorityValue priorityValue; |
|||
return TryGetValueUntyped(property, out _); |
|||
} |
|||
|
|||
if (_propertyValues.TryGetValue(property, out var v)) |
|||
public bool TryGetValue<T>(StyledPropertyBase<T> property, out T value) |
|||
{ |
|||
if (_values.TryGetValue(property, out var slot)) |
|||
{ |
|||
priorityValue = v as PriorityValue; |
|||
|
|||
if (priorityValue == null) |
|||
if (slot is IValue<T> v) |
|||
{ |
|||
if (priority == (int)BindingPriority.LocalValue) |
|||
{ |
|||
Validate(property, ref value); |
|||
_propertyValues.SetValue(property, value); |
|||
Changed(property, priority, v, value); |
|||
return; |
|||
} |
|||
else |
|||
if (v.Value.HasValue) |
|||
{ |
|||
priorityValue = CreatePriorityValue(property); |
|||
priorityValue.SetValue(v, (int)BindingPriority.LocalValue); |
|||
_propertyValues.SetValue(property, priorityValue); |
|||
value = v.Value.Value; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (value == AvaloniaProperty.UnsetValue) |
|||
else |
|||
{ |
|||
return; |
|||
value = (T)slot; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
value = default!; |
|||
return false; |
|||
} |
|||
|
|||
if (priority == (int)BindingPriority.LocalValue) |
|||
public bool TryGetValueUntyped(AvaloniaProperty property, out object? value) |
|||
{ |
|||
if (_values.TryGetValue(property, out var slot)) |
|||
{ |
|||
if (slot is IValue v) |
|||
{ |
|||
Validate(property, ref value); |
|||
_propertyValues.AddValue(property, value); |
|||
Changed(property, priority, AvaloniaProperty.UnsetValue, value); |
|||
return; |
|||
if (v.Value.HasValue) |
|||
{ |
|||
value = v.Value.Value; |
|||
return true; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
priorityValue = CreatePriorityValue(property); |
|||
_propertyValues.AddValue(property, priorityValue); |
|||
value = slot; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
priorityValue.SetValue(value, priority); |
|||
} |
|||
|
|||
public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) |
|||
{ |
|||
_owner.BindingNotificationReceived(property, notification); |
|||
} |
|||
|
|||
public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue) |
|||
{ |
|||
_owner.PriorityValueChanged(property, priority, oldValue, newValue); |
|||
value = default; |
|||
return false; |
|||
} |
|||
|
|||
public IDictionary<AvaloniaProperty, object> GetSetValues() |
|||
public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority) |
|||
{ |
|||
return _propertyValues.ToDictionary(); |
|||
if (_values.TryGetValue(property, out var slot)) |
|||
{ |
|||
SetExisting(slot, property, value, priority); |
|||
} |
|||
else if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
_values.AddValue(property, (object)value!); |
|||
_sink.ValueChanged(property, priority, default, value); |
|||
} |
|||
else |
|||
{ |
|||
var entry = new ConstantValueEntry<T>(property, value, priority); |
|||
_values.AddValue(property, entry); |
|||
_sink.ValueChanged(property, priority, default, value); |
|||
} |
|||
} |
|||
|
|||
public void LogError(AvaloniaProperty property, Exception e) |
|||
public IDisposable AddBinding<T>( |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source, |
|||
BindingPriority priority) |
|||
{ |
|||
_owner.LogBindingError(property, e); |
|||
if (_values.TryGetValue(property, out var slot)) |
|||
{ |
|||
return BindExisting(slot, property, source, priority); |
|||
} |
|||
else |
|||
{ |
|||
var entry = new BindingEntry<T>(_owner, property, source, priority, this); |
|||
_values.AddValue(property, entry); |
|||
entry.Start(); |
|||
return entry; |
|||
} |
|||
} |
|||
|
|||
public object GetValue(AvaloniaProperty property) |
|||
public void ClearLocalValue<T>(StyledPropertyBase<T> property) |
|||
{ |
|||
var result = AvaloniaProperty.UnsetValue; |
|||
|
|||
if (_propertyValues.TryGetValue(property, out var value)) |
|||
if (_values.TryGetValue(property, out var slot)) |
|||
{ |
|||
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; |
|||
} |
|||
if (slot is PriorityValue<T> p) |
|||
{ |
|||
p.ClearLocalValue(); |
|||
} |
|||
else |
|||
{ |
|||
var remove = slot is ConstantValueEntry<T> c ? |
|||
c.Priority == BindingPriority.LocalValue : |
|||
!(slot is IPriorityValueEntry<T>); |
|||
|
|||
return result; |
|||
if (remove) |
|||
{ |
|||
var old = TryGetValue(property, out var value) ? value : default; |
|||
_values.Remove(property); |
|||
_sink.ValueChanged( |
|||
property, |
|||
BindingPriority.LocalValue, |
|||
old, |
|||
BindingValue<T>.Unset); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool IsAnimating(AvaloniaProperty property) |
|||
void IValueSink.ValueChanged<T>( |
|||
StyledPropertyBase<T> property, |
|||
BindingPriority priority, |
|||
Optional<T> oldValue, |
|||
BindingValue<T> newValue) |
|||
{ |
|||
return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; |
|||
_sink.ValueChanged(property, priority, oldValue, newValue); |
|||
} |
|||
|
|||
public bool IsSet(AvaloniaProperty property) |
|||
void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) |
|||
{ |
|||
if (_propertyValues.TryGetValue(property, out var value)) |
|||
if (_values.TryGetValue(property, out var slot)) |
|||
{ |
|||
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; |
|||
if (slot == entry) |
|||
{ |
|||
_values.Remove(property); |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public void Revalidate(AvaloniaProperty property) |
|||
private void SetExisting<T>( |
|||
object slot, |
|||
StyledPropertyBase<T> property, |
|||
T value, |
|||
BindingPriority priority) |
|||
{ |
|||
if (_propertyValues.TryGetValue(property, out var value)) |
|||
if (slot is IPriorityValueEntry<T> e) |
|||
{ |
|||
(value as PriorityValue)?.Revalidate(); |
|||
var priorityValue = new PriorityValue<T>(_owner, property, this, e); |
|||
_values.SetValue(property, priorityValue); |
|||
priorityValue.SetValue(value, priority); |
|||
} |
|||
} |
|||
|
|||
public void VerifyAccess() => _owner.VerifyAccess(); |
|||
|
|||
private PriorityValue CreatePriorityValue(AvaloniaProperty property) |
|||
{ |
|||
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); |
|||
Func<object, object> validate2 = null; |
|||
|
|||
if (validate != null) |
|||
else if (slot is PriorityValue<T> p) |
|||
{ |
|||
validate2 = v => validate(_owner, v); |
|||
p.SetValue(value, priority); |
|||
} |
|||
else if (priority == BindingPriority.LocalValue) |
|||
{ |
|||
var old = (T)slot; |
|||
_values.SetValue(property, (object)value!); |
|||
_sink.ValueChanged(property, priority, old, value); |
|||
} |
|||
else |
|||
{ |
|||
var existing = new ConstantValueEntry<T>(property, (T)slot, BindingPriority.LocalValue); |
|||
var priorityValue = new PriorityValue<T>(_owner, property, this, existing); |
|||
priorityValue.SetValue(value, priority); |
|||
_values.SetValue(property, priorityValue); |
|||
} |
|||
|
|||
return new PriorityValue( |
|||
this, |
|||
property, |
|||
property.PropertyType, |
|||
validate2); |
|||
} |
|||
|
|||
private void Validate(AvaloniaProperty property, ref object value) |
|||
private IDisposable BindExisting<T>( |
|||
object slot, |
|||
StyledPropertyBase<T> property, |
|||
IObservable<BindingValue<T>> source, |
|||
BindingPriority priority) |
|||
{ |
|||
var validate = ((IStyledPropertyAccessor)property).GetValidationFunc(_owner.GetType()); |
|||
PriorityValue<T> priorityValue; |
|||
|
|||
if (validate != null && value != AvaloniaProperty.UnsetValue) |
|||
if (slot is IPriorityValueEntry<T> e) |
|||
{ |
|||
value = validate(_owner, value); |
|||
priorityValue = new PriorityValue<T>(_owner, property, this, e); |
|||
} |
|||
} |
|||
|
|||
private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property) |
|||
{ |
|||
if (_deferredSetters.TryGetValue(property, out var deferredSetter)) |
|||
else if (slot is PriorityValue<T> p) |
|||
{ |
|||
return (DeferredSetter<T>)deferredSetter; |
|||
priorityValue = p; |
|||
} |
|||
else |
|||
{ |
|||
var existing = new ConstantValueEntry<T>(property, (T)slot, BindingPriority.LocalValue); |
|||
priorityValue = new PriorityValue<T>(_owner, property, this, existing); |
|||
} |
|||
|
|||
var newDeferredSetter = new DeferredSetter<T>(); |
|||
|
|||
_deferredSetters.AddValue(property, newDeferredSetter); |
|||
|
|||
return newDeferredSetter; |
|||
} |
|||
|
|||
public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property) |
|||
{ |
|||
return GetDeferredSetter<object>(property); |
|||
} |
|||
|
|||
public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property) |
|||
{ |
|||
return GetDeferredSetter<T>(property); |
|||
var binding = priorityValue.AddBinding(source, priority); |
|||
_values.SetValue(property, priorityValue); |
|||
binding.Start(); |
|||
return binding; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,156 +0,0 @@ |
|||
// 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.Reactive.Subjects; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests |
|||
{ |
|||
public class AvaloniaObjectTests_Validation |
|||
{ |
|||
[Fact] |
|||
public void SetValue_Causes_Validation() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
target.SetValue(Class1.QuxProperty, 5); |
|||
Assert.Throws<ArgumentOutOfRangeException>(() => target.SetValue(Class1.QuxProperty, 25)); |
|||
Assert.Equal(5, target.GetValue(Class1.QuxProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Causes_Coercion() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
target.SetValue(Class1.QuxProperty, 5); |
|||
Assert.Equal(5, target.GetValue(Class1.QuxProperty)); |
|||
target.SetValue(Class1.QuxProperty, -5); |
|||
Assert.Equal(0, target.GetValue(Class1.QuxProperty)); |
|||
target.SetValue(Class1.QuxProperty, 15); |
|||
Assert.Equal(10, target.GetValue(Class1.QuxProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Revalidate_Causes_Recoercion() |
|||
{ |
|||
var target = new Class1(); |
|||
|
|||
target.SetValue(Class1.QuxProperty, 7); |
|||
Assert.Equal(7, target.GetValue(Class1.QuxProperty)); |
|||
target.MaxQux = 5; |
|||
target.Revalidate(Class1.QuxProperty); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Validation_Can_Be_Overridden() |
|||
{ |
|||
var target = new Class2(); |
|||
Assert.Throws<ArgumentOutOfRangeException>(() => target.SetValue(Class1.QuxProperty, 5)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Validation_Can_Be_Overridden_With_Null() |
|||
{ |
|||
var target = new Class3(); |
|||
target.SetValue(Class1.QuxProperty, 50); |
|||
Assert.Equal(50, target.GetValue(Class1.QuxProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_To_UnsetValue_Doesnt_Throw() |
|||
{ |
|||
var target = new Class1(); |
|||
var source = new Subject<object>(); |
|||
|
|||
target.Bind(Class1.QuxProperty, source); |
|||
|
|||
source.OnNext(AvaloniaProperty.UnsetValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Attached_Property_Should_Be_Validated() |
|||
{ |
|||
var target = new Class2(); |
|||
|
|||
target.SetValue(Class1.AttachedProperty, 15); |
|||
Assert.Equal(10, target.GetValue(Class1.AttachedProperty)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void PropertyChanged_Event_Uses_Coerced_Value() |
|||
{ |
|||
var inst = new Class1(); |
|||
inst.PropertyChanged += (sender, e) => |
|||
{ |
|||
Assert.Equal(10, e.NewValue); |
|||
}; |
|||
|
|||
inst.SetValue(Class1.QuxProperty, 15); |
|||
} |
|||
|
|||
private class Class1 : AvaloniaObject |
|||
{ |
|||
public static readonly StyledProperty<int> QuxProperty = |
|||
AvaloniaProperty.Register<Class1, int>("Qux", validate: Validate); |
|||
|
|||
public static readonly AttachedProperty<int> AttachedProperty = |
|||
AvaloniaProperty.RegisterAttached<Class1, Class2, int>("Attached", validate: Validate); |
|||
|
|||
public Class1() |
|||
{ |
|||
MaxQux = 10; |
|||
ErrorQux = 20; |
|||
} |
|||
|
|||
public int MaxQux { get; set; } |
|||
|
|||
public int ErrorQux { get; } |
|||
|
|||
private static int Validate(Class1 instance, int value) |
|||
{ |
|||
if (value > instance.ErrorQux) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(); |
|||
} |
|||
|
|||
return Math.Min(Math.Max(value, 0), ((Class1)instance).MaxQux); |
|||
} |
|||
|
|||
private static int Validate(Class2 instance, int value) |
|||
{ |
|||
return Math.Min(value, 10); |
|||
} |
|||
} |
|||
|
|||
private class Class2 : AvaloniaObject |
|||
{ |
|||
public static readonly StyledProperty<int> QuxProperty = |
|||
Class1.QuxProperty.AddOwner<Class2>(); |
|||
|
|||
static Class2() |
|||
{ |
|||
QuxProperty.OverrideValidation<Class2>(Validate); |
|||
} |
|||
|
|||
private static int Validate(Class2 instance, int value) |
|||
{ |
|||
if (value < 100) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(); |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
|
|||
private class Class3 : Class2 |
|||
{ |
|||
static Class3() |
|||
{ |
|||
QuxProperty.OverrideValidation<Class3>(null); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,314 +1,239 @@ |
|||
// 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 Avalonia.Utilities; |
|||
using Moq; |
|||
using System; |
|||
using System; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using System.Reactive.Subjects; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Data; |
|||
using Avalonia.PropertyStore; |
|||
using Moq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests |
|||
{ |
|||
public class PriorityValueTests |
|||
{ |
|||
private static readonly AvaloniaProperty TestProperty = |
|||
new StyledProperty<string>( |
|||
"Test", |
|||
typeof(PriorityValueTests), |
|||
new StyledPropertyMetadata<string>()); |
|||
private static readonly IValueSink NullSink = Mock.Of<IValueSink>(); |
|||
private static readonly IAvaloniaObject Owner = Mock.Of<IAvaloniaObject>(); |
|||
private static readonly StyledProperty<string> TestProperty = new StyledProperty<string>( |
|||
"Test", |
|||
typeof(PriorityValueTests), |
|||
new StyledPropertyMetadata<string>()); |
|||
|
|||
[Fact] |
|||
public void Initial_Value_Should_Be_UnsetValue() |
|||
public void Constructor_Should_Set_Value_Based_On_Initial_Entry() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
NullSink, |
|||
new ConstantValueEntry<string>(TestProperty, "1", BindingPriority.StyleTrigger)); |
|||
|
|||
Assert.Same(AvaloniaProperty.UnsetValue, target.Value); |
|||
Assert.Equal("1", target.Value.Value); |
|||
Assert.Equal(BindingPriority.StyleTrigger, target.ValuePriority); |
|||
} |
|||
|
|||
[Fact] |
|||
public void First_Binding_Sets_Value() |
|||
public void SetValue_LocalValue_Should_Not_Add_Entries() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
NullSink); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
target.SetValue("1", BindingPriority.LocalValue); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal("foo", target.Value); |
|||
Assert.Empty(target.Entries); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Changing_Binding_Should_Set_Value() |
|||
public void SetValue_Non_LocalValue_Should_Add_Entries() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var subject = new BehaviorSubject<string>("foo"); |
|||
var target = new PriorityValue<string>( |
|||
Owner, |
|||
TestProperty, |
|||
NullSink); |
|||
|
|||
target.Add(subject, 0); |
|||
Assert.Equal("foo", target.Value); |
|||
subject.OnNext("bar"); |
|||
Assert.Equal("bar", target.Value); |
|||
} |
|||
target.SetValue("1", BindingPriority.Style); |
|||
target.SetValue("2", BindingPriority.Animation); |
|||
|
|||
[Fact] |
|||
public void Setting_Direct_Value_Should_Override_Binding() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var result = target.Entries |
|||
.OfType<ConstantValueEntry<string>>() |
|||
.Select(x => x.Value.Value) |
|||
.ToList(); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
target.SetValue("bar", 0); |
|||
|
|||
Assert.Equal("bar", target.Value); |
|||
Assert.Equal(new[] { "1", "2" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_Firing_Should_Override_Direct_Value() |
|||
public void Binding_With_Same_Priority_Should_Be_Appended() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var source = new BehaviorSubject<object>("initial"); |
|||
|
|||
target.Add(source, 0); |
|||
Assert.Equal("initial", target.Value); |
|||
target.SetValue("first", 0); |
|||
Assert.Equal("first", target.Value); |
|||
source.OnNext("second"); |
|||
Assert.Equal("second", target.Value); |
|||
} |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
|
|||
[Fact] |
|||
public void Earlier_Binding_Firing_Should_Not_Override_Later() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var nonActive = new BehaviorSubject<object>("na"); |
|||
var source = new BehaviorSubject<object>("initial"); |
|||
|
|||
target.Add(nonActive, 1); |
|||
target.Add(source, 1); |
|||
Assert.Equal("initial", target.Value); |
|||
target.SetValue("first", 1); |
|||
Assert.Equal("first", target.Value); |
|||
nonActive.OnNext("second"); |
|||
Assert.Equal("first", target.Value); |
|||
} |
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.LocalValue); |
|||
|
|||
[Fact] |
|||
public void Binding_Completing_Should_Revert_To_Direct_Value() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var source = new BehaviorSubject<object>("initial"); |
|||
|
|||
target.Add(source, 0); |
|||
Assert.Equal("initial", target.Value); |
|||
target.SetValue("first", 0); |
|||
Assert.Equal("first", target.Value); |
|||
source.OnNext("second"); |
|||
Assert.Equal("second", target.Value); |
|||
source.OnCompleted(); |
|||
Assert.Equal("first", target.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Binding_With_Lower_Priority_Has_Precedence() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
|
|||
target.Add(Single("foo"), 1); |
|||
target.Add(Single("bar"), 0); |
|||
target.Add(Single("baz"), 1); |
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
Assert.Equal("bar", target.Value); |
|||
Assert.Equal(new[] { "1", "2" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Later_Binding_With_Same_Priority_Should_Take_Precedence() |
|||
public void Binding_With_Higher_Priority_Should_Be_Appended() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
|
|||
target.Add(Single("foo"), 1); |
|||
target.Add(Single("bar"), 0); |
|||
target.Add(Single("baz"), 0); |
|||
target.Add(Single("qux"), 1); |
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.Animation); |
|||
|
|||
Assert.Equal("baz", target.Value); |
|||
} |
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
[Fact] |
|||
public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var subject = new BehaviorSubject<string>("bar"); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
target.Add(subject, 1); |
|||
Assert.Equal("foo", target.Value); |
|||
subject.OnNext("baz"); |
|||
Assert.Equal("foo", target.Value); |
|||
Assert.Equal(new[] { "1", "2" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UnsetValue_Should_Fall_Back_To_Next_Binding() |
|||
public void Binding_With_Lower_Priority_Should_Be_Prepended() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var subject = new BehaviorSubject<object>("bar"); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
|
|||
target.Add(subject, 0); |
|||
target.Add(Single("foo"), 1); |
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.Style); |
|||
|
|||
Assert.Equal("bar", target.Value); |
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
subject.OnNext(AvaloniaProperty.UnsetValue); |
|||
|
|||
Assert.Equal("foo", target.Value); |
|||
Assert.Equal(new[] { "2", "1" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Adding_Value_Should_Call_OnNext() |
|||
public void Second_Binding_With_Lower_Priority_Should_Be_Inserted_In_Middle() |
|||
{ |
|||
var owner = GetMockOwner(); |
|||
var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
owner.Verify(x => x.Changed(target.Property, target.ValuePriority, AvaloniaProperty.UnsetValue, "foo")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Changing_Value_Should_Call_OnNext() |
|||
{ |
|||
var owner = GetMockOwner(); |
|||
var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); |
|||
var subject = new BehaviorSubject<object>("foo"); |
|||
target.AddBinding(source1, BindingPriority.LocalValue); |
|||
target.AddBinding(source2, BindingPriority.Style); |
|||
target.AddBinding(source3, BindingPriority.Style); |
|||
|
|||
target.Add(subject, 0); |
|||
subject.OnNext("bar"); |
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
owner.Verify(x => x.Changed(target.Property, target.ValuePriority, "foo", "bar")); |
|||
Assert.Equal(new[] { "2", "3", "1" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Disposing_A_Binding_Should_Revert_To_Next_Value() |
|||
public void Competed_Binding_Should_Be_Removed() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
var disposable = target.Add(Single("bar"), 0); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
Assert.Equal("bar", target.Value); |
|||
disposable.Dispose(); |
|||
Assert.Equal("foo", target.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Disposing_A_Binding_Should_Remove_BindingEntry() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
target.AddBinding(source1, BindingPriority.LocalValue).Start(); |
|||
target.AddBinding(source2, BindingPriority.Style).Start(); |
|||
target.AddBinding(source3, BindingPriority.Style).Start(); |
|||
source3.OnCompleted(); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
var disposable = target.Add(Single("bar"), 0); |
|||
var result = target.Entries |
|||
.OfType<BindingEntry<string>>() |
|||
.Select(x => x.Source) |
|||
.OfType<Source>() |
|||
.Select(x => x.Id) |
|||
.ToList(); |
|||
|
|||
Assert.Equal(2, target.GetBindings().Count()); |
|||
disposable.Dispose(); |
|||
Assert.Single(target.GetBindings()); |
|||
Assert.Equal(new[] { "2", "1" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Completing_A_Binding_Should_Revert_To_Previous_Binding() |
|||
public void Value_Should_Come_From_Last_Entry() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var source = new BehaviorSubject<object>("bar"); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
var source2 = new Source("2"); |
|||
var source3 = new Source("3"); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
target.Add(source, 0); |
|||
target.AddBinding(source1, BindingPriority.LocalValue).Start(); |
|||
target.AddBinding(source2, BindingPriority.Style).Start(); |
|||
target.AddBinding(source3, BindingPriority.Style).Start(); |
|||
|
|||
Assert.Equal("bar", target.Value); |
|||
source.OnCompleted(); |
|||
Assert.Equal("foo", target.Value); |
|||
Assert.Equal("1", target.Value.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Completing_A_Binding_Should_Revert_To_Lower_Priority() |
|||
public void LocalValue_Should_Override_LocalValue_Binding() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var source = new BehaviorSubject<object>("bar"); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
|
|||
target.Add(Single("foo"), 1); |
|||
target.Add(source, 0); |
|||
target.AddBinding(source1, BindingPriority.LocalValue).Start(); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal("bar", target.Value); |
|||
source.OnCompleted(); |
|||
Assert.Equal("foo", target.Value); |
|||
Assert.Equal("2", target.Value.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Completing_A_Binding_Should_Remove_BindingEntry() |
|||
public void LocalValue_Should_Override_Style_Binding() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string)); |
|||
var subject = new BehaviorSubject<object>("bar"); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
|
|||
target.Add(Single("foo"), 0); |
|||
target.Add(subject, 0); |
|||
target.AddBinding(source1, BindingPriority.Style).Start(); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
Assert.Equal(2, target.GetBindings().Count()); |
|||
subject.OnCompleted(); |
|||
Assert.Single(target.GetBindings()); |
|||
Assert.Equal("2", target.Value.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Direct_Value_Should_Be_Coerced() |
|||
public void LocalValue_Should_Not_Override_Animation_Binding() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10)); |
|||
var target = new PriorityValue<string>(Owner, TestProperty, NullSink); |
|||
var source1 = new Source("1"); |
|||
|
|||
target.SetValue(5, 0); |
|||
Assert.Equal(5, target.Value); |
|||
target.SetValue(15, 0); |
|||
Assert.Equal(10, target.Value); |
|||
} |
|||
target.AddBinding(source1, BindingPriority.Animation).Start(); |
|||
target.SetValue("2", BindingPriority.LocalValue); |
|||
|
|||
[Fact] |
|||
public void Bound_Value_Should_Be_Coerced() |
|||
{ |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10)); |
|||
var source = new Subject<object>(); |
|||
|
|||
target.Add(source, 0); |
|||
source.OnNext(5); |
|||
Assert.Equal(5, target.Value); |
|||
source.OnNext(15); |
|||
Assert.Equal(10, target.Value); |
|||
Assert.Equal("1", target.Value.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Revalidate_Should_ReCoerce_Value() |
|||
private class Source : IObservable<BindingValue<string>> |
|||
{ |
|||
var max = 10; |
|||
var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max)); |
|||
var source = new Subject<object>(); |
|||
|
|||
target.Add(source, 0); |
|||
source.OnNext(5); |
|||
Assert.Equal(5, target.Value); |
|||
source.OnNext(15); |
|||
Assert.Equal(10, target.Value); |
|||
max = 12; |
|||
target.Revalidate(); |
|||
Assert.Equal(12, target.Value); |
|||
} |
|||
private IObserver<BindingValue<string>> _observer; |
|||
|
|||
/// <summary>
|
|||
/// Returns an observable that returns a single value but does not complete.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of the observable.</typeparam>
|
|||
/// <param name="value">The value.</param>
|
|||
/// <returns>The observable.</returns>
|
|||
private IObservable<T> Single<T>(T value) |
|||
{ |
|||
return Observable.Never<T>().StartWith(value); |
|||
} |
|||
public Source(string id) => Id = id; |
|||
public string Id { get; } |
|||
|
|||
private static Mock<IPriorityValueOwner> GetMockOwner() |
|||
{ |
|||
var owner = new Mock<IPriorityValueOwner>(); |
|||
owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny<AvaloniaProperty>())).Returns(new DeferredSetter<object>()); |
|||
return owner; |
|||
public IDisposable Subscribe(IObserver<BindingValue<string>> observer) |
|||
{ |
|||
_observer = observer; |
|||
observer.OnNext(Id); |
|||
return Disposable.Empty; |
|||
} |
|||
|
|||
public void OnCompleted() => _observer.OnCompleted(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue