Browse Source

Merge pull request #1684 from AvaloniaUI/feature/stringformat

Add StringFormat to Binding.
Jeremy Koritzinsky 7 years ago
parent
commit
5289ddd423

+ 49 - 0
src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs

@@ -0,0 +1,49 @@
+using System;
+using System.Globalization;
+
+namespace Avalonia.Data.Converters
+{
+    /// <summary>
+    /// A value converter which calls <see cref="string.Format(string, object)"/>
+    /// </summary>
+    public class StringFormatValueConverter : IValueConverter
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StringFormatValueConverter"/> 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 StringFormatValueConverter(string format, IValueConverter 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 IValueConverter Inner { get; }
+
+        /// <summary>
+        /// Gets the format string.
+        /// </summary>
+        public string Format { get; }
+
+        /// <inheritdoc/>
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            value = Inner?.Convert(value, targetType, parameter, culture) ?? value;
+            return string.Format(culture, Format, value);
+        }
+
+        /// <inheritdoc/>
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotSupportedException("Two way bindings are not supported with a string format");
+        }
+    }
+}

+ 4 - 1
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@@ -43,8 +43,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                 Path = Path,
                 Priority = Priority,
                 Source = Source,
+                StringFormat = StringFormat,
                 RelativeSource = RelativeSource,
-                DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
+                DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext))
             };
         }
 
@@ -79,6 +80,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
 
         public object Source { get; set; }
 
+        public string StringFormat { get; set; }
+
         public RelativeSource RelativeSource { get; set; }
     }
 }

+ 19 - 2
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -84,6 +84,11 @@ namespace Avalonia.Data
         /// </summary>
         public object Source { get; set; }
 
+        /// <summary>
+        /// Gets or sets the string format.
+        /// </summary>
+        public string StringFormat { get; set; }
+
         public WeakReference DefaultAnchor { get; set; }
 
         /// <summary>
@@ -181,11 +186,23 @@ namespace Avalonia.Data
                 fallback = null;
             }
 
+            var converter = Converter;
+            var targetType = targetProperty?.PropertyType ?? typeof(object);
+
+            // 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 StringFormatValueConverter(StringFormat, converter);
+            }
+
             var subject = new BindingExpression(
                 observer,
-                targetProperty?.PropertyType ?? typeof(object),
+                targetType,
                 fallback,
-                Converter ?? DefaultValueConverter.Instance,
+                converter ?? DefaultValueConverter.Instance,
                 ConverterParameter,
                 Priority);
 

+ 146 - 0
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs

@@ -0,0 +1,146 @@
+// 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;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Data.Converters;
+using Avalonia.Data.Core;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+    public class BindingTests_Converters
+    {
+        [Fact]
+        public void Converter_Should_Be_Used()
+        {
+            var textBlock = new TextBlock
+            {
+                DataContext = new Class1(),
+            };
+
+            var target = new Binding(nameof(Class1.Foo))
+            {
+                Converter = StringConverters.NullOrEmpty,
+            };
+
+            var expressionObserver = (BindingExpression)target.Initiate(
+                textBlock, 
+                TextBlock.TextProperty).Observable;
+
+            Assert.Same(StringConverters.NullOrEmpty, expressionObserver.Converter);
+        }
+
+        public class When_Binding_To_String
+        {
+            [Fact]
+            public void StringFormatConverter_Should_Be_Used_When_Binding_Has_StringFormat()
+            {
+                var textBlock = new TextBlock
+                {
+                    DataContext = new Class1(),
+                };
+
+                var target = new Binding(nameof(Class1.Foo))
+                {
+                    StringFormat = "Hello {0}",
+                };
+
+                var expressionObserver = (BindingExpression)target.Initiate(
+                    textBlock,
+                    TextBlock.TextProperty).Observable;
+
+                Assert.IsType<StringFormatValueConverter>(expressionObserver.Converter);
+            }
+        }
+
+        public class When_Binding_To_Object
+        {
+            [Fact]
+            public void StringFormatConverter_Should_Be_Used_When_Binding_Has_StringFormat()
+            {
+                var textBlock = new TextBlock
+                {
+                    DataContext = new Class1(),
+                };
+
+                var target = new Binding(nameof(Class1.Foo))
+                {
+                    StringFormat = "Hello {0}",
+                };
+
+                var expressionObserver = (BindingExpression)target.Initiate(
+                    textBlock,
+                    TextBlock.TagProperty).Observable;
+
+                Assert.IsType<StringFormatValueConverter>(expressionObserver.Converter);
+            }
+        }
+
+        public class When_Binding_To_Non_String_Or_Object
+        {
+            [Fact]
+            public void StringFormatConverter_Should_Not_Be_Used_When_Binding_Has_StringFormat()
+            {
+                var textBlock = new TextBlock
+                {
+                    DataContext = new Class1(),
+                };
+
+                var target = new Binding(nameof(Class1.Foo))
+                {
+                    StringFormat = "Hello {0}",
+                };
+
+                var expressionObserver = (BindingExpression)target.Initiate(
+                    textBlock,
+                    TextBlock.MarginProperty).Observable;
+
+                Assert.Same(DefaultValueConverter.Instance, expressionObserver.Converter);
+            }
+        }
+
+        [Fact]
+        public void StringFormat_Should_Be_Applied()
+        {
+            var textBlock = new TextBlock
+            {
+                DataContext = new Class1(),
+            };
+
+            var target = new Binding(nameof(Class1.Foo))
+            {
+                StringFormat = "Hello {0}",
+            };
+
+            textBlock.Bind(TextBlock.TextProperty, target);
+
+            Assert.Equal("Hello foo", textBlock.Text);
+        }
+
+        [Fact]
+        public void StringFormat_Should_Be_Applied_After_Converter()
+        {
+            var textBlock = new TextBlock
+            {
+                DataContext = new Class1(),
+            };
+
+            var target = new Binding(nameof(Class1.Foo))
+            {
+                Converter = StringConverters.NotNullOrEmpty,
+                StringFormat = "Hello {0}",
+            };
+
+            textBlock.Bind(TextBlock.TextProperty, target);
+
+            Assert.Equal("Hello True", textBlock.Text);
+        }
+
+        private class Class1
+        {
+            public string Foo { get; set; } = "foo";
+        }
+    }
+}

+ 22 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@@ -308,5 +308,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock));
             }
         }
+
+        [Fact]
+        public void Binding_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' Text='{Binding Foo, StringFormat=Hello \{0\}}'/> 
+</Window>"; 
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml); 
+                var textBlock = window.FindControl<TextBlock>("textBlock"); 
+
+                textBlock.DataContext = new { Foo = "world" };
+                window.ApplyTemplate(); 
+
+                Assert.Equal("Hello world", textBlock.Text); 
+            }
+        } 
     }
 }