Browse Source

Start making Converters work.

When passed to Binding. Found another problem with OmniXAML which means
we can't continue along this road: OmniXAML issue #50.
pull/297/head
Steven Kirk 10 years ago
parent
commit
e301e56a9d
  1. 2
      samples/XamlTestApplicationPcl/ScrollBar.paml
  2. 33
      samples/XamlTestApplicationPcl/StringNullOrEmpty.cs
  3. 15
      samples/XamlTestApplicationPcl/TextBox.paml
  4. 7
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  5. 5
      samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
  6. 22
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  7. 15
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  8. 33
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs
  9. 86
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/StaticExtension.cs
  10. 11
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TypeExtension.cs
  11. 2
      src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
  12. 1
      src/Markup/Perspex.Markup.Xaml/Properties/AssemblyInfo.cs
  13. 65
      src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
  14. 4
      src/Markup/Perspex.Markup/DefaultValueConverter.cs
  15. 10
      src/Markup/Perspex.Markup/IValueConverter.cs
  16. 10
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs
  17. 12
      tests/Perspex.Markup.UnitTests/Binding/ExpressionSubjectTests.cs
  18. 31
      tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs
  19. 4
      tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

2
samples/XamlTestApplicationPcl/ScrollBar.paml

@ -8,7 +8,7 @@
Value="{TemplateBinding Path=Value, Mode=TwoWay}"
ViewportSize="{TemplateBinding ViewportSize}"
Orientation="{TemplateBinding Orientation}">
<Thumb Width="10" Height="10">
<Thumb>
<Thumb.Template>
<ControlTemplate>
<Border Background="Gray"/>

33
samples/XamlTestApplicationPcl/StringNullOrEmpty.cs

@ -0,0 +1,33 @@
// 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;
using Perspex.Markup;
namespace XamlTestApplication
{
public class StringNullOrEmpty : IValueConverter
{
public static readonly StringNullOrEmpty Instance = new StringNullOrEmpty();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
{
return true;
}
else
{
var s = value as string;
return s != null ? string.IsNullOrEmpty(s) : PerspexProperty.UnsetValue;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

15
samples/XamlTestApplicationPcl/TextBox.paml

@ -1,4 +1,6 @@
<Styles xmlns="https://github.com/perspex">
<Styles xmlns="https://github.com/perspex"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XamlTestApplication;assembly=XamlTestApplicationPcl">
<Style Selector="TextBox">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#ff707070"/>
@ -19,12 +21,19 @@
Foreground="#ff007ACC"
FontSize="10"
Text="{TemplateBinding Watermark}"
IsVisible="{TemplateBinding Path=IsFloatingWatermarkVisible, Converter={x:Type FooBar}}"/>
IsVisible="{TemplateBinding FloatingWatermark}">
</TextBlock>
<Panel>
<TextBlock Name="watermark"
Opacity="0.5"
Text="{TemplateBinding Watermark}"
IsVisible="{TemplateBinding IsWatermarkVisible}"/>
IsVisible="{TemplateBinding IsWatermarkVisible}">
<TextBlock.IsVisible>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
SourcePropertyPath="Text"
Converter="{Static local:StringNullOrEmpty.Instance}"/>
</TextBlock.IsVisible>
</TextBlock>
<TextPresenter Name="PART_TextPresenter"
CaretIndex="{TemplateBinding CaretIndex}"
SelectionStart="{TemplateBinding SelectionStart}"

7
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -3,7 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:XamlTestApplication.ViewModels;assembly=XamlTestApplicationPcl"
Title="Perspex Test Application" Width="800" Height="600">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<TextBox Watermark="Hello"></TextBox>
<!--<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<TabControl Grid.Row="1" Grid.ColumnSpan="2" Padding="5">
<TabControl.Transition>
<PageSlide Duration="0.25" />
@ -40,7 +41,7 @@
Foreground="#212121" />
<TextBlock Text="A label capable of displaying HTML content" FontSize="13" Foreground="#727272"
Margin="0, 0, 0, 10" />
<!--PLEASE, FIX <HtmlLabel /> -->
--><!--PLEASE, FIX <HtmlLabel /> --><!--
</StackPanel>
</TabItem>
<TabItem Header="Input">
@ -221,5 +222,5 @@
</Grid>
</TabItem>
</TabControl>
</Grid>
</Grid>-->
</Window>

5
samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj

@ -41,6 +41,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="StringNullOrEmpty.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
<Compile Include="ViewModels\TestItem.cs" />
<Compile Include="ViewModels\TestNode.cs" />
@ -60,6 +61,10 @@
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
<Name>Perspex.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Perspex.Markup\Perspex.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Perspex.Markup</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Animation\Perspex.Animation.csproj">
<Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project>
<Name>Perspex.Animation</Name>

22
src/Markup/Perspex.Markup.Xaml/Data/Binding.cs

@ -23,6 +23,7 @@ namespace Perspex.Markup.Xaml.Data
_typeConverterProvider = typeConverterProvider;
}
public IValueConverter Converter { get; set; }
public BindingMode Mode { get; set; }
public BindingPriority Priority { get; set; }
public RelativeSource RelativeSource { get; set; }
@ -30,9 +31,7 @@ namespace Perspex.Markup.Xaml.Data
public void Bind(IObservablePropertyBag instance, PerspexProperty property)
{
var subject = new ExpressionSubject(
CreateExpressionObserver(instance, property),
property.PropertyType);
var subject = CreateExpressionSubject(instance, property);
if (subject != null)
{
@ -40,22 +39,29 @@ namespace Perspex.Markup.Xaml.Data
}
}
public ExpressionObserver CreateExpressionObserver(
public ISubject<object> CreateExpressionSubject(
IObservablePropertyBag instance,
PerspexProperty property)
{
ExpressionObserver observer;
if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
return CreateDataContextExpressionObserver(instance, property);
observer = CreateDataContextExpressionSubject(instance, property);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
return CreateTemplatedParentExpressionObserver(instance, property);
observer = CreateTemplatedParentExpressionSubject(instance, property);
}
else
{
throw new NotSupportedException();
}
return new ExpressionSubject(
observer,
property.PropertyType,
Converter ?? DefaultValueConverter.Instance);
}
internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject<object> subject)
@ -84,7 +90,7 @@ namespace Perspex.Markup.Xaml.Data
}
}
public ExpressionObserver CreateDataContextExpressionObserver(
public ExpressionObserver CreateDataContextExpressionSubject(
IObservablePropertyBag instance,
PerspexProperty property)
{
@ -105,7 +111,7 @@ namespace Perspex.Markup.Xaml.Data
return null;
}
public ExpressionObserver CreateTemplatedParentExpressionObserver(
public ExpressionObserver CreateTemplatedParentExpressionSubject(
IObservablePropertyBag instance,
PerspexProperty property)
{

15
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -6,28 +6,25 @@ using Perspex.Markup.Xaml.Data;
namespace Perspex.Markup.Xaml.MarkupExtensions
{
public class BindingExtension : MarkupExtension
public class RelativeSourceExtension : MarkupExtension
{
public BindingExtension()
public RelativeSourceExtension()
{
}
public BindingExtension(string path)
public RelativeSourceExtension(RelativeSourceMode mode)
{
Path = path;
Mode = mode;
}
public override object ProvideValue(MarkupExtensionContext extensionContext)
{
return new Data.Binding
return new RelativeSource
{
Mode = Mode,
SourcePropertyPath = Path,
};
}
public object Converter { get; set; }
public BindingMode Mode { get; set; }
public string Path { get; set; }
public RelativeSourceMode Mode { get; set; }
}
}

33
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/RelativeSourceExtension.cs

@ -0,0 +1,33 @@
// 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 OmniXaml;
using Perspex.Markup.Xaml.Data;
namespace Perspex.Markup.Xaml.MarkupExtensions
{
public class BindingExtension : MarkupExtension
{
public BindingExtension()
{
}
public BindingExtension(string path)
{
Path = path;
}
public override object ProvideValue(MarkupExtensionContext extensionContext)
{
return new Data.Binding
{
Mode = Mode,
SourcePropertyPath = Path,
};
}
public object Converter { get; set; }
public BindingMode Mode { get; set; }
public string Path { get; set; }
}
}

86
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/StaticExtension.cs

@ -0,0 +1,86 @@
// 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.Linq;
using System.Reflection;
using Glass;
using OmniXaml;
namespace Perspex.Markup.Xaml.MarkupExtensions
{
public class StaticExtension : MarkupExtension
{
public StaticExtension()
{
}
public StaticExtension(string identifier)
{
Identifier = identifier;
}
public string Identifier { get; set; }
public override object ProvideValue(MarkupExtensionContext markupExtensionContext)
{
var typeRepository = markupExtensionContext.TypeRepository;
var typeAndMember = GetTypeAndMember(Identifier);
var prefixAndType = GetPrefixAndType(typeAndMember.Item1);
var xamlType = typeRepository.GetByPrefix(prefixAndType.Item1, prefixAndType.Item2);
return GetValue(xamlType.UnderlyingType, typeAndMember.Item2);
}
private static Tuple<string, string> GetTypeAndMember(string s)
{
var parts = s.Split('.');
if (parts.Length != 2)
{
throw new ArgumentException("Static member must be in the form Type.Member.");
}
return Tuple.Create(parts[0], parts[1]);
}
private static Tuple<string, string> GetPrefixAndType(string s)
{
if (s.Contains(":"))
{
return s.Dicotomize(':');
}
else
{
return new Tuple<string, string>(string.Empty, s);
}
}
private object GetValue(Type type, string name)
{
var t = type;
while (t != null)
{
var result = t.GetTypeInfo().DeclaredMembers.FirstOrDefault(x => x.Name == name);
if (result is PropertyInfo)
{
var property = ((PropertyInfo)result);
if (property.GetMethod.IsStatic)
{
return ((PropertyInfo)result).GetValue(null);
}
}
else if (result is FieldInfo)
{
return ((FieldInfo)result).GetValue(null);
}
t = t.GetTypeInfo().BaseType;
}
throw new ArgumentException($"Static member '{type}.{name}' not found.");
}
}
}

11
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TypeExtension.cs

@ -25,13 +25,14 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
public string TypeName { get; set; }
private Type ResolveFromString(string typeLocator, IXamlTypeRepository typeRepository)
private Type ResolveFromString(string type, IXamlTypeRepository typeRepository)
{
Guard.ThrowIfNull(typeLocator, nameof(typeLocator));
Guard.ThrowIfNull(type, nameof(type));
var prefixAndType = typeLocator.Dicotomize(':');
var xamlType = typeRepository.GetByPrefix(prefixAndType.Item1, prefixAndType.Item2);
var split = type.Split(':');
var prefix = split.Length == 1 ? split[0] : null;
var typeName = split.Length == 1 ? split[1] : split[0];
var xamlType = typeRepository.GetByPrefix(prefix, typeName);
return xamlType.UnderlyingType;
}

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

@ -59,12 +59,14 @@
<Compile Include="Converters\SelectorTypeConverter.cs" />
<Compile Include="Context\PerspexWiringContext.cs" />
<Compile Include="Converters\TimeSpanTypeConverter.cs" />
<Compile Include="MarkupExtensions\RelativeSourceExtension.cs" />
<Compile Include="MarkupExtensions\TemplateBindingExtension.cs" />
<Compile Include="MarkupExtensions\BindingExtension.cs" />
<Compile Include="Converters\BrushTypeConverter.cs" />
<Compile Include="Converters\BitmapTypeConverter.cs" />
<Compile Include="Converters\GridLengthTypeConverter.cs" />
<Compile Include="Context\PerspexParserFactory.cs" />
<Compile Include="MarkupExtensions\StaticExtension.cs" />
<Compile Include="OmniXAML\Source\Glass\AutoKeyDictionary.cs" />
<Compile Include="OmniXAML\Source\Glass\ChangeTracking\ObservableProperty.cs" />
<Compile Include="OmniXAML\Source\Glass\ChangeTracking\ObservablePropertyChain.cs" />

1
src/Markup/Perspex.Markup.Xaml/Properties/AssemblyInfo.cs

@ -6,6 +6,7 @@ using Perspex.Metadata;
using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("Perspex.Markup.Xaml")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Markup.Xaml.Data")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Markup.Xaml.MarkupExtensions")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Markup.Xaml.Styling")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Markup.Xaml.Templates")]

65
src/Markup/Perspex.Markup/Data/ExpressionSubject.cs

@ -15,7 +15,6 @@ namespace Perspex.Markup.Data
/// </summary>
public class ExpressionSubject : ISubject<object>, IDescription
{
private IValueConverter _converter;
private ExpressionObserver _inner;
private Type _targetType;
@ -37,11 +36,20 @@ namespace Perspex.Markup.Data
/// <param name="converter">The value converter to use.</param>
public ExpressionSubject(ExpressionObserver inner, Type targetType, IValueConverter converter)
{
_converter = converter;
Contract.Requires<ArgumentNullException>(inner != null);
Contract.Requires<ArgumentNullException>(targetType != null);
Contract.Requires<ArgumentNullException>(converter != null);
_inner = inner;
_targetType = targetType;
Converter = converter;
}
/// <summary>
/// Gets the converter to use on the expression.
/// </summary>
public IValueConverter Converter { get; }
/// <inheritdoc/>
string IDescription.Description => _inner.Expression;
@ -62,12 +70,14 @@ namespace Perspex.Markup.Data
if (type != null)
{
object converted;
var converted = Converter.ConvertBack(value, type, null, CultureInfo.CurrentUICulture);
if (ConvertBack(value, type, out converted))
if (converted == PerspexProperty.UnsetValue)
{
_inner.SetValue(converted);
converted = TypeUtilities.Default(_targetType);
}
_inner.SetValue(converted);
}
}
@ -75,51 +85,8 @@ namespace Perspex.Markup.Data
public IDisposable Subscribe(IObserver<object> observer)
{
return _inner
.Select(x => Convert(x, _targetType))
.Select(x => Converter.Convert(x, _targetType, null, CultureInfo.CurrentUICulture))
.Subscribe(observer);
}
private object Convert(object value, Type type)
{
try
{
if (value == null || value == PerspexProperty.UnsetValue)
{
return TypeUtilities.Default(type);
}
else
{
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
{
if (value == null || value == PerspexProperty.UnsetValue)
{
result = TypeUtilities.Default(type);
return true;
}
else
{
result = _converter.ConvertBack(value, type, null, CultureInfo.CurrentUICulture);
return true;
}
}
catch
{
// TODO: Log something.
result = null;
return false;
}
}
}
}

4
src/Markup/Perspex.Markup/DefaultValueConverter.cs

@ -30,13 +30,13 @@ namespace Perspex.Markup
{
object result;
if (TypeUtilities.TryConvert(targetType, value, culture, out result))
if (value != null && TypeUtilities.TryConvert(targetType, value, culture, out result))
{
return result;
}
else
{
throw new InvalidCastException($"Cannot convert value from {value.GetType()} to {targetType}");
return PerspexProperty.UnsetValue;
}
}

10
src/Markup/Perspex.Markup/IValueConverter.cs

@ -19,6 +19,11 @@ namespace Perspex.Markup
/// <param name="parameter">A user-defined parameter.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>The converted value.</returns>
/// <remarks>
/// This method should not throw exceptions. If the value is not convertible, return
/// <see cref="PerspexProperty.UnsetValue"/>. Any exception thrown will be treated as
/// an application exception.
/// </remarks>
object Convert(object value, Type targetType, object parameter, CultureInfo culture);
/// <summary>
@ -29,6 +34,11 @@ namespace Perspex.Markup
/// <param name="parameter">A user-defined parameter.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>The converted value.</returns>
/// <remarks>
/// This method should not throw exceptions. If the value is not convertible, return
/// <see cref="PerspexProperty.UnsetValue"/>. Any exception thrown will be treated as
/// an application exception.
/// </remarks>
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
}

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

@ -31,6 +31,16 @@ namespace Perspex.Markup.UnitTests.Binding
Assert.Equal(typeof(string), target.ResultType);
}
[Fact]
public async void Should_Get_Simple_Property_Value_Null()
{
var data = new { Foo = (string)null };
var target = new ExpressionObserver(data, "Foo");
var result = await target.Take(1);
Assert.Null(result);
}
[Fact]
public async void Should_Get_Simple_Property_From_Base_Class()
{

12
tests/Perspex.Markup.UnitTests/Binding/ExpressionSubjectTests.cs

@ -51,13 +51,13 @@ namespace Perspex.Markup.UnitTests.Binding
}
[Fact]
public async void Should_Coerce_Get_Null_String_To_Double_Deafult_Value()
public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
{
var data = new Class1 { StringValue = null };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.Equal(0.0, result);
Assert.Equal(PerspexProperty.UnsetValue, result);
}
[Fact]
@ -93,18 +93,18 @@ namespace Perspex.Markup.UnitTests.Binding
}
[Fact]
public void Should_Ignore_Set_Invalid_Double_String()
public void Should_Coerce_Set_Invalid_Double_String_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext("foo");
Assert.Equal(5.6, data.DoubleValue);
Assert.Equal(0, data.DoubleValue);
}
[Fact]
public void Should_Coerce_Set_Null_To_Default_Value()
public void Should_Coerce_Setting_Null_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
@ -115,7 +115,7 @@ namespace Perspex.Markup.UnitTests.Binding
}
[Fact]
public void Should_Coerce_Set_UnsetValue_To_Default_Value()
public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
{
var data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));

31
tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests.cs

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using System.Reactive.Subjects;
using Moq;
using Perspex.Controls;
using Perspex.Markup.Data;
using Perspex.Markup.Xaml.Data;
using Xunit;
@ -138,6 +139,36 @@ namespace Perspex.Markup.Xaml.UnitTests.Data
Assert.Equal("Bar", parent.Child.DataContext);
}
[Fact]
public void Should_Use_DefaultValueConverter_When_No_Converter_Specified()
{
var target = CreateTarget(null);
var binding = new Binding
{
SourcePropertyPath = "Foo",
};
var result = binding.CreateExpressionSubject(target.Object, TextBox.TextProperty);
Assert.IsType<DefaultValueConverter>(((ExpressionSubject)result).Converter);
}
[Fact]
public void Should_Use_Supplied_Converter()
{
var target = CreateTarget(null);
var converter = new Mock<IValueConverter>();
var binding = new Binding
{
Converter = converter.Object,
SourcePropertyPath = "Foo",
};
var result = binding.CreateExpressionSubject(target.Object, TextBox.TextProperty);
Assert.Same(converter.Object, ((ExpressionSubject)result).Converter);
}
/// <summary>
/// Tests a problem discovered with ListBox with selection.
/// </summary>

4
tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

@ -111,6 +111,10 @@
<Project>{3E53A01A-B331-47F3-B828-4A5717E77A24}</Project>
<Name>Perspex.Markup.Xaml</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Markup\Perspex.Markup\Perspex.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Perspex.Markup</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Animation\Perspex.Animation.csproj">
<Project>{D211E587-D8BC-45B9-95A4-F297C8FA5200}</Project>
<Name>Perspex.Animation</Name>

Loading…
Cancel
Save