From 83f786f4287d3465ef43d55bb4dce6e521962940 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 6 Apr 2020 18:35:36 +0200 Subject: [PATCH] Merge pull request #3693 from donandren/issues/3692 Multibinding issue #3692 fixed --- .../Converters/FuncMultiValueConverter.cs | 18 +- .../AvaloniaObjectTests_MultiBinding.cs | 178 ++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs diff --git a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs index 6e1c4cb0e3..887641b5c3 100644 --- a/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/FuncMultiValueConverter.cs @@ -30,7 +30,23 @@ namespace Avalonia.Data.Converters /// public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) { - var converted = values.OfType().ToList(); + //standard OfType skip null values, even they are valid for the Type + static IEnumerable OfTypeWithDefaultSupport(IList list) + { + foreach (object obj in list) + { + if (obj is TIn result) + { + yield return result; + } + else if (Equals(obj, default(TIn))) + { + yield return default(TIn); + } + } + } + + var converted = OfTypeWithDefaultSupport(values).ToList(); if (converted.Count == values.Count) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs new file mode 100644 index 0000000000..757160a0a4 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_MultiBinding.cs @@ -0,0 +1,178 @@ +// 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.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_MultiBinding + { + [Fact] + public void Should_Update() + { + var target = new Class1(); + + var b = new Subject(); + + var mb = new MultiBinding() + { + Converter = StringJoinConverter, + Bindings = new[] + { + b.ToBinding() + } + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal(null, target.Foo); + + b.OnNext("Foo"); + + Assert.Equal("Foo", target.Foo); + + b.OnNext("Bar"); + + Assert.Equal("Bar", target.Foo); + } + + [Fact] + public void Should_Update_With_Multiple_Bindings() + { + var target = new Class1(); + + var bindings = Enumerable.Range(0, 3).Select(i => new BehaviorSubject("Empty")).ToArray(); + + var mb = new MultiBinding() + { + Converter = StringJoinConverter, + Bindings = bindings.Select(b => b.ToBinding()).ToArray() + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal("Empty,Empty,Empty", target.Foo); + + bindings[0].OnNext("Foo"); + + Assert.Equal("Foo,Empty,Empty", target.Foo); + + bindings[1].OnNext("Bar"); + + Assert.Equal("Foo,Bar,Empty", target.Foo); + + bindings[2].OnNext("Baz"); + + Assert.Equal("Foo,Bar,Baz", target.Foo); + } + + [Fact] + public void Should_Update_When_Null_Value_In_Bindings() + { + var target = new Class1(); + + var b = new Subject(); + + var mb = new MultiBinding() + { + Converter = StringJoinConverter, + Bindings = new[] + { + b.ToBinding() + } + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal(null, target.Foo); + + b.OnNext("Foo"); + + Assert.Equal("Foo", target.Foo); + + b.OnNext(null); + + Assert.Equal("", target.Foo); + } + + [Fact] + public void Should_Update_When_Null_Value_In_Bindings_With_StringFormat() + { + var target = new Class1(); + + var b = new Subject(); + + var mb = new MultiBinding() + { + StringFormat = "Converted: {0}", + Bindings = new[] + { + b.ToBinding() + } + }; + target.Bind(Class1.FooProperty, mb); + + Assert.Equal(null, target.Foo); + b.OnNext("Foo"); + Assert.Equal("Converted: Foo", target.Foo); + b.OnNext(null); + Assert.Equal("Converted: ", target.Foo); + } + + [Fact] + public void MultiValueConverter_Should_Not_Skip_Valid_Null_ReferenceType_Value() + { + var target = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); + + object value = target.Convert(new[] { "Foo", "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal("Foo,Bar,Baz", value); + + value = target.Convert(new[] { null, "Bar", "Baz" }, typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal(",Bar,Baz", value); + } + + [Fact] + public void MultiValueConverter_Should_Not_Skip_Valid_Default_ValueType_Value() + { + var target = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); + + IList create(string[] values) => + values.Select(v => (object)(v != null ? new StringValueTypeWrapper() { Value = v } : default)).ToList(); + + object value = target.Convert(create(new[] { "Foo", "Bar", "Baz" }), typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal("Foo,Bar,Baz", value); + + value = target.Convert(create(new[] { null, "Bar", "Baz" }), typeof(string), null, CultureInfo.InvariantCulture); + + Assert.Equal(",Bar,Baz", value); + } + + private struct StringValueTypeWrapper + { + public string Value; + + public override string ToString() => Value; + } + + private static IMultiValueConverter StringJoinConverter = new FuncMultiValueConverter(v => string.Join(",", v.ToArray())); + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty FooProperty = + AvaloniaProperty.Register("Foo"); + + public string Foo + { + get => GetValue(FooProperty); + set => SetValue(FooProperty, value); + } + } + } +}