From b519ecc121220a972dcc20585e5d36e75cba8252 Mon Sep 17 00:00:00 2001 From: OronDF343 Date: Fri, 31 May 2019 10:14:28 +0300 Subject: [PATCH 1/3] Support StringFormat without Converter in MultiBinding --- .../StringFormatMultiValueConverter.cs | 46 +++++++++++++++++++ .../Avalonia.Markup/Data/MultiBinding.cs | 23 ++++------ .../Data/MultiBindingTests_Converters.cs | 40 ++++++++++++---- 3 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs diff --git a/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs new file mode 100644 index 0000000000..b190f06be5 --- /dev/null +++ b/src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Avalonia.Data.Converters +{ + /// + /// A multi-value converter which calls + /// + public class StringFormatMultiValueConverter : IMultiValueConverter + { + /// + /// Initializes a new instance of the class. + /// + /// The format string. + /// + /// An optional inner converter to be called before the format takes place. + /// + public StringFormatMultiValueConverter(string format, IMultiValueConverter inner) + { + Contract.Requires(format != null); + + Format = format; + Inner = inner; + } + + /// + /// Gets an inner value converter which will be called before the string format takes place. + /// + public IMultiValueConverter Inner { get; } + + /// + /// Gets the format string. + /// + public string Format { get; } + + /// + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + return Inner == null + ? string.Format(culture, Format, values.ToArray()) + : string.Format(culture, Format, Inner.Convert(values, targetType, parameter, culture)); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index eb2a8df2eb..45e4739eb0 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -64,16 +64,20 @@ namespace Avalonia.Data object anchor = null, bool enableDataValidation = false) { - if (Converter == null) - { - throw new NotSupportedException("MultiBinding without Converter not currently supported."); - } - var targetType = targetProperty?.PropertyType ?? typeof(object); var children = Bindings.Select(x => x.Initiate(target, null)); var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType)); var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; + + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + Converter = new StringFormatMultiValueConverter(StringFormat, Converter); + } switch (mode) { @@ -97,15 +101,6 @@ namespace Avalonia.Data converted = FallbackValue; } - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) - { - converted = string.Format(culture, StringFormat, converted); - } - return converted; } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs index bd4b5b9d04..d3e3ce5507 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs @@ -5,11 +5,10 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Converters; -using Avalonia.Data.Core; +using Avalonia.Layout; using Xunit; namespace Avalonia.Markup.UnitTests.Data @@ -21,7 +20,30 @@ namespace Avalonia.Markup.UnitTests.Data { var textBlock = new TextBlock { - DataContext = new MultiBindingTests_Converters.Class1(), + DataContext = new Class1(), + }; + + var target = new MultiBinding + { + StringFormat = "{0:0.0} + {1:00}", + Bindings = + { + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), + } + }; + + textBlock.Bind(TextBlock.TextProperty, target); + + Assert.Equal("1.0 + 02", textBlock.Text); + } + + [Fact] + public void StringFormat_Should_Be_Applied_After_Converter() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), }; var target = new MultiBinding @@ -30,8 +52,8 @@ namespace Avalonia.Markup.UnitTests.Data Converter = new SumOfDoublesConverter(), Bindings = { - new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), - new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), } }; @@ -45,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Data { var textBlock = new TextBlock { - DataContext = new MultiBindingTests_Converters.Class1(), + DataContext = new Class1(), }; var target = new MultiBinding @@ -54,12 +76,12 @@ namespace Avalonia.Markup.UnitTests.Data Converter = new SumOfDoublesConverter(), Bindings = { - new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), - new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), + new Binding(nameof(Class1.Foo)), + new Binding(nameof(Class1.Bar)), } }; - textBlock.Bind(TextBlock.WidthProperty, target); + textBlock.Bind(Layoutable.WidthProperty, target); Assert.Equal(3.0, textBlock.Width); } From d6b273a40cc89efcdb44cb45e472cdff79f082ac Mon Sep 17 00:00:00 2001 From: OronDF343 Date: Fri, 7 Jun 2019 12:05:53 +0300 Subject: [PATCH 2/3] Fixed StringFormat being applied multiple times --- src/Markup/Avalonia.Markup/Data/MultiBinding.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 45e4739eb0..33d12e8eb5 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -65,19 +65,20 @@ namespace Avalonia.Data bool enableDataValidation = false) { var targetType = targetProperty?.PropertyType ?? typeof(object); - var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType)); - var mode = Mode == BindingMode.Default ? - targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; - + var converter = Converter; // We only respect `StringFormat` if the type of the property we're assigning to will // accept a string. Note that this is slightly different to WPF in that WPF only applies // `StringFormat` for target type `string` (not `object`). if (!string.IsNullOrWhiteSpace(StringFormat) && (targetType == typeof(string) || targetType == typeof(object))) { - Converter = new StringFormatMultiValueConverter(StringFormat, Converter); + converter = new StringFormatMultiValueConverter(StringFormat, converter); } + + var children = Bindings.Select(x => x.Initiate(target, null)); + var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter)); + var mode = Mode == BindingMode.Default ? + targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; switch (mode) { @@ -91,10 +92,10 @@ namespace Avalonia.Data } } - private object ConvertValue(IList values, Type targetType) + private object ConvertValue(IList values, Type targetType, IMultiValueConverter converter) { var culture = CultureInfo.CurrentCulture; - var converted = Converter.Convert(values, targetType, ConverterParameter, culture); + var converted = converter.Convert(values, targetType, ConverterParameter, culture); if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null) { From fdd5d7afec4151862327a0b225359fd15291cf08 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 26 Jun 2019 19:40:12 +0200 Subject: [PATCH 3/3] Added failing (skipped) test for #2592. --- .../Xaml/BindingTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 14abebcdb5..3930608515 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -331,6 +331,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact(Skip="Issue #2592")] + public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + textBlock.DataContext = new WindowViewModel(); + window.ApplyTemplate(); + + Assert.Equal("Hello World!", textBlock.Text); + } + } + [Fact] public void Binding_OneWayToSource_Works() { @@ -356,6 +385,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml private class WindowViewModel { public bool ShowInTaskbar { get; set; } + public string Greeting1 { get; set; } = "Hello"; + public string Greeting2 { get; set; } = "World"; } } }