Browse Source

Added default type converter for bindings.

To convert between basic types. Closes #256. Closes #270.
pull/278/head
Steven Kirk 11 years ago
parent
commit
62b8c5a40c
  1. 41
      samples/BindingTest/MainWindow.paml
  2. 7
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  3. 4
      src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs
  4. 34
      src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs
  5. 66
      src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs
  6. 2
      src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs
  7. 56
      src/Markup/Perspex.Markup/DefaultValueConverter.cs
  8. 34
      src/Markup/Perspex.Markup/IValueConverter.cs
  9. 2
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  10. 3
      src/Perspex.Base/PerspexObject.cs
  11. 36
      src/Perspex.Base/Utilities/TypeUtilities.cs
  12. 13
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs
  13. 27
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs
  14. 47
      tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs
  15. 1
      tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

41
samples/BindingTest/MainWindow.paml

@ -1,22 +1,29 @@
<Window xmlns="https://github.com/perspex">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding StringValue}"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneTime}"/>
<TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWayToSource}"/>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding StringValue}"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneTime}"/>
<TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWayToSource}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Items[1].StringValue}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Negated Bindings"/>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding BooleanString}"/>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox>
</StackPanel>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Items[1].StringValue}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Negated Bindings"/>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding BooleanString}"/>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox>
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Numeric Bindings"/>
<TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding DoubleValue, Mode=TwoWay}"/>
<TextBlock Text="{Binding DoubleValue}"/>
</StackPanel>
</StackPanel>
</Window>

7
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -7,6 +7,7 @@ namespace BindingTest.ViewModels
public class MainWindowViewModel : ReactiveObject
{
private string _booleanString = "True";
private double _doubleValue = 5.0;
private string _stringValue = "Simple Binding";
public MainWindowViewModel()
@ -35,6 +36,12 @@ namespace BindingTest.ViewModels
set { this.RaiseAndSetIfChanged(ref _booleanString, value); }
}
public double DoubleValue
{
get { return _doubleValue; }
set { this.RaiseAndSetIfChanged(ref _doubleValue, value); }
}
public string StringValue
{
get { return _stringValue; }

4
src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs

@ -29,7 +29,9 @@ namespace Perspex.Markup.Xaml.Binding
public void Bind(IObservablePropertyBag instance, PerspexProperty property)
{
var subject = new ExpressionSubject(CreateExpressionObserver(instance, property));
var subject = new ExpressionSubject(
CreateExpressionObserver(instance, property),
property.PropertyType);
if (subject != null)
{

34
src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs

@ -79,9 +79,43 @@ namespace Perspex.Markup.Binding
}
}
/// <summary>
/// Gets the type of the expression result or null if the expression could not be
/// evaluated.
/// </summary>
public Type ResultType
{
get
{
IncrementCount();
try
{
return (Leaf as PropertyAccessorNode)?.PropertyType;
}
finally
{
DecrementCount();
}
}
}
/// <inheritdoc/>
string IDescription.Description => Expression;
/// <summary>
/// Gets the leaf node.
/// </summary>
private ExpressionNode Leaf
{
get
{
var node = _node;
while (node.Next != null) node = node.Next;
return node;
}
}
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{

66
src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs

@ -2,25 +2,43 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Globalization;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace Perspex.Markup.Binding
{
/// <summary>
/// Turns an <see cref="ExpressionObserver"/> into a subject that can be bound two-ways.
/// Turns an <see cref="ExpressionObserver"/> into a subject that can be bound two-way with
/// a value converter.
/// </summary>
public class ExpressionSubject : ISubject<object>, IDescription
{
private IValueConverter _converter;
private ExpressionObserver _inner;
private Type _targetType;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
public ExpressionSubject(ExpressionObserver inner)
/// <param name="targetType">The type to convert the value to.</param>
public ExpressionSubject(ExpressionObserver inner, Type targetType)
: this(inner, targetType, DefaultValueConverter.Instance)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
/// <param name="targetType">The type to convert the value to.</param>
/// <param name="converter">The value converter to use.</param>
public ExpressionSubject(ExpressionObserver inner, Type targetType, IValueConverter converter)
{
_converter = converter;
_inner = inner;
_targetType = targetType;
}
/// <inheritdoc/>
@ -39,13 +57,53 @@ namespace Perspex.Markup.Binding
/// <inheritdoc/>
public void OnNext(object value)
{
_inner.SetValue(value);
var type = _inner.ResultType;
if (type != null)
{
object converted;
if (ConvertBack(value, type, out converted))
{
_inner.SetValue(converted);
}
}
}
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer)
{
return _inner.Subscribe(observer);
return _inner
.Select(x => Convert(x, _targetType))
.Subscribe(observer);
}
private object Convert(object value, Type type)
{
try
{
return _converter.Convert(value, type, null, CultureInfo.CurrentUICulture);
}
catch
{
// TODO: Log something.
return PerspexProperty.UnsetValue;
}
}
private bool ConvertBack(object value, Type type, out object result)
{
try
{
result = _converter.ConvertBack(value, type, null, CultureInfo.CurrentUICulture);
return true;
}
catch
{
// TODO: Log something.
result = null;
return false;
}
}
}
}

2
src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs

@ -23,6 +23,8 @@ namespace Perspex.Markup.Binding
public string PropertyName { get; }
public Type PropertyType => _propertyInfo?.PropertyType;
public override bool SetValue(object value)
{
if (Next != null)

56
src/Markup/Perspex.Markup/DefaultValueConverter.cs

@ -0,0 +1,56 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Globalization;
using Perspex.Utilities;
namespace Perspex.Markup
{
/// <summary>
/// Provides a default set of value conversions for bindings that do not specify a value
/// converter.
/// </summary>
public class DefaultValueConverter : IValueConverter
{
/// <summary>
/// Gets an instance of a <see cref="DefaultValueConverter"/>.
/// </summary>
public static readonly DefaultValueConverter Instance = new DefaultValueConverter();
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="targetType">The type of the target.</param>
/// <param name="parameter">A user-defined parameter.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>The converted value.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
object result;
if (TypeUtilities.TryConvert(targetType, value, culture, out result))
{
return result;
}
else
{
throw new InvalidCastException($"Cannot convert value from {value.GetType()} to {targetType}");
}
}
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="targetType">The type of the target.</param>
/// <param name="parameter">A user-defined parameter.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>The converted value.</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Convert(value, targetType, parameter, culture);
}
}
}

34
src/Markup/Perspex.Markup/IValueConverter.cs

@ -0,0 +1,34 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Globalization;
namespace Perspex.Markup
{
/// <summary>
/// Converts a binding value.
/// </summary>
public interface IValueConverter
{
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="targetType">The type of the target.</param>
/// <param name="parameter">A user-defined parameter.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>The converted value.</returns>
object Convert(object value, Type targetType, object parameter, CultureInfo culture);
/// <summary>
/// Converts a value.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="targetType">The type of the target.</param>
/// <param name="parameter">A user-defined parameter.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>The converted value.</returns>
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
}

2
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -49,6 +49,8 @@
<Compile Include="Binding\PropertyAccessorNode.cs" />
<Compile Include="Binding\ExpressionNode.cs" />
<Compile Include="Binding\ExpressionObserver.cs" />
<Compile Include="DefaultValueConverter.cs" />
<Compile Include="IValueConverter.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>

3
src/Perspex.Base/PerspexObject.cs

@ -596,6 +596,7 @@ namespace Perspex
{
Contract.Requires<ArgumentNullException>(property != null);
VerifyAccess();
if (property.IsDirect)
{
property = GetRegistered(property);
@ -611,7 +612,7 @@ namespace Perspex
GetDescription(source));
return source
.Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType, false))
.Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType))
.Subscribe(x => SetValue(property, x));
}
else

36
src/Perspex.Base/Utilities/TypeUtilities.cs

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
@ -11,7 +12,7 @@ namespace Perspex.Utilities
/// <summary>
/// Provides utilities for working with types at runtime.
/// </summary>
internal static class TypeUtilities
public static class TypeUtilities
{
private static readonly Dictionary<Type, List<Type>> Conversions = new Dictionary<Type, List<Type>>()
{
@ -32,9 +33,8 @@ namespace Perspex.Utilities
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="result">If sucessful, contains the cast value.</param>
/// <param name="allowUnset">Allow <see cref="PerspexProperty.UnsetValue"/>.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns>
public static bool TryCast(Type to, object value, out object result, bool allowUnset = true)
public static bool TryCast(Type to, object value, out object result)
{
Contract.Requires<ArgumentNullException>(to != null);
@ -47,7 +47,7 @@ namespace Perspex.Utilities
var from = value.GetType();
if (allowUnset && value == PerspexProperty.UnsetValue)
if (value == PerspexProperty.UnsetValue)
{
result = value;
return true;
@ -79,20 +79,42 @@ namespace Perspex.Utilities
return false;
}
/// <summary>
/// Try to convert a value to a type, using <see cref="System.Convert"/> if possible,
/// otherwise using <see cref="TryCast(Type, object, out object, bool)"/>.
/// </summary>
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="culture">The culture to use.</param>
/// <param name="result">If sucessful, contains the cast value.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns>
public static bool TryConvert(Type to, object value, CultureInfo culture, out object result)
{
if ((value.GetType() == typeof(string) && Conversions.ContainsKey(to)) ||
(to == typeof(string) && Conversions.ContainsKey(value.GetType())))
{
result = Convert.ChangeType(value, to, culture);
return true;
}
else
{
return TryCast(to, value, out result);
}
}
/// <summary>
/// Casts a value to a type, returning the default for that type if the value could not be
/// cast.
/// </summary>
/// <param name="value">The value to cast.</param>
/// <param name="type">The type to cast to..</param>
/// <param name="allowUnset">Allow <see cref="PerspexProperty.UnsetValue"/>.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
public static object CastOrDefault(object value, Type type, bool allowUnset = true)
public static object CastOrDefault(object value, Type type)
{
var typeInfo = type.GetTypeInfo();
object result;
if (TypeUtilities.TryCast(type, value, out result, allowUnset))
if (TypeUtilities.TryCast(type, value, out result))
{
return result;
}

13
tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs

@ -174,19 +174,6 @@ namespace Perspex.Base.UnitTests
Assert.Equal(0, target.Baz);
}
[Fact]
public void Bind_Handles_UnsetValue()
{
var target = new Class1();
var source = new Subject<object>();
var sub = target.Bind(Class1.BazProperty, source);
source.OnNext(PerspexProperty.UnsetValue);
Assert.Equal(0, target.Baz);
}
[Fact]
public void ReadOnly_Property_Cannot_Be_Set()
{

27
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs

@ -21,6 +21,15 @@ namespace Perspex.Markup.UnitTests.Binding
Assert.Equal("foo", result);
}
[Fact]
public void Should_Get_Simple_Property_Value_Type()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
Assert.Equal(typeof(string), target.ResultType);
}
[Fact]
public async void Should_Get_Simple_Property_From_Base_Class()
{
@ -41,6 +50,15 @@ namespace Perspex.Markup.UnitTests.Binding
Assert.Equal("baz", result);
}
[Fact]
public void Should_Get_Simple_Property_Chain_Type()
{
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
Assert.Equal(typeof(string), target.ResultType);
}
[Fact]
public async void Should_Not_Have_Value_For_Broken_Chain()
{
@ -51,6 +69,15 @@ namespace Perspex.Markup.UnitTests.Binding
Assert.Equal(PerspexProperty.UnsetValue, result);
}
[Fact]
public void Should_Have_Null_ResultType_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
Assert.Null(target.ResultType);
}
[Fact]
public void Should_Track_Simple_Property_Value()
{

47
tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs

@ -0,0 +1,47 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Globalization;
using Xunit;
namespace Perspex.Markup.UnitTests
{
public class DefaultValueConverterTests
{
[Fact]
public void Can_Convert_String_To_Int()
{
var result = DefaultValueConverter.Instance.Convert(
"5",
typeof(int),
null,
CultureInfo.InvariantCulture);
Assert.Equal(5, result);
}
[Fact]
public void Can_Convert_String_To_Double()
{
var result = DefaultValueConverter.Instance.Convert(
"5",
typeof(double),
null,
CultureInfo.InvariantCulture);
Assert.Equal(5.0, result);
}
[Fact]
public void Can_Convert_Double_To_String()
{
var result = DefaultValueConverter.Instance.Convert(
5.0,
typeof(string),
null,
CultureInfo.InvariantCulture);
Assert.Equal("5", result);
}
}
}

1
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@ -81,6 +81,7 @@
<Compile Include="Binding\ExpressionObserverTests_Property.cs" />
<Compile Include="Binding\ExpressionNodeBuilderTests.cs" />
<Compile Include="Binding\NotifyingBase.cs" />
<Compile Include="DefaultValueConverterTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnitTestSynchronizationContext.cs" />
</ItemGroup>

Loading…
Cancel
Save