diff --git a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
new file mode 100644
index 0000000000..fc695762b8
--- /dev/null
+++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Globalization;
+
+namespace Avalonia.Data.Converters
+{
+ ///
+ /// A value converter which calls
+ ///
+ public class StringFormatValueConverter : IValueConverter
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The format string.
+ ///
+ /// An optional inner converter to be called before the format takes place.
+ ///
+ public StringFormatValueConverter(string format, IValueConverter 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 IValueConverter Inner { get; }
+
+ ///
+ /// Gets the format string.
+ ///
+ public string Format { get; }
+
+ ///
+ 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);
+ }
+
+ ///
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotSupportedException("Two way bindings are not supported with a string format");
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
index 08ba26573f..1ceef5c824 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/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; }
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs
index 03678c3b5a..4f18c682b4 100644
--- a/src/Markup/Avalonia.Markup/Data/Binding.cs
+++ b/src/Markup/Avalonia.Markup/Data/Binding.cs
@@ -84,6 +84,11 @@ namespace Avalonia.Data
///
public object Source { get; set; }
+ ///
+ /// Gets or sets the string format.
+ ///
+ public string StringFormat { get; set; }
+
public WeakReference DefaultAnchor { get; set; }
///
@@ -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);
diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs
new file mode 100644
index 0000000000..123aadfda5
--- /dev/null
+++ b/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(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(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";
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
index f327e9ccf2..fef9dfb675 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
+++ b/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 = @"
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var textBlock = window.FindControl("textBlock");
+
+ textBlock.DataContext = new { Foo = "world" };
+ window.ApplyTemplate();
+
+ Assert.Equal("Hello world", textBlock.Text);
+ }
+ }
}
}