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); }