From 96ab9e62cd5b77865a0e283b559c3b4b6a88806f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Mar 2016 23:18:06 +0100 Subject: [PATCH] Added attached property support in bindings. --- .../Data/Parsers/ExpressionParser.cs | 51 +++++- ...xpressionObserverTests_AttachedProperty.cs | 147 ++++++++++++++++++ .../Perspex.Markup.UnitTests.csproj | 1 + 3 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs diff --git a/src/Markup/Perspex.Markup/Data/Parsers/ExpressionParser.cs b/src/Markup/Perspex.Markup/Data/Parsers/ExpressionParser.cs index 2ec7f49f5c..983336dc11 100644 --- a/src/Markup/Perspex.Markup/Data/Parsers/ExpressionParser.cs +++ b/src/Markup/Perspex.Markup/Data/Parsers/ExpressionParser.cs @@ -29,6 +29,10 @@ namespace Perspex.Markup.Data.Parsers case State.BeforeMember: state = ParseBeforeMember(r, nodes); break; + + case State.AttachedProperty: + state = ParseAttachedProperty(r, nodes); + break; } } @@ -52,6 +56,10 @@ namespace Perspex.Markup.Data.Parsers nodes.Add(new LogicalNotNode()); return State.Start; } + else if (ParseOpenBrace(r)) + { + return State.AttachedProperty; + } else { var identifier = IdentifierParser.Parse(r); @@ -93,15 +101,42 @@ namespace Perspex.Markup.Data.Parsers private static State ParseBeforeMember(Reader r, IList nodes) { - var identifier = IdentifierParser.Parse(r); + if (ParseOpenBrace(r)) + { + return State.AttachedProperty; + } + else + { + var identifier = IdentifierParser.Parse(r); + + if (identifier != null) + { + nodes.Add(new PropertyAccessorNode(identifier)); + return State.AfterMember; + } + + return State.End; + } + } + + private static State ParseAttachedProperty(Reader r, List nodes) + { + var owner = IdentifierParser.Parse(r); - if (identifier != null) + if (r.End || !r.TakeIf('.')) { - nodes.Add(new PropertyAccessorNode(identifier)); - return State.AfterMember; + throw new ExpressionParseException(r.Position, "Invalid attached property name."); } - return State.End; + var name = IdentifierParser.Parse(r); + + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + + nodes.Add(new PropertyAccessorNode(owner + '.' + name)); + return State.AfterMember; } private static bool ParseNot(Reader r) @@ -114,11 +149,17 @@ namespace Perspex.Markup.Data.Parsers return !r.End && r.TakeIf('.'); } + private static bool ParseOpenBrace(Reader r) + { + return !r.End && r.TakeIf('('); + } + private enum State { Start, AfterMember, BeforeMember, + AttachedProperty, End, } } diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs new file mode 100644 index 0000000000..b3aa287269 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_AttachedProperty.cs @@ -0,0 +1,147 @@ +// 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.Collections.Generic; +using System.Reactive.Linq; +using Perspex.Diagnostics; +using Perspex.Markup.Data; +using Xunit; + +namespace Perspex.Markup.UnitTests.Data +{ + public class ExpressionObserverTests_AttachedProperty + { + public ExpressionObserverTests_AttachedProperty() + { + var foo = Owner.FooProperty; + } + + [Fact] + public async void Should_Get_Attached_Property_Value() + { + var data = new Class1(); + var target = new ExpressionObserver(data, "(Owner.Foo)"); + var result = await target.Take(1); + + Assert.Equal("foo", result); + + Assert.Null(((IPerspexObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public async void Should_Get_Chained_Attached_Property_Value() + { + var data = new Class1 + { + Next = new Class1 + { + [Owner.FooProperty] = "bar", + } + }; + + var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); + var result = await target.Take(1); + + Assert.Equal("bar", result); + + Assert.Null(((IPerspexObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public void Should_Track_Simple_Attached_Value() + { + var data = new Class1(); + var target = new ExpressionObserver(data, "(Owner.Foo)"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.SetValue(Owner.FooProperty, "bar"); + + Assert.Equal(new[] { "foo", "bar" }, result); + + sub.Dispose(); + + Assert.Null(((IPerspexObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public void Should_Track_Chained_Attached_Value() + { + var data = new Class1 + { + Next = new Class1 + { + [Owner.FooProperty] = "foo", + } + }; + + var target = new ExpressionObserver(data, "Next.(Owner.Foo)"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Next.SetValue(Owner.FooProperty, "bar"); + + Assert.Equal(new[] { "foo", "bar" }, result); + + sub.Dispose(); + + Assert.Null(((IPerspexObjectDebug)data).GetPropertyChangedSubscribers()); + } + + [Fact] + public void Should_Not_Keep_Source_Alive() + { + Func> run = () => + { + var source = new Class1(); + var target = new ExpressionObserver(source, "(Owner.Foo)"); + return Tuple.Create(target, new WeakReference(source)); + }; + + var result = run(); + result.Item1.Subscribe(x => { }); + + GC.Collect(); + + Assert.Null(result.Item2.Target); + } + + [Fact] + public void Should_Fail_With_Attached_Property_With_Only_1_Part() + { + var data = new Class1(); + + Assert.Throws(() => new ExpressionObserver(data, "(Owner)")); + } + + [Fact] + public void Should_Fail_With_Attached_Property_With_More_Than_2_Parts() + { + var data = new Class1(); + + Assert.Throws(() => new ExpressionObserver(data, "(Owner.Foo.Bar)")); + } + + private static class Owner + { + public static readonly AttachedProperty FooProperty = + PerspexProperty.RegisterAttached( + "Foo", + typeof(Owner), + defaultValue: "foo"); + } + + private class Class1 : PerspexObject + { + public static readonly StyledProperty NextProperty = + PerspexProperty.Register("Next"); + + public Class1 Next + { + get { return GetValue(NextProperty); } + set { SetValue(NextProperty, value); } + } + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index f63b05c890..f32a584d1d 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -91,6 +91,7 @@ +