Browse Source

Merge pull request #5038 from donandren/issues/compiledbinding

support cast in binding + some related small fixes for compiled binding
Dan Walmsley 5 years ago
parent
commit
ac681ec942

+ 34 - 0
src/Avalonia.Base/Data/Core/TypeCastNode.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Data.Core
+{
+    public class TypeCastNode : ExpressionNode
+    {
+        public override string Description => $"as {TargetType.FullName}";
+
+        public Type TargetType { get; }
+
+        public TypeCastNode(Type type)
+        {
+            TargetType = type;
+        }
+
+        protected virtual object Cast(object value)
+        {
+            return TargetType.IsInstanceOfType(value) ? value : null;
+        }
+
+        protected override void StartListeningCore(WeakReference<object> reference)
+        {
+            if (reference.TryGetTarget(out object target))
+            {
+                target = Cast(target);
+                reference = target == null ? NullReference : new WeakReference<object>(target);
+            }
+
+            base.StartListeningCore(reference);
+        }
+    }
+}

+ 29 - 3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@@ -203,6 +203,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                                 .ParentNodes()
                                 .OfType<XamlAstConstructableObjectNode>()
                                 .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType()))
+                                .Skip(1)
                                 .ElementAtOrDefault(ancestor.Level)
                                 ?.Type.GetClrType();
 
@@ -242,6 +243,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                     case RawSourceBindingExpressionNode rawSource:
                         nodes.Add(new RawSourcePathElementNode(rawSource.RawSource));
                         break;
+                    case BindingExpressionGrammar.TypeCastNode typeCastNode:
+                        var castType = GetType(typeCastNode.Namespace, typeCastNode.TypeName);
+
+                        if (castType is null)
+                        {
+                            throw new XamlX.XamlParseException($"Unable to resolve cast to type {typeCastNode.Namespace}:{typeCastNode.TypeName} based on XAML tree.", lineInfo);
+                        }
+
+                        nodes.Add(new TypeCastPathElementNode(castType));
+                        break;
                 }
             }
 
@@ -422,7 +433,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             {
                 codeGen.Ldtype(Type)
                     .Ldc_I4(_level)
-                    .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "FindAncestor"));
+                    .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Ancestor"));
             }
         }
 
@@ -608,10 +619,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             private readonly IXamlAstValueNode _rawSource;
 
             public RawSourcePathElementNode(IXamlAstValueNode rawSource)
-                :base(rawSource)
+                : base(rawSource)
             {
                 _rawSource = rawSource;
-                
+
             }
 
             public IXamlType Type => _rawSource.Type.GetClrType();
@@ -625,6 +636,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             }
         }
 
+        class TypeCastPathElementNode : IXamlIlBindingPathElementNode
+        {
+            public TypeCastPathElementNode(IXamlType ancestorType)
+            {
+                Type = ancestorType;
+            }
+
+            public IXamlType Type { get; }
+
+            public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
+            {
+                codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "TypeCast").MakeGenericMethod(new[] { Type }));
+            }
+        }
+
         class XamlIlBindingPathNode : XamlAstNode, IXamlIlBindingPathNode, IXamlAstEmitableNode<IXamlILEmitter, XamlILNodeEmitResult>
         {
             private readonly List<IXamlIlBindingPathElementNode> _transformElements;

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -23,6 +23,7 @@
         <Compile Include="MarkupExtensions\CompiledBindings\ObservableStreamPlugin.cs" />
         <Compile Include="MarkupExtensions\CompiledBindings\PropertyInfoAccessorFactory.cs" />
         <Compile Include="MarkupExtensions\CompiledBindings\PropertyInfoAccessorPlugin.cs" />
+        <Compile Include="MarkupExtensions\CompiledBindings\StrongTypeCastNode.cs" />
         <Compile Include="MarkupExtensions\CompiledBindings\TaskStreamPlugin.cs" />
         <Compile Include="MarkupExtensions\DynamicResourceExtension.cs" />
         <Compile Include="MarkupExtensions\ResolveByNameExtension.cs" />

+ 2 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@@ -26,6 +26,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
             {
                 Path = Path,
                 Converter = Converter,
+                ConverterParameter = ConverterParameter,
+                TargetNullValue = TargetNullValue,
                 FallbackValue = FallbackValue,
                 Mode = Mode,
                 Priority = Priority,

+ 51 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Data.Core;
 using Avalonia.Data.Core.Plugins;
@@ -53,6 +54,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
                     case IStronglyTypedStreamElement stream:
                         node = new StreamNode(stream.CreatePlugin());
                         break;
+                    case ITypeCastElement typeCast:
+                        node = new StrongTypeCastNode(typeCast.Type, typeCast.Cast);
+                        break;
                     default:
                         throw new InvalidOperationException($"Unknown binding path element type {element.GetType().FullName}");
                 }
@@ -66,6 +70,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
         internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data;
 
         internal object RawSource { get; }
+
+        public override string ToString()
+            => string.Concat(_elements.Select(e => e.ToString()));
     }
 
     public class CompiledBindingPathBuilder
@@ -126,6 +133,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
             return this;
         }
 
+        public CompiledBindingPathBuilder TypeCast<T>()
+        {
+            _elements.Add(new TypeCastPathElement<T>());
+            return this;
+        }
+
         public CompiledBindingPathBuilder SetRawSource(object rawSource)
         {
             _rawSource = rawSource;
@@ -157,6 +170,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
         public IPropertyInfo Property { get; }
 
         public Func<WeakReference<object>, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; }
+
+        public override string ToString()
+            => $".{Property.Name}";
     }
 
     internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement
@@ -164,6 +180,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
         IStreamPlugin CreatePlugin();
     }
 
+    internal interface ITypeCastElement : ICompiledBindingPathElement
+    {
+        Type Type { get; }
+
+        Func<object, object> Cast { get; }
+    }
+
     internal class TaskStreamPathElement<T> : IStronglyTypedStreamElement
     {
         public static readonly TaskStreamPathElement<T> Instance = new TaskStreamPathElement<T>();
@@ -181,6 +204,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
     internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement
     {
         public static readonly SelfPathElement Instance = new SelfPathElement();
+
+        public override string ToString()
+            => "$self";
     }
 
     internal class AncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement
@@ -193,6 +219,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
 
         public Type AncestorType { get; }
         public int Level { get; }
+
+        public override string ToString()
+           => $"$parent[{AncestorType?.Name},{Level}]";
     }
 
     internal class VisualAncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement
@@ -217,6 +246,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
 
         public INameScope NameScope { get; }
         public string Name { get; }
+
+        public override string ToString()
+            => $"#{Name}";
     }
 
     internal class ArrayElementPathElement : ICompiledBindingPathElement
@@ -229,5 +261,24 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
 
         public int[] Indices { get; }
         public Type ElementType { get; }
+        public override string ToString()
+            => $"[{string.Join(",", Indices)}]";
+    }
+
+    internal class TypeCastPathElement<T> : ITypeCastElement
+    {
+        private static object TryCast(object obj)
+        {
+            if (obj is T result)
+                return result;
+            return null;
+        }
+
+        public Type Type => typeof(T);
+
+        public Func<object, object> Cast => TryCast;
+
+        public override string ToString()
+            => $"({Type.FullName})";
     }
 }

+ 18 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs

@@ -0,0 +1,18 @@
+using System;
+using Avalonia.Data.Core;
+
+namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
+{
+    public class StrongTypeCastNode : TypeCastNode
+    {
+        private Func<object, object> _cast;
+
+        public StrongTypeCastNode(Type type, Func<object, object> cast) : base(type)
+        {
+            _cast = cast;
+        }
+
+        protected override object Cast(object value)
+            => _cast(value);
+    }
+}

+ 84 - 5
src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs

@@ -46,6 +46,10 @@ namespace Avalonia.Markup.Parsers
                         state = ParseIndexer(ref r, nodes);
                         break;
 
+                    case State.TypeCast:
+                        state = ParseTypeCast(ref r, nodes);
+                        break;
+
                     case State.ElementName:
                         state = ParseElementName(ref r, nodes);
                         mode = SourceMode.Control;
@@ -84,6 +88,11 @@ namespace Avalonia.Markup.Parsers
             }
             else if (ParseOpenBrace(ref r))
             {
+                if (PeekOpenBrace(ref r))
+                {
+                    return State.TypeCast;
+                }
+
                 return State.AttachedProperty;
             }
             else if (PeekOpenBracket(ref r))
@@ -124,6 +133,10 @@ namespace Avalonia.Markup.Parsers
             {
                 return State.Indexer;
             }
+            else if (ParseOpenBrace(ref r))
+            {
+                return State.TypeCast;
+            }
 
             return State.End;
         }
@@ -132,6 +145,11 @@ namespace Avalonia.Markup.Parsers
         {
             if (ParseOpenBrace(ref r))
             {
+                if (PeekOpenBrace(ref r))
+                {
+                    return State.TypeCast;
+                }
+
                 return State.AttachedProperty;
             }
             else
@@ -152,6 +170,12 @@ namespace Avalonia.Markup.Parsers
         {
             var (ns, owner) = ParseTypeName(ref r);
 
+            if(!r.End && r.TakeIf(')'))
+            {
+                nodes.Add(new TypeCastNode() { Namespace = ns, TypeName = owner });
+                return State.AfterMember;
+            }
+
             if (r.End || !r.TakeIf('.'))
             {
                 throw new ExpressionParseException(r.Position, "Invalid attached property name.");
@@ -159,6 +183,11 @@ namespace Avalonia.Markup.Parsers
 
             var name = r.ParseIdentifier();
 
+            if (name.Length == 0)
+            {
+                throw new ExpressionParseException(r.Position, "Attached Property name expected after '.'.");
+            }
+
             if (r.End || !r.TakeIf(')'))
             {
                 throw new ExpressionParseException(r.Position, "Expected ')'.");
@@ -186,6 +215,39 @@ namespace Avalonia.Markup.Parsers
             return State.AfterMember;
         }
 
+        private static State ParseTypeCast(ref CharacterReader r, List<INode> nodes)
+        {
+            bool parseMemberBeforeAddCast = ParseOpenBrace(ref r);
+
+            var (ns, typeName) = ParseTypeName(ref r);
+
+            var result = State.AfterMember;
+
+            if (parseMemberBeforeAddCast)
+            {
+                if (!ParseCloseBrace(ref r))
+                {
+                    throw new ExpressionParseException(r.Position, "Expected ')'.");
+                }
+
+                result = ParseBeforeMember(ref r, nodes);
+
+                if(r.Peek == '[')
+                {
+                    result = ParseIndexer(ref r, nodes);
+                }
+            }
+
+            nodes.Add(new TypeCastNode { Namespace = ns, TypeName = typeName });
+
+            if (r.End || !r.TakeIf(')'))
+            {
+                throw new ExpressionParseException(r.Position, "Expected ')'.");
+            }
+
+            return result;
+        }
+
         private static State ParseElementName(ref CharacterReader r, List<INode> nodes)
         {
             var name = r.ParseIdentifier();
@@ -288,11 +350,21 @@ namespace Avalonia.Markup.Parsers
             return !r.End && r.TakeIf('(');
         }
 
+        private static bool ParseCloseBrace(ref CharacterReader r)
+        {
+            return !r.End && r.TakeIf(')');
+        }
+
         private static bool PeekOpenBracket(ref CharacterReader r)
         {
             return !r.End && r.Peek == '[';
         }
 
+        private static bool PeekOpenBrace(ref CharacterReader r)
+        {
+            return !r.End && r.Peek == '(';
+        }
+
         private static bool ParseStreamOperator(ref CharacterReader r)
         {
             return !r.End && r.TakeIf('^');
@@ -322,6 +394,7 @@ namespace Avalonia.Markup.Parsers
             BeforeMember,
             AttachedProperty,
             Indexer,
+            TypeCast,
             End,
         }
 
@@ -343,9 +416,9 @@ namespace Avalonia.Markup.Parsers
             }
         }
 
-        public interface INode {}
+        public interface INode { }
 
-        public interface ITransformNode {}
+        public interface ITransformNode { }
 
         public class EmptyExpressionNode : INode { }
 
@@ -366,11 +439,11 @@ namespace Avalonia.Markup.Parsers
             public IList<string> Arguments { get; set; }
         }
 
-        public class NotNode : INode, ITransformNode {}
+        public class NotNode : INode, ITransformNode { }
 
-        public class StreamNode : INode {}
+        public class StreamNode : INode { }
 
-        public class SelfNode : INode {}
+        public class SelfNode : INode { }
 
         public class NameNode : INode
         {
@@ -383,5 +456,11 @@ namespace Avalonia.Markup.Parsers
             public string TypeName { get; set; }
             public int Level { get; set; }
         }
+
+        public class TypeCastNode : INode
+        {
+            public string Namespace { get; set; }
+            public string TypeName { get; set; }
+        }
     }
 }

+ 19 - 0
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@@ -59,6 +59,9 @@ namespace Avalonia.Markup.Parsers
                     case BindingExpressionGrammar.NameNode elementName:
                         nextNode = new ElementNameNode(_nameScope, elementName.Name);
                         break;
+                    case BindingExpressionGrammar.TypeCastNode typeCast:
+                        nextNode = ParseTypeCastNode(typeCast);
+                        break;
                 }
                 if (rootNode is null)
                 {
@@ -92,6 +95,22 @@ namespace Avalonia.Markup.Parsers
             return new FindAncestorNode(ancestorType, ancestorLevel);
         }
 
+        private TypeCastNode ParseTypeCastNode(BindingExpressionGrammar.TypeCastNode node)
+        {
+            Type castType = null;
+            if (!(node.Namespace is null) && !(node.TypeName is null))
+            {
+                if (_typeResolver == null)
+                {
+                    throw new InvalidOperationException("Cannot parse a binding path with a typed Cast without a type resolver. Maybe you can use a LINQ Expression binding path instead?");
+                }
+
+                castType = _typeResolver(node.Namespace, node.TypeName);
+            }
+
+            return new TypeCastNode(castType);
+        }
+
         private AvaloniaPropertyAccessorNode ParseAttachedProperty(BindingExpressionGrammar.AttachedPropertyNameNode node)
         {
             if (_typeResolver == null)

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

@@ -129,7 +129,7 @@ namespace Avalonia.Markup.UnitTests.Parsers
         {
             var data = new Class1();
 
-            Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver));
+            Assert.Throws<ExpressionParseException>(() => ExpressionObserverBuilder.Build(data, "(Owner.)", typeResolver: _typeResolver));
         }
 
         [Fact]

+ 48 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs

@@ -84,6 +84,54 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             }
         }
 
+        [Fact]
+        public void SupportCastToTypeInExpression()
+        {
+            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.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
+        >
+    <ContentControl Content='{Binding $parent.((local:TestDataContext)DataContext).StringProperty}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var dataContext = new TestDataContext
+                {
+                    StringProperty = "foobar"
+                };
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(dataContext.StringProperty, contentControl.Content);
+            }
+        }
+
+        [Fact]
+        public void SupportCastToTypeInExpression_DifferentTypeEvaluatesToNull()
+        {
+            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.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
+        >
+    <ContentControl Content='{Binding $parent.((local:TestDataContext)DataContext)}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var dataContext = "foo";
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(null, contentControl.Content);
+            }
+        }
         private class FooBar
         {
             public object Foo { get; } = null;

+ 225 - 4
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@@ -1,11 +1,13 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Globalization;
 using System.Reactive.Subjects;
 using System.Text;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
+using Avalonia.Data.Converters;
 using Avalonia.Data.Core;
 using Avalonia.Markup.Data;
 using Avalonia.UnitTests;
@@ -601,6 +603,48 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             }
         }
 
+        [Fact]
+        public void SupportParentInPath()
+        {
+            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.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
+        Title='foo'
+        x:DataType='local:TestDataContext'>
+    <ContentControl Content='{CompiledBinding $parent.Title}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                Assert.Equal("foo", contentControl.Content);
+            }
+        }
+
+        [Fact]
+        public void SupportConverterWithParameter()
+        {
+            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.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
+        x:DataType='local:TestDataContext' x:CompileBindings='True'>
+    <TextBlock Name='textBlock' Text='{Binding StringProperty, Converter={x:Static local:AppendConverter.Instance}, ConverterParameter=Bar}'/>
+</Window>";
+
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var textBlock = window.FindControl<TextBlock>("textBlock");
+
+                window.DataContext = new TestDataContext() { StringProperty = "Foo" };
+
+                Assert.Equal("Foo+Bar", textBlock.Text);
+            }
+        }
+
         [Fact]
         public void ThrowsOnInvalidCompileBindingsDirective()
         {
@@ -616,23 +660,198 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
                 Assert.Throws<XamlX.XamlParseException>(() => AvaloniaRuntimeXamlLoader.Load(xaml));
             }
         }
+
+        [Fact]
+        public void SupportCastToTypeInExpression()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'
+        x:DataType='local:TestDataContext'>
+    <ContentControl Content='{CompiledBinding $parent.((local:TestDataContext)DataContext)}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var dataContext = new TestDataContext();
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(dataContext, contentControl.Content);
+            }
+        }
+
+        [Fact]
+        public void SupportCastToTypeInExpression_DifferentTypeEvaluatesToNull()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'
+        x:DataType='local:TestDataContext'>
+    <ContentControl Content='{CompiledBinding $parent.((local:TestDataContext)DataContext)}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var dataContext = "foo";
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(null, contentControl.Content);
+            }
+        }
+
+        [Fact]
+        public void SupportCastToTypeInExpressionWithProperty()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'
+        x:DataType='local:TestDataContext'>
+    <ContentControl Content='{CompiledBinding $parent.((local:TestDataContext)DataContext).StringProperty}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var dataContext = new TestDataContext
+                {
+                    StringProperty = "foobar"
+                };
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(dataContext.StringProperty, contentControl.Content);
+            }
+        }
+
+        [Fact]
+        public void SupportCastToTypeInExpressionWithProperty1()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'
+        x:DataType='local:TestDataContext'>
+    <ContentControl Content='{CompiledBinding $parent.DataContext(local:TestDataContext).StringProperty}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var dataContext = new TestDataContext
+                {
+                    StringProperty = "foobar"
+                };
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(dataContext.StringProperty, contentControl.Content);
+            }
+        }
+
+        [Fact]
+        public void SupportCastToTypeInExpressionWithPropertyIndexer()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'
+        x:DataType='local:TestDataContext'>
+    <ContentControl Content='{CompiledBinding ((local:TestData)ObjectsArrayProperty[0]).StringProperty}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var data = new TestData()
+                {
+                    StringProperty = "Foo"
+                };
+                var dataContext = new TestDataContext
+                {
+                    ObjectsArrayProperty = new object[] { data }
+                };
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(data.StringProperty, contentControl.Content);
+            }
+        }
+
+        [Fact]
+        public void SupportCastToTypeInExpressionWithProperty_DifferentTypeEvaluatesToNull()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'
+        x:DataType='local:TestDataContext'>
+    <ContentControl Content='{CompiledBinding $parent.((local:TestDataContext)DataContext).StringProperty}' Name='contentControl' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var contentControl = window.FindControl<ContentControl>("contentControl");
+
+                var dataContext = new TestDataContext
+                {
+                    StringProperty = "foobar"
+                };
+
+                window.DataContext = dataContext;
+
+                Assert.Equal(dataContext.StringProperty, contentControl.Content);
+
+                window.DataContext = "foo";
+
+                Assert.Equal(null, contentControl.Content);
+            }
+        }
     }
 
     public interface INonIntegerIndexer
     {
-        string this[string key] {get; set;}
+        string this[string key] { get; set; }
     }
 
     public interface INonIntegerIndexerDerived : INonIntegerIndexer
-    {}
+    { }
 
     public interface IHasProperty
     {
-        string StringProperty {get; set; }
+        string StringProperty { get; set; }
     }
 
     public interface IHasPropertyDerived : IHasProperty
-    {}
+    { }
+
+    public class AppendConverter : IValueConverter
+    {
+        public static IValueConverter Instance { get; } = new AppendConverter();
+
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+            => string.Format("{0}+{1}", value, parameter);
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+            => throw new NotImplementedException();
+
+    }
+
+    public class TestData
+    {
+        public string StringProperty { get; set; }
+    }
 
     public class TestDataContext : IHasPropertyDerived
     {
@@ -646,6 +865,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
 
         public string[] ArrayProperty { get; set; }
 
+        public object[] ObjectsArrayProperty { get; set; }
+
         public List<string> ListProperty { get; set; } = new List<string>();
 
         public NonIntegerIndexer NonIntegerIndexerProperty { get; set; } = new NonIntegerIndexer();