Browse Source

Merge pull request #2598 from OronDF343/master

Support StringFormat without Converter in MultiBinding
pull/2698/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
13b10b0fb6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      src/Avalonia.Base/Data/Converters/StringFormatMultiValueConverter.cs
  2. 28
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  3. 40
      tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs
  4. 31
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

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

28
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -64,14 +64,19 @@ namespace Avalonia.Data
object anchor = null, object anchor = null,
bool enableDataValidation = false) 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 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 ? var mode = Mode == BindingMode.Default ?
targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; 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 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) if (converted == AvaloniaProperty.UnsetValue && FallbackValue != null)
{ {
converted = FallbackValue; 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; return converted;
} }
} }

40
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs

@ -5,11 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Layout;
using Xunit; using Xunit;
namespace Avalonia.Markup.UnitTests.Data namespace Avalonia.Markup.UnitTests.Data
@ -21,7 +20,30 @@ namespace Avalonia.Markup.UnitTests.Data
{ {
var textBlock = new TextBlock 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 var target = new MultiBinding
@ -30,8 +52,8 @@ namespace Avalonia.Markup.UnitTests.Data
Converter = new SumOfDoublesConverter(), Converter = new SumOfDoublesConverter(),
Bindings = Bindings =
{ {
new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), new Binding(nameof(Class1.Foo)),
new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), new Binding(nameof(Class1.Bar)),
} }
}; };
@ -45,7 +67,7 @@ namespace Avalonia.Markup.UnitTests.Data
{ {
var textBlock = new TextBlock var textBlock = new TextBlock
{ {
DataContext = new MultiBindingTests_Converters.Class1(), DataContext = new Class1(),
}; };
var target = new MultiBinding var target = new MultiBinding
@ -54,12 +76,12 @@ namespace Avalonia.Markup.UnitTests.Data
Converter = new SumOfDoublesConverter(), Converter = new SumOfDoublesConverter(),
Bindings = Bindings =
{ {
new Binding(nameof(MultiBindingTests_Converters.Class1.Foo)), new Binding(nameof(Class1.Foo)),
new Binding(nameof(MultiBindingTests_Converters.Class1.Bar)), new Binding(nameof(Class1.Bar)),
} }
}; };
textBlock.Bind(TextBlock.WidthProperty, target); textBlock.Bind(Layoutable.WidthProperty, target);
Assert.Equal(3.0, textBlock.Width); Assert.Equal(3.0, textBlock.Width);
} }

31
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] [Fact]
public void Binding_OneWayToSource_Works() public void Binding_OneWayToSource_Works()
{ {
@ -356,6 +385,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
private class WindowViewModel private class WindowViewModel
{ {
public bool ShowInTaskbar { get; set; } public bool ShowInTaskbar { get; set; }
public string Greeting1 { get; set; } = "Hello";
public string Greeting2 { get; set; } = "World";
} }
} }
} }

Loading…
Cancel
Save