A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

374 lines
12 KiB

using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data.Converters;
using Avalonia.Logging;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Data.Core
{
/// <summary>
/// Binds to an expression on an object using a type value converter to convert the values
/// that are sent and received.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
public class BindingExpression : LightweightObservableBase<object?>, ISubject<object?>, IDescription
{
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object? _fallbackValue;
private readonly object? _targetNullValue;
private readonly BindingPriority _priority;
InnerListener? _innerListener;
WeakReference<object>? _value;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
public BindingExpression(ExpressionObserver inner, Type targetType)
: this(inner, targetType, DefaultValueConverter.Instance)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public BindingExpression(
ExpressionObserver inner,
Type targetType,
IValueConverter converter,
object? converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
: this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="targetNullValue">
/// The value to use when the binding result is null.
/// </param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
/// </param>
/// <param name="priority">The binding priority.</param>
public BindingExpression(
ExpressionObserver inner,
Type targetType,
object? fallbackValue,
object? targetNullValue,
IValueConverter converter,
object? converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
{
_ = inner ?? throw new ArgumentNullException(nameof(inner));
_ = targetType ?? throw new ArgumentNullException(nameof(targetType));
_ = converter ?? throw new ArgumentNullException(nameof(converter));
_inner = inner;
_targetType = targetType;
Converter = converter;
ConverterParameter = converterParameter;
_fallbackValue = fallbackValue;
_targetNullValue = targetNullValue;
_priority = priority;
}
/// <summary>
/// Gets the converter to use on the expression.
/// </summary>
public IValueConverter Converter { get; }
/// <summary>
/// Gets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object? ConverterParameter { get; }
/// <inheritdoc/>
string? IDescription.Description => _inner.Expression;
/// <inheritdoc/>
public void OnCompleted()
{
}
/// <inheritdoc/>
public void OnError(Exception error)
{
}
/// <inheritdoc/>
public void OnNext(object? value)
{
if (value == BindingOperations.DoNothing)
{
return;
}
using (_inner.Subscribe(_ => { }))
{
var type = _inner.ResultType;
if (type != null)
{
var converted = Converter.ConvertBack(
value,
type,
ConverterParameter,
CultureInfo.CurrentCulture);
if (converted == BindingOperations.DoNothing)
{
return;
}
if (converted == AvaloniaProperty.UnsetValue)
{
converted = TypeUtilities.Default(type);
_inner.SetValue(converted, _priority);
}
else if (converted is BindingNotification notification)
{
if (notification.ErrorType == BindingErrorType.None)
{
throw new AvaloniaInternalException(
"IValueConverter should not return non-errored BindingNotification.");
}
PublishNext(notification);
if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
if (TypeUtilities.TryConvert(
type,
_fallbackValue,
CultureInfo.InvariantCulture,
out converted))
{
_inner.SetValue(converted, _priority);
}
else
{
Logger.TryGet(LogEventLevel.Error, LogArea.Binding)?.Log(
this,
"Could not convert FallbackValue {FallbackValue} to {Type}",
_fallbackValue,
type);
}
}
}
else
{
_inner.SetValue(converted, _priority);
}
}
}
}
protected override void Initialize() => _innerListener = new InnerListener(this);
protected override void Deinitialize() => _innerListener?.Dispose();
protected override void Subscribed(IObserver<object> observer, bool first)
{
if (!first && _value != null && _value.TryGetTarget(out var val))
{
observer.OnNext(val);
}
}
/// <inheritdoc/>
private object? ConvertValue(object? value)
{
if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue)
{
return _targetNullValue;
}
if (value == BindingOperations.DoNothing)
{
return value;
}
var notification = value as BindingNotification;
if (notification == null)
{
var converted = Converter.Convert(
value,
_targetType,
ConverterParameter,
CultureInfo.CurrentCulture);
if (converted == BindingOperations.DoNothing)
{
return converted;
}
notification = converted as BindingNotification;
if (notification?.ErrorType == BindingErrorType.None)
{
converted = notification.Value;
}
if (_fallbackValue != AvaloniaProperty.UnsetValue &&
(converted == AvaloniaProperty.UnsetValue || converted is BindingNotification))
{
var fallback = ConvertFallback();
converted = Merge(converted, fallback);
}
return converted;
}
else
{
return ConvertValue(notification);
}
}
private BindingNotification ConvertValue(BindingNotification notification)
{
if (notification.HasValue)
{
var converted = ConvertValue(notification.Value);
notification = Merge(notification, converted);
}
else if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
var fallback = ConvertFallback();
notification = Merge(notification, fallback);
}
return notification;
}
private BindingNotification ConvertFallback()
{
object? converted;
if (_fallbackValue == AvaloniaProperty.UnsetValue)
{
throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value");
}
if (TypeUtilities.TryConvert(
_targetType,
_fallbackValue,
CultureInfo.InvariantCulture,
out converted))
{
return new BindingNotification(converted);
}
else
{
return new BindingNotification(
new InvalidCastException(
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"),
BindingErrorType.Error);
}
}
private static BindingNotification Merge(object a, BindingNotification b)
{
var an = a as BindingNotification;
if (an != null)
{
Merge(an, b);
return an;
}
else
{
return b;
}
}
private static BindingNotification Merge(BindingNotification a, object? b)
{
var bn = b as BindingNotification;
if (bn != null)
{
Merge(a, bn);
}
else
{
a.SetValue(b);
}
return a;
}
private static BindingNotification Merge(BindingNotification a, BindingNotification b)
{
if (b.HasValue)
{
a.SetValue(b.Value);
}
else
{
a.ClearValue();
}
if (b.Error != null)
{
a.AddError(b.Error, b.ErrorType);
}
return a;
}
public class InnerListener : IObserver<object?>, IDisposable
{
private readonly BindingExpression _owner;
private readonly IDisposable _dispose;
public InnerListener(BindingExpression owner)
{
_owner = owner;
_dispose = owner._inner.Subscribe(this);
}
public void Dispose() => _dispose.Dispose();
public void OnCompleted() => _owner.PublishCompleted();
public void OnError(Exception error) => _owner.PublishError(error);
public void OnNext(object? value)
{
if (value == BindingOperations.DoNothing)
{
return;
}
var converted = _owner.ConvertValue(value);
if (converted == BindingOperations.DoNothing)
{
return;
}
_owner._value = converted is not null ? new WeakReference<object>(converted) : null;
_owner.PublishNext(converted);
}
}
}
}