Browse Source

Implemented binding negation.

Steven Kirk 10 years ago
parent
commit
774a9c0911

+ 5 - 4
src/Markup/Perspex.Markup/Binding/ExpressionNode.cs

@@ -14,12 +14,13 @@ namespace Perspex.Markup.Binding
 
         private ExpressionValue _value = ExpressionValue.None;
 
-        public ExpressionNode Next
+        public ExpressionNode(ExpressionNode next)
         {
-            get;
-            set;
+            Next = next;
         }
 
+        public ExpressionNode Next { get; }
+
         public object Target
         {
             get { return _target; }
@@ -76,7 +77,7 @@ namespace Perspex.Markup.Binding
             return Next?.SetValue(value) ?? false;
         }
 
-        public IDisposable Subscribe(IObserver<ExpressionValue> observer)
+        public virtual IDisposable Subscribe(IObserver<ExpressionValue> observer)
         {
             if (Next != null)
             {

+ 30 - 37
src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs

@@ -21,33 +21,11 @@ namespace Perspex.Markup.Binding
 
             var syntaxTree = CSharpSyntaxTree.ParseText(expression, new CSharpParseOptions(kind: SourceCodeKind.Interactive));
             var syntaxRoot = syntaxTree.GetRoot();
-            var syntax = syntaxRoot.ChildNodes().SingleOrDefault()?.ChildNodes()?.SingleOrDefault() as ExpressionStatementSyntax;
+            var syntax = syntaxRoot.ChildNodes().SingleOrDefault()?.ChildNodes()?.SingleOrDefault();
 
             if (syntax != null)
             {
-                var result = new List<ExpressionNode>();
-
-                foreach (SyntaxNode node in syntax.ChildNodes())
-                {
-                    var identifier = node as IdentifierNameSyntax;
-                    var memberAccess = node as MemberAccessExpressionSyntax;
-
-                    if (identifier != null)
-                    {
-                        result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText));
-                    }
-                    else if (memberAccess != null)
-                    {
-                        Build(memberAccess, result);
-                    }
-                }
-
-                for (int i = 0; i < result.Count - 1; ++i)
-                {
-                    result[i].Next = result[i + 1];
-                }
-
-                return result[0];
+                return Build(expression, syntax, null);
             }
             else
             {
@@ -55,22 +33,37 @@ namespace Perspex.Markup.Binding
             }
         }
 
-        private static void Build(MemberAccessExpressionSyntax syntax, IList<ExpressionNode> result)
+        private static ExpressionNode Build(string expression, SyntaxNode syntax, ExpressionNode next)
         {
-            foreach (SyntaxNode node in syntax.ChildNodes())
-            {
-                var identifier = node as IdentifierNameSyntax;
-                var memberAccess = node as MemberAccessExpressionSyntax;
+            var expressionStatement = syntax as ExpressionStatementSyntax;
+            var identifier = syntax as IdentifierNameSyntax;
+            var memberAccess = syntax as MemberAccessExpressionSyntax;
+            var unaryExpression = syntax as PrefixUnaryExpressionSyntax;
 
-                if (identifier != null)
-                {
-                    result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText));
-                }
-                else if (memberAccess != null)
-                {
-                    Build(memberAccess, result);
-                }
+            if (expressionStatement != null)
+            {
+                return Build(expression, expressionStatement.Expression, next);
+            }
+            else if (identifier != null)
+            {
+                next = new PropertyAccessorNode(next, identifier.Identifier.ValueText);
             }
+            else if (memberAccess != null)
+            {
+                next = new PropertyAccessorNode(next, memberAccess.Name.Identifier.ValueText);
+                next = Build(expression, memberAccess.Expression, next);
+            }
+            else if (unaryExpression != null && unaryExpression.Kind() == SyntaxKind.LogicalNotExpression)
+            {
+                next = Build(expression, unaryExpression.Operand, next);
+                next = new LogicalNotNode(next);
+            }
+            else
+            {
+                throw new Exception($"Invalid expression: {expression}");
+            }
+
+            return next;
         }
     }
 }

+ 49 - 0
src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs

@@ -0,0 +1,49 @@
+// 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 System.Reactive.Linq;
+
+namespace Perspex.Markup.Binding
+{
+    internal class LogicalNotNode : ExpressionNode
+    {
+        public LogicalNotNode(ExpressionNode next)
+            : base(next)
+        {
+        }
+
+        public override IDisposable Subscribe(IObserver<ExpressionValue> observer)
+        {
+            return Next.Select(x => Negate(x)).Subscribe(observer);
+        }
+
+        protected override void SubscribeAndUpdate(object target)
+        {
+            CurrentValue = new ExpressionValue(target);
+        }
+
+        protected override void Unsubscribe(object target)
+        {
+        }
+
+        private ExpressionValue Negate(ExpressionValue v)
+        {
+            if (v.HasValue)
+            {
+                try
+                {
+                    var boolean = Convert.ToBoolean(v.Value, CultureInfo.InvariantCulture);
+                    return new ExpressionValue(!boolean);
+                }
+                catch
+                {
+                    // TODO: Maybe should log something here.
+                }
+            }
+
+            return ExpressionValue.None;
+        }
+    }
+}

+ 2 - 1
src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs

@@ -11,7 +11,8 @@ namespace Perspex.Markup.Binding
     {
         private PropertyInfo _propertyInfo;
 
-        public PropertyAccessorNode(string propertyName)
+        public PropertyAccessorNode(ExpressionNode next, string propertyName)
+            : base(next)
         {
             PropertyName = propertyName;
         }

+ 1 - 0
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@@ -36,6 +36,7 @@
   <ItemGroup>
     <Compile Include="Binding\ExpressionNodeBuilder.cs" />
     <Compile Include="Binding\ExpressionValue.cs" />
+    <Compile Include="Binding\LogicalNotNode.cs" />
     <Compile Include="Binding\PropertyAccessorNode.cs" />
     <Compile Include="Binding\ExpressionNode.cs" />
     <Compile Include="Binding\ExpressionObserver.cs" />

+ 27 - 0
tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs

@@ -25,6 +25,33 @@ namespace Perspex.Markup.UnitTests.Binding
 
             Assert.Equal(3, result.Count);
             Assert.IsType<PropertyAccessorNode>(result[0]);
+            Assert.IsType<PropertyAccessorNode>(result[1]);
+            Assert.IsType<PropertyAccessorNode>(result[2]);
+        }
+
+        [Fact]
+        public void Should_Build_Negated_Property_Chain()
+        {
+            var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz"));
+
+            Assert.Equal(4, result.Count);
+            Assert.IsType<LogicalNotNode>(result[0]);
+            Assert.IsType<PropertyAccessorNode>(result[1]);
+            Assert.IsType<PropertyAccessorNode>(result[2]);
+            Assert.IsType<PropertyAccessorNode>(result[3]);
+        }
+
+        [Fact]
+        public void Should_Build_Double_Negated_Property_Chain()
+        {
+            var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz"));
+
+            Assert.Equal(5, result.Count);
+            Assert.IsType<LogicalNotNode>(result[0]);
+            Assert.IsType<LogicalNotNode>(result[1]);
+            Assert.IsType<PropertyAccessorNode>(result[2]);
+            Assert.IsType<PropertyAccessorNode>(result[3]);
+            Assert.IsType<PropertyAccessorNode>(result[4]);
         }
 
         private List<ExpressionNode> ToList(ExpressionNode node)

+ 77 - 0
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs

@@ -0,0 +1,77 @@
+// 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.Reactive.Linq;
+using Perspex.Markup.Binding;
+using Xunit;
+
+namespace Perspex.Markup.UnitTests.Binding
+{
+    public class ExpressionObserverTests_Negation
+    {
+        [Fact]
+        public async void Should_Negate_Boolean_Value()
+        {
+            var data = new { Foo = true };
+            var target = new ExpressionObserver(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.True(result.HasValue);
+            Assert.Equal(false, result.Value);
+        }
+
+        [Fact]
+        public async void Should_Negate_0()
+        {
+            var data = new { Foo = 0 };
+            var target = new ExpressionObserver(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.True(result.HasValue);
+            Assert.Equal(true, result.Value);
+        }
+
+        [Fact]
+        public async void Should_Negate_1()
+        {
+            var data = new { Foo = 1 };
+            var target = new ExpressionObserver(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.True(result.HasValue);
+            Assert.Equal(false, result.Value);
+        }
+
+        [Fact]
+        public async void Should_Negate_False_String()
+        {
+            var data = new { Foo = "false" };
+            var target = new ExpressionObserver(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.True(result.HasValue);
+            Assert.Equal(true, result.Value);
+        }
+
+        [Fact]
+        public async void Should_Negate_True_String()
+        {
+            var data = new { Foo = "True" };
+            var target = new ExpressionObserver(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.True(result.HasValue);
+            Assert.Equal(false, result.Value);
+        }
+
+        [Fact]
+        public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean()
+        {
+            var data = new { Foo = new object() };
+            var target = new ExpressionObserver(data, "!Foo");
+            var result = await target.Take(1);
+
+            Assert.False(result.HasValue);
+        }
+    }
+}

+ 1 - 1
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs → tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs

@@ -9,7 +9,7 @@ using Xunit;
 
 namespace Perspex.Markup.UnitTests.Binding
 {
-    public class ExpressionObserverTests
+    public class ExpressionObserverTests_Property
     {
         [Fact]
         public async void Should_Get_Simple_Property_Value()

+ 2 - 1
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@@ -72,7 +72,8 @@
     </Reference>
   </ItemGroup>
   <ItemGroup>
-    <Compile Include="Binding\ExpressionObserverTests.cs" />
+    <Compile Include="Binding\ExpressionObserverTests_Negation.cs" />
+    <Compile Include="Binding\ExpressionObserverTests_Property.cs" />
     <Compile Include="Binding\ExpressionNodeBuilderTests.cs" />
     <Compile Include="Binding\NotifyingBase.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />