Browse Source

Merge pull request #2598 from OronDF343/master

Support StringFormat without Converter in MultiBinding
Steven Kirk 6 years ago
parent
commit
13b10b0fb6

+ 46 - 0
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
+{
+    /// <summary>
+    /// A multi-value converter which calls <see cref="string.Format(string, object)"/>
+    /// </summary>
+    public class StringFormatMultiValueConverter : IMultiValueConverter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StringFormatMultiValueConverter"/> class.
+        /// </summary>
+        /// <param name="format">The format string.</param>
+        /// <param name="inner">
+        /// An optional inner converter to be called before the format takes place.
+        /// </param>
+        public StringFormatMultiValueConverter(string format, IMultiValueConverter inner)
+        {
+            Contract.Requires<ArgumentNullException>(format != null);
+
+            Format = format;
+            Inner = inner;
+        }
+
+        /// <summary>
+        /// Gets an inner value converter which will be called before the string format takes place.
+        /// </summary>
+        public IMultiValueConverter Inner { get; }
+
+        /// <summary>
+        /// Gets the format string.
+        /// </summary>
+        public string Format { get; }
+
+        /// <inheritdoc/>
+        public object Convert(IList<object> 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));
+        }
+    }
+}

+ 12 - 16
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<object> values, Type targetType)
+        private object ConvertValue(IList<object> 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;
         }
     }

+ 31 - 9
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);
         }

+ 31 - 0
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 = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <TextBlock Name='textBlock'>
+        <TextBlock.Text>
+            <MultiBinding StringFormat='\{0\} \{1\}!'>
+                <Binding Path='Greeting1'/>
+                <Binding Path='Greeting2'/>
+            </MultiBinding>
+        </TextBlock.Text>
+    </TextBlock> 
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var textBlock = window.FindControl<TextBlock>("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";
         }
     }
 }