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.
Steven Kirk 10 years ago
parent
commit
e301e56a9d

+ 1 - 1
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 - 0
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();
+        }
+    }
+}

+ 12 - 3
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}"

+ 4 - 3
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 - 0
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>

+ 14 - 8
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)
         {

+ 6 - 9
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 - 0
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 - 0
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.");
+        }
+    }
+}

+ 6 - 5
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 - 0
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 - 0
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")]

+ 16 - 49
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;
-            }
-        }
     }
 }

+ 2 - 2
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 - 0
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 - 0
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()
         {

+ 6 - 6
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 - 0
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 - 0
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>