using System; using System.Collections.Generic; 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 FromUntyped(value, typeof(T)); } /// /// Creates a from an object, handling the special values /// , and /// . /// /// The untyped value. /// The runtime target type. /// The typed binding value. public static BindingValue FromUntyped(object? value, Type targetType) { if (value == AvaloniaProperty.UnsetValue) return Unset; else if (value == BindingOperations.DoNothing) return DoNothing; var type = BindingValueType.Value; T? v = default; Exception? error = null; List? errors = null; if (value is BindingNotification n) { error = n.Error; type = n.ErrorType switch { BindingErrorType.Error => BindingValueType.BindingError, BindingErrorType.DataValidationError => BindingValueType.DataValidationError, _ => BindingValueType.Value, }; if (n.HasValue) type |= BindingValueType.HasValue; value = n.Value; } if ((type & BindingValueType.HasValue) != 0) { if (TypeUtilities.TryConvertImplicit(targetType, value, out var typed)) v = (T)typed!; else { var e = new InvalidCastException( $"Unable to convert object '{value ?? "(null)"}' " + $"of type '{value?.GetType()}' to type '{targetType}'."); if (error is null) error = e; else { errors ??= new List() { error }; errors.Add(e); } type = BindingValueType.BindingError; } } if (errors is not null) error = new AggregateException(errors); return new BindingValue(type, v, error); } /// /// 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<>."); } } } }