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..33d12e8eb5 100644
--- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
+++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs
@@ -64,14 +64,19 @@ namespace Avalonia.Data
object anchor = null,
bool enableDataValidation = false)
{
- if (Converter == null)
+ var targetType = targetProperty?.PropertyType ?? typeof(object);
+ 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)))
{
- throw new NotSupportedException("MultiBinding without Converter not currently supported.");
+ converter = new StringFormatMultiValueConverter(StringFormat, converter);
}
-
- 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 input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter));
var mode = Mode == BindingMode.Default ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode;
@@ -87,25 +92,16 @@ 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)
{
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);
}
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";
}
}
}