using System; using System.Diagnostics; using Avalonia.Utilities; namespace Avalonia.Data { /// /// Describes the type of a . /// [Flags] public enum BindingValueType { /// /// An unset value: the target property will revert to its unbound state until a new /// binding value is produced. /// UnsetValue = 0, /// /// Do nothing: the binding value will be ignored. /// DoNothing = 1, /// /// A simple value. /// Value = 2 | HasValue, /// /// A binding error, such as a missing source property. /// BindingError = 3 | HasError, /// /// A data validation error. /// DataValidationError = 4 | HasError, /// /// A binding error with a fallback value. /// BindingErrorWithFallback = BindingError | HasValue, /// /// A data validation error with a fallback value. /// DataValidationErrorWithFallback = DataValidationError | HasValue, TypeMask = 0x00ff, HasValue = 0x0100, HasError = 0x0200, } /// /// A value passed into a binding. /// /// The value type. /// /// 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 /// property: /// /// - : a simple value /// - : the target property will revert to its unbound /// state until a new binding value is produced. Represented by /// in an untyped context /// - : the binding value will be ignored. Represented /// by in an untyped context /// - : a binding error, such as a missing source /// property, with an optional fallback value /// - : a data validation error, with an /// optional fallback value /// /// To create a new binding value you can: /// /// - For a simple value, call the constructor or use an implicit /// conversion from /// - For an unset value, use or simply `default` /// - For other types, call one of the static factory methods /// public readonly struct BindingValue { private readonly T _value; /// /// Initializes a new instance of the struct with a type of /// /// /// The value. 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; } /// /// Gets a value indicating whether the binding value represents either a binding or data /// validation error. /// public bool HasError => Type.HasAllFlags(BindingValueType.HasError); /// /// Gets a value indicating whether the binding value has a value. /// public bool HasValue => Type.HasAllFlags(BindingValueType.HasValue); /// /// Gets the type of the binding value. /// public BindingValueType Type { get; } /// /// Gets the binding value or fallback value. /// /// /// is false. /// public T Value => HasValue ? _value! : throw new InvalidOperationException("BindingValue has no value."); /// /// Gets the binding or data validation error. /// public Exception? Error { get; } /// /// Converts the binding value to an . /// /// public Optional ToOptional() => HasValue ? new Optional(_value) : default; /// public override string ToString() => HasError ? $"Error: {Error!.Message}" : _value?.ToString() ?? "(null)"; /// /// Converts the value to untyped representation, using , /// and where /// appropriate. /// /// The untyped representation of the binding value. 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("Invalid BindingValueType."), }; } /// /// Returns a new binding value with the specified value. /// /// The new value. /// The new binding value. /// /// The binding type is or /// . /// public BindingValue 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(type | BindingValueType.HasValue, value, Error); } /// /// Gets the value of the binding value if present, otherwise the default value. /// /// The value. public T? GetValueOrDefault() => HasValue ? _value : default; /// /// Gets the value of the binding value if present, otherwise a default value. /// /// The default value. /// The value. public T? GetValueOrDefault(T defaultValue) => HasValue ? _value : defaultValue; /// /// Gets the value if present, otherwise the default value. /// /// /// The value if present and of the correct type, `default(TResult)` if the value is /// not present or of an incorrect type. /// public TResult? GetValueOrDefault() { return HasValue ? _value is TResult result ? result : default : default; } /// /// Gets the value of the binding value if present, otherwise a default value. /// /// The default value. /// /// 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 if the /// value is not present. /// public TResult? GetValueOrDefault(TResult defaultValue) { return HasValue ? _value is TResult result ? result : default : defaultValue; } /// /// Creates a from an object, handling the special values /// and . /// /// The untyped value. /// The typed binding value. public static BindingValue FromUntyped(object? value) { return value switch { UnsetValueType _ => Unset, DoNothingType _ => DoNothing, BindingNotification n => n.ToBindingValue().Cast(), _ => new BindingValue((T)value!) }; } /// /// Creates a binding value from an instance of the underlying value type. /// /// The value. public static implicit operator BindingValue(T value) => new BindingValue(value); /// /// Creates a binding value from an . /// /// The optional value. public static implicit operator BindingValue(Optional optional) { return optional.HasValue ? optional.Value : Unset; } /// /// Returns a binding value with a type of . /// public static BindingValue Unset => new BindingValue(BindingValueType.UnsetValue, default, null); /// /// Returns a binding value with a type of . /// public static BindingValue DoNothing => new BindingValue(BindingValueType.DoNothing, default, null); /// /// Returns a binding value with a type of . /// /// The binding error. public static BindingValue BindingError(Exception e) { e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.BindingError, default, e); } /// /// Returns a binding value with a type of . /// /// The binding error. /// The fallback value. public static BindingValue BindingError(Exception e, T fallbackValue) { e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e); } /// /// Returns a binding value with a type of or /// . /// /// The binding error. /// The fallback value. public static BindingValue BindingError(Exception e, Optional fallbackValue) { e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? BindingValueType.BindingErrorWithFallback : BindingValueType.BindingError, fallbackValue.HasValue ? fallbackValue.Value : default, e); } /// /// Returns a binding value with a type of . /// /// The data validation error. public static BindingValue DataValidationError(Exception e) { e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.DataValidationError, default, e); } /// /// Returns a binding value with a type of . /// /// The data validation error. /// The fallback value. public static BindingValue DataValidationError(Exception e, T fallbackValue) { e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e); } /// /// Returns a binding value with a type of or /// . /// /// The binding error. /// The fallback value. public static BindingValue DataValidationError(Exception e, Optional fallbackValue) { e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? BindingValueType.DataValidationErrorWithFallback : BindingValueType.DataValidationError, fallbackValue.HasValue ? fallbackValue.Value : default, e); } [Conditional("DEBUG")] 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<>."); } if (value is BindingValue) { throw new InvalidOperationException("BindingValue cannot be wrapped in a BindingValue<>."); } } } public static class BindingValueExtensions { /// /// Casts the type of a using only the C# cast operator. /// /// The target type. /// The binding value. /// The cast value. public static BindingValue Cast(this BindingValue value) { return value.Type switch { BindingValueType.DoNothing => BindingValue.DoNothing, BindingValueType.UnsetValue => BindingValue.Unset, BindingValueType.Value => new BindingValue((T)value.Value!), BindingValueType.BindingError => BindingValue.BindingError(value.Error!), BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( value.Error!, (T)value.Value!), BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( value.Error!, (T)value.Value!), _ => throw new NotSupportedException("Invalid BindingValue type."), }; } /// /// Casts the type of a using the implicit conversions /// allowed by the C# language. /// /// The target type. /// The binding value. /// The cast value. /// /// Note that this method uses reflection and as such may be slow. /// public static BindingValue Convert(this BindingValue value) { return value.Type switch { BindingValueType.DoNothing => BindingValue.DoNothing, BindingValueType.UnsetValue => BindingValue.Unset, BindingValueType.Value => new BindingValue(TypeUtilities.ConvertImplicit(value.Value!)), BindingValueType.BindingError => BindingValue.BindingError(value.Error!), BindingValueType.BindingErrorWithFallback => BindingValue.BindingError( value.Error!, TypeUtilities.ConvertImplicit(value.Value!)), BindingValueType.DataValidationError => BindingValue.DataValidationError(value.Error!), BindingValueType.DataValidationErrorWithFallback => BindingValue.DataValidationError( value.Error!, TypeUtilities.ConvertImplicit(value.Value!)), _ => throw new NotSupportedException("Invalid BindingValue type."), }; } } }