diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 986e2cf012..9eec5d6b2b 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -21,6 +21,7 @@ namespace Avalonia.Data.Core private readonly ExpressionObserver _inner; private readonly Type _targetType; private readonly object _fallbackValue; + private readonly object _targetNullValue; private readonly BindingPriority _priority; InnerListener _innerListener; WeakReference _value; @@ -51,7 +52,7 @@ namespace Avalonia.Data.Core IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) { } @@ -63,6 +64,9 @@ namespace Avalonia.Data.Core /// /// The value to use when the binding is unable to produce a value. /// + /// + /// The value to use when the binding result is null. + /// /// The value converter to use. /// /// A parameter to pass to . @@ -72,6 +76,7 @@ namespace Avalonia.Data.Core ExpressionObserver inner, Type targetType, object fallbackValue, + object targetNullValue, IValueConverter converter, object converterParameter = null, BindingPriority priority = BindingPriority.LocalValue) @@ -85,6 +90,7 @@ namespace Avalonia.Data.Core Converter = converter; ConverterParameter = converterParameter; _fallbackValue = fallbackValue; + _targetNullValue = targetNullValue; _priority = priority; } @@ -196,6 +202,11 @@ namespace Avalonia.Data.Core /// private object ConvertValue(object value) { + if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue) + { + return _targetNullValue; + } + if (value == BindingOperations.DoNothing) { return value; diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index dbe5800e55..61d0f7c83b 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -26,6 +26,7 @@ namespace Avalonia.Data public Binding() { FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; } /// @@ -60,6 +61,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -209,6 +215,7 @@ namespace Avalonia.Data observer, targetType, fallback, + TargetNullValue, converter ?? DefaultValueConverter.Instance, ConverterParameter, Priority); diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 29945e25c3..4325ad8a74 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -37,6 +37,11 @@ namespace Avalonia.Data /// public object FallbackValue { get; set; } + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + /// /// Gets or sets the binding mode. /// @@ -57,6 +62,12 @@ namespace Avalonia.Data /// public string StringFormat { get; set; } + public MultiBinding() + { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; + } + /// public InstancedBinding Initiate( IAvaloniaObject target, @@ -102,6 +113,11 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); + if (converted == null) + { + converted = TargetNullValue; + } + if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs index d51f56f558..8eb8d1b5db 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs @@ -139,6 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -160,6 +161,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), 42, + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -181,6 +183,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -203,6 +206,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.StringValue, true), typeof(int), "bar", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); var result = await target.Take(1); @@ -238,6 +242,7 @@ namespace Avalonia.Base.UnitTests.Data.Core ExpressionObserver.Create(data, o => o.DoubleValue), typeof(string), "9.8", + AvaloniaProperty.UnsetValue, DefaultValueConverter.Instance); target.OnNext("foo"); @@ -353,6 +358,29 @@ namespace Avalonia.Base.UnitTests.Data.Core GC.KeepAlive(data); } + [Fact] + public async Task Null_Value_Should_Use_TargetNullValue() + { + var data = new Class1 { StringValue = "foo" }; + + var target = new BindingExpression( + ExpressionObserver.Create(data, o => o.StringValue), + typeof(string), + AvaloniaProperty.UnsetValue, + "bar", + DefaultValueConverter.Instance); + + object result = null; + target.Subscribe(x => result = x); + + Assert.Equal("foo", result); + + data.StringValue = null; + Assert.Equal("bar", result); + + GC.KeepAlive(data); + } + private class Class1 : NotifyingBase { private string _stringValue; diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 7e053392c7..cd33fae6f3 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -405,6 +405,24 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal(42, target.Value); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + var source = new Source { Foo = null }; + + var binding = new Binding + { + Source = source, + Path = "Foo", + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + [Fact] public void Null_Path_Should_Bind_To_DataContext() { diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index 78c538d99d..f3e8ff9248 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -94,6 +94,28 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("fallback", target.Text); } + [Fact] + public void Should_Return_TargetNullValue_When_Value_Is_Null() + { + var target = new TextBlock(); + + var binding = new MultiBinding + { + Converter = new NullValueConverter(), + Bindings = new[] + { + new Binding { Path = "A" }, + new Binding { Path = "B" }, + new Binding { Path = "C" }, + }, + TargetNullValue = "(null)", + }; + + target.Bind(TextBlock.TextProperty, binding); + + Assert.Equal("(null)", target.Text); + } + private class ConcatConverter : IMultiValueConverter { public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) @@ -109,5 +131,13 @@ namespace Avalonia.Markup.UnitTests.Data return AvaloniaProperty.UnsetValue; } } + + private class NullValueConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return null; + } + } } }