From b6c66c67e0b79226ef505aa0f547350836fcecb6 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Tue, 19 Jan 2016 16:55:06 +0300 Subject: [PATCH 1/8] Added Line and LineGeometry for line drawing. --- samples/TestApplicationShared/MainWindow.cs | 9 ++++ src/Perspex.Controls/Perspex.Controls.csproj | 1 + src/Perspex.Controls/Shapes/Line.cs | 36 +++++++++++++++ src/Perspex.SceneGraph/Media/LineGeometry.cs | 42 +++++++++++++++++ .../Perspex.SceneGraph.csproj | 1 + .../Perspex.Direct2D1.RenderTests.csproj | 1 + tests/Perspex.RenderTests/Shapes/LineTests.cs | 43 ++++++++++++++++++ .../Line/Circle_1px_Stroke.expected.png | Bin 0 -> 624 bytes 8 files changed, 133 insertions(+) create mode 100644 src/Perspex.Controls/Shapes/Line.cs create mode 100644 src/Perspex.SceneGraph/Media/LineGeometry.cs create mode 100644 tests/Perspex.RenderTests/Shapes/LineTests.cs create mode 100644 tests/TestFiles/Direct2D1/Shapes/Line/Circle_1px_Stroke.expected.png diff --git a/samples/TestApplicationShared/MainWindow.cs b/samples/TestApplicationShared/MainWindow.cs index 6a412a95b5..02edd23634 100644 --- a/samples/TestApplicationShared/MainWindow.cs +++ b/samples/TestApplicationShared/MainWindow.cs @@ -688,6 +688,15 @@ namespace TestApplication [Canvas.LeftProperty] = 130, [Canvas.TopProperty] = 79, }, + new Line + { + Width = 90, + Height = 70, + Stroke = Brushes.Red, + StrokeThickness = 2, + [Canvas.LeftProperty] = 30, + [Canvas.TopProperty] = 120 + } } }, } diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index a99b3fbbfd..394fdccd2d 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -47,6 +47,7 @@ + diff --git a/src/Perspex.Controls/Shapes/Line.cs b/src/Perspex.Controls/Shapes/Line.cs new file mode 100644 index 0000000000..785ea7bbd1 --- /dev/null +++ b/src/Perspex.Controls/Shapes/Line.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Perspex.Media; + +namespace Perspex.Controls.Shapes +{ + public class Line : Shape + { + private Geometry _geometry; + + private Size _geometrySize; + + public override Geometry DefiningGeometry + { + get + { + if (_geometry == null || _geometrySize != Bounds.Size) + { + var rect = new Rect(Bounds.Size).Deflate(StrokeThickness); + _geometry = new LineGeometry(rect.TopLeft, rect.BottomRight); + _geometrySize = Bounds.Size; + } + + return _geometry; + } + } + + protected override Size MeasureOverride(Size availableSize) + { + return new Size(StrokeThickness, StrokeThickness); + } + } +} diff --git a/src/Perspex.SceneGraph/Media/LineGeometry.cs b/src/Perspex.SceneGraph/Media/LineGeometry.cs new file mode 100644 index 0000000000..fb5219e82e --- /dev/null +++ b/src/Perspex.SceneGraph/Media/LineGeometry.cs @@ -0,0 +1,42 @@ +// 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 Perspex.Platform; + +namespace Perspex.Media +{ + /// + /// Represents the geometry of a line. + /// + public class LineGeometry : Geometry + { + /// + /// Initializes a new instance of the class. + /// + /// The start point. + /// The end point. + public LineGeometry(Point startPoint, Point endPoint) + { + IPlatformRenderInterface factory = PerspexLocator.Current.GetService(); + IStreamGeometryImpl impl = factory.CreateStreamGeometry(); + + using (IStreamGeometryContextImpl context = impl.Open()) + { + context.BeginFigure(startPoint, true); + context.LineTo(endPoint); + context.EndFigure(true); + } + + PlatformImpl = impl; + } + + /// + public override Rect Bounds => PlatformImpl.Bounds; + + /// + public override Geometry Clone() + { + return new LineGeometry(Bounds.TopLeft, Bounds.BottomRight); + } + } +} diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index 2121be7bfc..ac08406268 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -70,6 +70,7 @@ + diff --git a/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj b/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj index eb948aec81..2abf92fdb5 100644 --- a/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj +++ b/tests/Perspex.RenderTests/Perspex.Direct2D1.RenderTests.csproj @@ -76,6 +76,7 @@ + diff --git a/tests/Perspex.RenderTests/Shapes/LineTests.cs b/tests/Perspex.RenderTests/Shapes/LineTests.cs new file mode 100644 index 0000000000..bc5eb36dbf --- /dev/null +++ b/tests/Perspex.RenderTests/Shapes/LineTests.cs @@ -0,0 +1,43 @@ +// 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 Perspex.Controls; +using Perspex.Controls.Shapes; +using Perspex.Media; +using Xunit; + +#if PERSPEX_CAIRO +namespace Perspex.Cairo.RenderTests.Shapes +#elif PERSPEX_SKIA +namespace Perspex.Skia.RenderTests +#else +namespace Perspex.Direct2D1.RenderTests.Shapes +#endif +{ + public class LineTests : TestBase + { + public LineTests() + : base(@"Shapes\Line") + { + } + + [Fact] + public void Circle_1px_Stroke() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Line + { + Stroke = Brushes.Black, + StrokeThickness = 1, + } + }; + + RenderToFile(target); + CompareImages(); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Shapes/Line/Circle_1px_Stroke.expected.png b/tests/TestFiles/Direct2D1/Shapes/Line/Circle_1px_Stroke.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..539d27fdd330f6231a123ae7a8a57036b7472c0c GIT binary patch literal 624 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZk(GggNy^j3F{C2y?d6l44FLkI2jzPbXVhygd)O2w@@BEzssxER23w%Ex&$lx zg3ZS_=9tTKSKN3kz_~|&lVzgAC^4MEphbJq%ZLNb77Ppx{kzopr0F5k&$p8QV literal 0 HcmV?d00001 From e24770c0535261790e14028ad0b70274cb1fa220 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 19 Jan 2016 20:35:49 +0100 Subject: [PATCH 2/8] Use correct match logic in TreeDataTemplate. Use same as DataTemplate. --- .../Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index 0ff1ec7a6d..ceabd09bb7 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Reactive.Linq; +using System.Reflection; using Perspex.Controls; using Perspex.Controls.Templates; using Perspex.Data; @@ -27,10 +28,12 @@ namespace Perspex.Markup.Xaml.Templates { if (DataType == null) { - throw new InvalidOperationException("DataTemplate must have a DataType."); + return true; + } + else + { + return DataType.GetTypeInfo().IsAssignableFrom(data.GetType().GetTypeInfo()); } - - return DataType == data.GetType(); } public IEnumerable ItemsSelector(object item) From 2cfa6f842e7a6810c38a56f3585219f34cc22423 Mon Sep 17 00:00:00 2001 From: Ivan Kochurkin Date: Wed, 20 Jan 2016 14:30:17 +0300 Subject: [PATCH 3/8] Fixed @kekekeks notes. Fixed test name (Line instead of Circle). --- src/Perspex.SceneGraph/Media/LineGeometry.cs | 11 ++++++++--- .../Perspex.Cairo.RenderTests.csproj | 1 + .../Perspex.Skia.RenderTests.csproj | 1 + tests/Perspex.RenderTests/Shapes/LineTests.cs | 2 +- .../Shapes/Line/Line_1px_Stroke.expected.png} | Bin 624 -> 618 bytes .../Shapes/Line/Line_1px_Stroke.expected.png | Bin 0 -> 618 bytes .../Shapes/Line/Line_1px_Stroke.expected.png | Bin 0 -> 618 bytes 7 files changed, 11 insertions(+), 4 deletions(-) rename tests/TestFiles/{Direct2D1/Shapes/Line/Circle_1px_Stroke.expected.png => Cairo/Shapes/Line/Line_1px_Stroke.expected.png} (52%) create mode 100644 tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke.expected.png create mode 100644 tests/TestFiles/Skia/Shapes/Line/Line_1px_Stroke.expected.png diff --git a/src/Perspex.SceneGraph/Media/LineGeometry.cs b/src/Perspex.SceneGraph/Media/LineGeometry.cs index fb5219e82e..196fdc40eb 100644 --- a/src/Perspex.SceneGraph/Media/LineGeometry.cs +++ b/src/Perspex.SceneGraph/Media/LineGeometry.cs @@ -10,6 +10,9 @@ namespace Perspex.Media /// public class LineGeometry : Geometry { + private Point _startPoint; + private Point _endPoint; + /// /// Initializes a new instance of the class. /// @@ -17,21 +20,23 @@ namespace Perspex.Media /// The end point. public LineGeometry(Point startPoint, Point endPoint) { + _startPoint = startPoint; + _endPoint = endPoint; IPlatformRenderInterface factory = PerspexLocator.Current.GetService(); IStreamGeometryImpl impl = factory.CreateStreamGeometry(); using (IStreamGeometryContextImpl context = impl.Open()) { - context.BeginFigure(startPoint, true); + context.BeginFigure(startPoint, false); context.LineTo(endPoint); - context.EndFigure(true); + context.EndFigure(false); } PlatformImpl = impl; } /// - public override Rect Bounds => PlatformImpl.Bounds; + public override Rect Bounds => new Rect(_startPoint, _endPoint); /// public override Geometry Clone() diff --git a/tests/Perspex.RenderTests/Perspex.Cairo.RenderTests.csproj b/tests/Perspex.RenderTests/Perspex.Cairo.RenderTests.csproj index 60b3b02b11..dd6953d355 100644 --- a/tests/Perspex.RenderTests/Perspex.Cairo.RenderTests.csproj +++ b/tests/Perspex.RenderTests/Perspex.Cairo.RenderTests.csproj @@ -71,6 +71,7 @@ + diff --git a/tests/Perspex.RenderTests/Perspex.Skia.RenderTests.csproj b/tests/Perspex.RenderTests/Perspex.Skia.RenderTests.csproj index 14dd170a8f..08bba41fce 100644 --- a/tests/Perspex.RenderTests/Perspex.Skia.RenderTests.csproj +++ b/tests/Perspex.RenderTests/Perspex.Skia.RenderTests.csproj @@ -68,6 +68,7 @@ + diff --git a/tests/Perspex.RenderTests/Shapes/LineTests.cs b/tests/Perspex.RenderTests/Shapes/LineTests.cs index bc5eb36dbf..80eb7c75ca 100644 --- a/tests/Perspex.RenderTests/Shapes/LineTests.cs +++ b/tests/Perspex.RenderTests/Shapes/LineTests.cs @@ -22,7 +22,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes } [Fact] - public void Circle_1px_Stroke() + public void Line_1px_Stroke() { Decorator target = new Decorator { diff --git a/tests/TestFiles/Direct2D1/Shapes/Line/Circle_1px_Stroke.expected.png b/tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke.expected.png similarity index 52% rename from tests/TestFiles/Direct2D1/Shapes/Line/Circle_1px_Stroke.expected.png rename to tests/TestFiles/Cairo/Shapes/Line/Line_1px_Stroke.expected.png index 539d27fdd330f6231a123ae7a8a57036b7472c0c..18ade2da0f3d2f0e70679fdbcc139ddfc43265a6 100644 GIT binary patch delta 116 zcmeys@``1GqoatYi(^Pd+}q0=c@H^=upXQ|CtwQqBAzVOg}mpEaZWn=tirA8S2^n& z1~B*$e7dlH=lQ$O=KbdXvFY62KJGaiH_l<4I6*_DMYr$xb55|T0@LI@jNg)e&oR_| Q`5h$U>FVdQ&MBb@0Fp>CeElqK{*;pL_t(|UhUJ#34l-#1;HPTVVHj^rs0BK6zroOs$dvi06hQz z0DfRPKX9|hv1~UV3wYS$5DC1ok+lJlz#b!nE0XD0z=Jpd0DzB;u>xM=1gbGF?)Lxy N002ovPDHLkV1fyLDk}g0 diff --git a/tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke.expected.png b/tests/TestFiles/Direct2D1/Shapes/Line/Line_1px_Stroke.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..18ade2da0f3d2f0e70679fdbcc139ddfc43265a6 GIT binary patch literal 618 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>VM%xNb!1@J z*w6hZk(GggNyO8|F{C2y?d6TUha5y$4^EyFFok;&PnPOJ-gCz|CmnrO;a2slob?S* zD-iq$K3!P9^Zeas^M3RH*mQ1hANL#qPL_!dqr{L7gBIPsVM%xNb!1@J z*w6hZk(GggNyO8|F{C2y?d6TUha5y$4^EyFFok;&PnPOJ-gCz|CmnrO;a2slob?S* zD-iq$K3!P9^Zeas^M3RH*mQ1hANL#qPL_!dqr{L7gBIPs Date: Fri, 22 Jan 2016 22:04:10 +0100 Subject: [PATCH 4/8] More work on allowing bindings in setters. Support activated ISubjects. --- src/Perspex.Base/PerspexObjectExtensions.cs | 30 +++++ src/Perspex.Styling/Perspex.Styling.csproj | 4 +- .../Styling/ActivatedObservable.cs | 80 +++++++++++ .../Styling/ActivatedSubject.cs | 124 ++++++++++++++++++ src/Perspex.Styling/Styling/ActivatedValue.cs | 72 ++++++++++ src/Perspex.Styling/Styling/Setter.cs | 19 ++- src/Perspex.Styling/Styling/StyleBinding.cs | 107 --------------- .../ActivatedObservableTests.cs | 70 ++++++++++ .../ActivatedSubjectTests.cs | 46 +++++++ .../ActivatedValueTests.cs | 42 ++++++ .../Perspex.Styling.UnitTests.csproj | 4 +- .../StyleBindingTests.cs | 79 ----------- 12 files changed, 485 insertions(+), 192 deletions(-) create mode 100644 src/Perspex.Styling/Styling/ActivatedObservable.cs create mode 100644 src/Perspex.Styling/Styling/ActivatedSubject.cs create mode 100644 src/Perspex.Styling/Styling/ActivatedValue.cs delete mode 100644 src/Perspex.Styling/Styling/StyleBinding.cs create mode 100644 tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs create mode 100644 tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs create mode 100644 tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs delete mode 100644 tests/Perspex.Styling.UnitTests/StyleBindingTests.cs diff --git a/src/Perspex.Base/PerspexObjectExtensions.cs b/src/Perspex.Base/PerspexObjectExtensions.cs index d7b2d5a748..c958ecbffc 100644 --- a/src/Perspex.Base/PerspexObjectExtensions.cs +++ b/src/Perspex.Base/PerspexObjectExtensions.cs @@ -103,6 +103,36 @@ namespace Perspex GetDescription(o, property)); } + /// + /// Gets a subject for a . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this IPerspexObject o, + PerspexProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + // TODO: Subject.Create is not yet in stable Rx : once it is, remove the + // AnonymousSubject classes from this file and use Subject.Create. + var output = new Subject(); + var result = new AnonymousSubject( + Observer.Create( + x => output.OnNext(x), + e => output.OnError(e), + () => output.OnCompleted()), + o.GetObservable(property)); + o.Bind(property, output, priority); + return result; + } + /// /// Gets a subject for a . /// diff --git a/src/Perspex.Styling/Perspex.Styling.csproj b/src/Perspex.Styling/Perspex.Styling.csproj index 5f79ca5820..df3c17278d 100644 --- a/src/Perspex.Styling/Perspex.Styling.csproj +++ b/src/Perspex.Styling/Perspex.Styling.csproj @@ -45,6 +45,8 @@ + + @@ -59,7 +61,7 @@ - + diff --git a/src/Perspex.Styling/Styling/ActivatedObservable.cs b/src/Perspex.Styling/Styling/ActivatedObservable.cs new file mode 100644 index 0000000000..ed313fa3a6 --- /dev/null +++ b/src/Perspex.Styling/Styling/ActivatedObservable.cs @@ -0,0 +1,80 @@ +// 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.Reactive; +using System.Reactive.Linq; + +namespace Perspex.Styling +{ + /// + /// An observable which is switched on or off according to an activator observable. + /// + /// + /// An has two inputs: an activator observable a + /// observable which produces the activated value. When the activator + /// produces true, the will produce the current activated + /// value. When the activator produces false it will produce + /// . + /// + internal class ActivatedObservable : ObservableBase, IDescription + { + /// + /// The activator. + /// + private readonly IObservable _activator; + + /// + /// Initializes a new instance of the class. + /// + /// The activator. + /// An observable that produces the activated value. + /// The binding description. + public ActivatedObservable( + IObservable activator, + IObservable source, + string description) + { + _activator = activator; + Description = description; + Source = source; + } + + /// + /// Gets a description of the binding. + /// + public string Description + { + get; + } + + /// + /// Gets an observable which produces the . + /// + public IObservable Source + { + get; + } + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The observer. + /// IDisposable object used to unsubscribe from the observable sequence. + protected override IDisposable SubscribeCore(IObserver observer) + { + Contract.Requires(observer != null); + + var sourceCompleted = Source.TakeLast(1).Select(_ => Unit.Default); + var activatorCompleted = _activator.TakeLast(1).Select(_ => Unit.Default); + var completed = sourceCompleted.Merge(activatorCompleted); + + return _activator + .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) + .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue) + .DistinctUntilChanged() + .TakeUntil(completed) + .Subscribe(observer); + } + } +} diff --git a/src/Perspex.Styling/Styling/ActivatedSubject.cs b/src/Perspex.Styling/Styling/ActivatedSubject.cs new file mode 100644 index 0000000000..d99ffae105 --- /dev/null +++ b/src/Perspex.Styling/Styling/ActivatedSubject.cs @@ -0,0 +1,124 @@ +// 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.Reactive; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Perspex.Styling +{ + /// + /// A subject which is switched on or off according to an activator observable. + /// + /// + /// An has two inputs: an activator observable and either an + /// or a observable which produces the + /// activated value. When the activator produces true, the will + /// produce the current activated value. When the activator produces false it will produce + /// . + /// + internal class ActivatedSubject : ISubject, IDescription + { + private IObservable _activator; + private bool _active; + private object _pushValue; + + /// + /// Initializes a new instance of the class. + /// + /// The activator. + /// An observable that produces the activated value. + /// The binding description. + public ActivatedSubject( + IObservable activator, + ISubject source, + string description) + { + _activator = activator; + Description = description; + Source = source; + + _activator.Skip(1).Subscribe(ActivatorChanged); + } + + /// + /// Gets a description of the binding. + /// + public string Description + { + get; + } + + /// + /// Gets the underlying subject. + /// + public ISubject Source + { + get; + } + + /// + /// Notifies all subscribed observers about the end of the sequence. + /// + public void OnCompleted() + { + if (_active) + { + Source.OnCompleted(); + } + } + + /// + /// Notifies all subscribed observers with the exception. + /// + /// The exception to send to all subscribed observers. + /// is null. + public void OnError(Exception error) + { + if (_active) + { + Source.OnError(error); + } + } + + /// + /// Notifies all subscribed observers with the value. + /// + /// The value to send to all subscribed observers. + public void OnNext(object value) + { + _pushValue = value; + + if (_active) + { + Source.OnNext(value); + } + } + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The observer. + /// IDisposable object used to unsubscribe from the observable sequence. + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + + var completed = _activator.TakeLast(1).Select(_ => Unit.Default); + + return _activator + .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) + .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue) + .DistinctUntilChanged() + .TakeUntil(completed) + .Subscribe(observer); + } + + private void ActivatorChanged(bool active) + { + _active = active; + Source.OnNext(active ? _pushValue : PerspexProperty.UnsetValue); + } + } +} diff --git a/src/Perspex.Styling/Styling/ActivatedValue.cs b/src/Perspex.Styling/Styling/ActivatedValue.cs new file mode 100644 index 0000000000..0c02598086 --- /dev/null +++ b/src/Perspex.Styling/Styling/ActivatedValue.cs @@ -0,0 +1,72 @@ +// 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.Reactive; +using System.Reactive.Linq; + +namespace Perspex.Styling +{ + /// + /// An value which is switched on or off according to an activator observable. + /// + /// + /// An has two inputs: an activator observable and an + /// . When the activator produces true, the + /// will produce the current value. When the activator + /// produces false it will produce . + /// + internal class ActivatedValue : ObservableBase, IDescription + { + /// + /// The activator. + /// + private readonly IObservable _activator; + + /// + /// Initializes a new instance of the class. + /// + /// The activator. + /// The activated value. + /// The binding description. + public ActivatedValue( + IObservable activator, + object value, + string description) + { + _activator = activator; + Value = value; + Description = description; + } + + /// + /// Gets the activated value. + /// + public object Value + { + get; + } + + /// + /// Gets a description of the binding. + /// + public string Description + { + get; + } + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The observer. + /// IDisposable object used to unsubscribe from the observable sequence. + protected override IDisposable SubscribeCore(IObserver observer) + { + Contract.Requires(observer != null); + + return _activator + .Select(active => active ? Value : PerspexProperty.UnsetValue) + .Subscribe(observer); + } + } +} diff --git a/src/Perspex.Styling/Styling/Setter.cs b/src/Perspex.Styling/Styling/Setter.cs index e93729cf6e..103ebb6548 100644 --- a/src/Perspex.Styling/Styling/Setter.cs +++ b/src/Perspex.Styling/Styling/Setter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Subjects; using Perspex.Data; using Perspex.Metadata; @@ -77,8 +78,9 @@ namespace Perspex.Styling } else { - throw new NotSupportedException( - "Setter bindings with activators not yet supported."); + var subject = binding.CreateSubject(control, Property); + var activated = new ActivatedSubject(activator, subject, style.ToString()); + Bind(control, Property, binding, activated); } } else @@ -89,13 +91,22 @@ namespace Perspex.Styling } else { - var activated = new StyleBinding(activator, Value, style.ToString()); + var activated = new ActivatedValue(activator, Value, style.ToString()); control.Bind(Property, activated, BindingPriority.StyleTrigger); } } } private void Bind(IStyleable control, PerspexProperty property, IBinding binding) + { + Bind(control, property, binding, binding.CreateSubject(control, property)); + } + + private void Bind( + IStyleable control, + PerspexProperty property, + IBinding binding, + ISubject subject) { var mode = binding.Mode; @@ -106,7 +117,7 @@ namespace Perspex.Styling control.Bind( property, - binding.CreateSubject(control, property), + subject, mode, binding.Priority); } diff --git a/src/Perspex.Styling/Styling/StyleBinding.cs b/src/Perspex.Styling/Styling/StyleBinding.cs deleted file mode 100644 index b2ddbeee1b..0000000000 --- a/src/Perspex.Styling/Styling/StyleBinding.cs +++ /dev/null @@ -1,107 +0,0 @@ -// 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.Reactive; -using System.Reactive.Linq; - -namespace Perspex.Styling -{ - /// - /// Provides an observable for a style. - /// - /// - /// A has two inputs: an activator observable and either an - /// or a observable which produces the - /// activated value. When the activator produces true, the will - /// produce the current activated value. When the activator produces false it will produce - /// . - /// - internal class StyleBinding : ObservableBase, IDescription - { - /// - /// The activator. - /// - private readonly IObservable _activator; - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// The activated value. - /// The binding description. - public StyleBinding( - IObservable activator, - object activatedValue, - string description) - { - _activator = activator; - ActivatedValue = activatedValue; - Description = description; - } - - /// - /// Initializes a new instance of the class. - /// - /// The activator. - /// An observable that produces the activated value. - /// The binding description. - public StyleBinding( - IObservable activator, - IObservable source, - string description) - { - _activator = activator; - Description = description; - Source = source; - } - - /// - /// Gets the activated value. - /// - public object ActivatedValue - { - get; - } - - /// - /// Gets a description of the binding. - /// - public string Description - { - get; - } - - /// - /// Gets an observable which produces the . - /// - public IObservable Source - { - get; - } - - /// - /// Notifies the provider that an observer is to receive notifications. - /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - protected override IDisposable SubscribeCore(IObserver observer) - { - Contract.Requires(observer != null); - - if (Source == null) - { - return _activator - .Select(active => active ? ActivatedValue : PerspexProperty.UnsetValue) - .Subscribe(observer); - } - else - { - return _activator - .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) - .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue) - .Subscribe(observer); - } - } - } -} diff --git a/tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs b/tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs new file mode 100644 index 0000000000..e1beb992c2 --- /dev/null +++ b/tests/Perspex.Styling.UnitTests/ActivatedObservableTests.cs @@ -0,0 +1,70 @@ +// 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 System.Reactive.Subjects; +using Xunit; + +namespace Perspex.Styling.UnitTests +{ + public class ActivatedObservableTests + { + [Fact] + public void Should_Produce_Correct_Values() + { + var activator = new BehaviorSubject(false); + var source = new BehaviorSubject(1); + var target = new ActivatedObservable(activator, source, string.Empty); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + activator.OnNext(true); + source.OnNext(2); + activator.OnNext(false); + source.OnNext(3); + activator.OnNext(true); + + Assert.Equal( + new[] + { + PerspexProperty.UnsetValue, + 1, + 2, + PerspexProperty.UnsetValue, + 3, + }, + result); + } + + [Fact] + public void Should_Complete_When_Source_Completes() + { + var activator = new BehaviorSubject(false); + var source = new BehaviorSubject(1); + var target = new ActivatedObservable(activator, source, string.Empty); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + source.OnCompleted(); + + Assert.True(completed); + } + + [Fact] + public void Should_Complete_When_Activator_Completes() + { + var activator = new BehaviorSubject(false); + var source = new BehaviorSubject(1); + var target = new ActivatedObservable(activator, source, string.Empty); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + activator.OnCompleted(); + + Assert.True(completed); + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs new file mode 100644 index 0000000000..d4654dd199 --- /dev/null +++ b/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs @@ -0,0 +1,46 @@ +// 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.Subjects; +using Perspex.Data; +using Xunit; + +namespace Perspex.Styling.UnitTests +{ + public class ActivatedSubjectTests + { + [Fact] + public void Should_Set_Values() + { + var data = new Class1 { Foo = "foo" }; + var activator = new BehaviorSubject(false); + var source = data.GetSubject( + (PerspexProperty)Class1.FooProperty, + BindingPriority.LocalValue); + var target = new ActivatedSubject(activator, source, string.Empty); + + target.OnNext("bar"); + Assert.Equal("foo", data.Foo); + activator.OnNext(true); + target.OnNext("baz"); + Assert.Equal("baz", data.Foo); + activator.OnNext(false); + Assert.Equal("foo", data.Foo); + target.OnNext("bax"); + activator.OnNext(true); + Assert.Equal("bax", data.Foo); + } + + private class Class1 : PerspexObject + { + public static readonly PerspexProperty FooProperty = + PerspexProperty.Register("Foo", "foodefault"); + + public string Foo + { + get { return GetValue(FooProperty); } + set { SetValue(FooProperty, value); } + } + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs b/tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs new file mode 100644 index 0000000000..4921aeaa7b --- /dev/null +++ b/tests/Perspex.Styling.UnitTests/ActivatedValueTests.cs @@ -0,0 +1,42 @@ +// 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 System.Reactive.Subjects; +using Xunit; + +namespace Perspex.Styling.UnitTests +{ + public class ActivatedValueTests + { + [Fact] + public void Should_Produce_Correct_Values() + { + var activator = new BehaviorSubject(false); + var target = new ActivatedValue(activator, 1, string.Empty); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + activator.OnNext(true); + activator.OnNext(false); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, PerspexProperty.UnsetValue }, result); + } + + [Fact] + public void Should_Complete_When_Activator_Completes() + { + var activator = new BehaviorSubject(false); + var target = new ActivatedValue(activator, 1, string.Empty); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + activator.OnCompleted(); + + Assert.True(completed); + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj index b9e82d589d..af640ebdf4 100644 --- a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj +++ b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj @@ -80,6 +80,8 @@ + + @@ -90,7 +92,7 @@ - + diff --git a/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs b/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs deleted file mode 100644 index 82a7f66d07..0000000000 --- a/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -// 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 System.Reactive.Subjects; -using Xunit; - -namespace Perspex.Styling.UnitTests -{ - public class StyleBindingTests - { - [Fact] - public async void Should_Produce_UnsetValue_On_Activator_False() - { - var activator = new BehaviorSubject(false); - var target = new StyleBinding(activator, 1, string.Empty); - var result = await target.Take(1); - - Assert.Equal(PerspexProperty.UnsetValue, result); - } - - [Fact] - public async void Should_Produce_Value_On_Activator_True() - { - var activator = new BehaviorSubject(true); - var target = new StyleBinding(activator, 1, string.Empty); - var result = await target.Take(1); - - Assert.Equal(1, result); - } - - [Fact] - public void Should_Change_Value_On_Activator_Change() - { - var activator = new BehaviorSubject(false); - var target = new StyleBinding(activator, 1, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - activator.OnNext(false); - - Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, PerspexProperty.UnsetValue }, result); - } - - [Fact] - public void Should_Change_Value_With_Source_Observable() - { - var activator = new BehaviorSubject(false); - var source = new BehaviorSubject(1); - var target = new StyleBinding(activator, source, string.Empty); - var result = new List(); - - target.Subscribe(x => result.Add(x)); - - activator.OnNext(true); - source.OnNext(2); - activator.OnNext(false); - - Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, 2, PerspexProperty.UnsetValue }, result); - } - - [Fact] - public void Should_Complete_When_Activator_Completes() - { - var activator = new BehaviorSubject(false); - var target = new StyleBinding(activator, 1, string.Empty); - var completed = false; - - target.Subscribe(_ => { }, () => completed = true); - activator.OnCompleted(); - - Assert.True(completed); - } - } -} From ce997f6965e9e1190e1cecbb88190ff7f50e1253 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 22 Jan 2016 22:10:05 +0100 Subject: [PATCH 5/8] Refactor ActivatedSubject Inherit from ActivatedObject. --- .../Styling/ActivatedObservable.cs | 26 ++++------ .../Styling/ActivatedSubject.cs | 48 ++++--------------- 2 files changed, 18 insertions(+), 56 deletions(-) diff --git a/src/Perspex.Styling/Styling/ActivatedObservable.cs b/src/Perspex.Styling/Styling/ActivatedObservable.cs index ed313fa3a6..97c1b73106 100644 --- a/src/Perspex.Styling/Styling/ActivatedObservable.cs +++ b/src/Perspex.Styling/Styling/ActivatedObservable.cs @@ -19,11 +19,6 @@ namespace Perspex.Styling /// internal class ActivatedObservable : ObservableBase, IDescription { - /// - /// The activator. - /// - private readonly IObservable _activator; - /// /// Initializes a new instance of the class. /// @@ -35,26 +30,25 @@ namespace Perspex.Styling IObservable source, string description) { - _activator = activator; + Activator = activator; Description = description; Source = source; } + /// + /// Gets the activator observable. + /// + public IObservable Activator { get; } + /// /// Gets a description of the binding. /// - public string Description - { - get; - } + public string Description { get; } /// /// Gets an observable which produces the . /// - public IObservable Source - { - get; - } + public IObservable Source { get; } /// /// Notifies the provider that an observer is to receive notifications. @@ -66,10 +60,10 @@ namespace Perspex.Styling Contract.Requires(observer != null); var sourceCompleted = Source.TakeLast(1).Select(_ => Unit.Default); - var activatorCompleted = _activator.TakeLast(1).Select(_ => Unit.Default); + var activatorCompleted = Activator.TakeLast(1).Select(_ => Unit.Default); var completed = sourceCompleted.Merge(activatorCompleted); - return _activator + return Activator .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue) .DistinctUntilChanged() diff --git a/src/Perspex.Styling/Styling/ActivatedSubject.cs b/src/Perspex.Styling/Styling/ActivatedSubject.cs index d99ffae105..99da010dae 100644 --- a/src/Perspex.Styling/Styling/ActivatedSubject.cs +++ b/src/Perspex.Styling/Styling/ActivatedSubject.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -18,11 +17,10 @@ namespace Perspex.Styling /// produce the current activated value. When the activator produces false it will produce /// . /// - internal class ActivatedSubject : ISubject, IDescription + internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription { - private IObservable _activator; private bool _active; - private object _pushValue; + private object _value; /// /// Initializes a new instance of the class. @@ -34,28 +32,17 @@ namespace Perspex.Styling IObservable activator, ISubject source, string description) + : base(activator, source, description) { - _activator = activator; - Description = description; - Source = source; - - _activator.Skip(1).Subscribe(ActivatorChanged); - } - - /// - /// Gets a description of the binding. - /// - public string Description - { - get; + Activator.Skip(1).Subscribe(ActivatorChanged); } /// /// Gets the underlying subject. /// - public ISubject Source + public new ISubject Source { - get; + get { return (ISubject)base.Source; } } /// @@ -88,7 +75,7 @@ namespace Perspex.Styling /// The value to send to all subscribed observers. public void OnNext(object value) { - _pushValue = value; + _value = value; if (_active) { @@ -96,29 +83,10 @@ namespace Perspex.Styling } } - /// - /// Notifies the provider that an observer is to receive notifications. - /// - /// The observer. - /// IDisposable object used to unsubscribe from the observable sequence. - public IDisposable Subscribe(IObserver observer) - { - Contract.Requires(observer != null); - - var completed = _activator.TakeLast(1).Select(_ => Unit.Default); - - return _activator - .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) - .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue) - .DistinctUntilChanged() - .TakeUntil(completed) - .Subscribe(observer); - } - private void ActivatorChanged(bool active) { _active = active; - Source.OnNext(active ? _pushValue : PerspexProperty.UnsetValue); + Source.OnNext(active ? _value : PerspexProperty.UnsetValue); } } } From 3e6402711c82b835b9462d1216bd671fa6333a50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jan 2016 18:19:20 +0100 Subject: [PATCH 6/8] Make TreeViewItem.IsExpanded bindings work. --- .../ViewModels/MainWindowViewModel.cs | 34 ++++++++ .../ViewModels/TestNode.cs | 11 ++- .../Views/MainWindow.paml | 9 ++ .../XamlTestApplicationPcl.csproj | 16 ++++ .../XamlTestApplicationPcl/packages.config | 5 ++ .../Perspex.Markup.Xaml/Data/Binding.cs | 12 +-- .../MarkupExtensions/BindingExtension.cs | 2 + .../TemplateBindingExtension.cs | 3 +- src/Perspex.Base/Perspex.Base.csproj | 2 + src/Perspex.Base/PerspexObjectExtensions.cs | 51 +---------- .../Reactive/AnonymousSubject`1.cs | 16 ++++ .../Reactive/AnonymousSubject`2.cs | 49 +++++++++++ .../Generators/TreeItemContainerGenerator.cs | 1 - .../Primitives/ToggleButton.cs | 15 ++-- .../Styling/ActivatedSubject.cs | 31 +++++-- src/Perspex.Styling/Styling/Setter.cs | 9 +- .../StyleTests.cs | 87 +++++++++++++++++++ .../ActivatedSubjectTests.cs | 70 +++++++++++++-- 18 files changed, 340 insertions(+), 83 deletions(-) create mode 100644 src/Perspex.Base/Reactive/AnonymousSubject`1.cs create mode 100644 src/Perspex.Base/Reactive/AnonymousSubject`2.cs diff --git a/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs b/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs index 1523299d41..9ac24a622e 100644 --- a/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs +++ b/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,9 @@ // 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 ReactiveUI; namespace XamlTestApplication.ViewModels { @@ -22,6 +24,7 @@ namespace XamlTestApplication.ViewModels { Header = "Root", SubHeader = "Root Item", + IsExpanded = true, Children = new[] { new TestNode @@ -33,6 +36,7 @@ namespace XamlTestApplication.ViewModels { Header = "Child 2", SubHeader = "Child 2 Value", + IsExpanded = false, Children = new[] { new TestNode @@ -50,9 +54,39 @@ namespace XamlTestApplication.ViewModels } } }; + + CollapseNodesCommand = ReactiveCommand.Create(); + CollapseNodesCommand.Subscribe(_ => ExpandNodes(false)); + ExpandNodesCommand = ReactiveCommand.Create(); + ExpandNodesCommand.Subscribe(_ => ExpandNodes(true)); } public List Items { get; } public List Nodes { get; } + + public ReactiveCommand CollapseNodesCommand { get; } + + public ReactiveCommand ExpandNodesCommand { get; } + + public void ExpandNodes(bool expanded) + { + foreach (var node in Nodes) + { + ExpandNodes(node, expanded); + } + } + + private void ExpandNodes(TestNode node, bool expanded) + { + node.IsExpanded = expanded; + + if (node.Children != null) + { + foreach (var child in node.Children) + { + ExpandNodes(child, expanded); + } + } + } } } diff --git a/samples/XamlTestApplicationPcl/ViewModels/TestNode.cs b/samples/XamlTestApplicationPcl/ViewModels/TestNode.cs index 953bfd0f58..c2e0f4f15c 100644 --- a/samples/XamlTestApplicationPcl/ViewModels/TestNode.cs +++ b/samples/XamlTestApplicationPcl/ViewModels/TestNode.cs @@ -2,13 +2,22 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using ReactiveUI; namespace XamlTestApplication.ViewModels { - public class TestNode + public class TestNode : ReactiveObject { + private bool _isExpanded; + public string Header { get; set; } public string SubHeader { get; set; } public IEnumerable Children { get; set; } + + public bool IsExpanded + { + get { return _isExpanded; } + set { this.RaiseAndSetIfChanged(ref this._isExpanded, value); } + } } } \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index 98633354b1..584c43c3af 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -147,6 +147,11 @@ + + + @@ -156,6 +161,10 @@ + + + + diff --git a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj index a32ae5eec6..c7fb0e8484 100644 --- a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj +++ b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj @@ -123,6 +123,22 @@ ..\..\packages\Splat.1.6.2\lib\Portable-net45+win+wpa81+wp80\Splat.dll True + + ..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll + True + diff --git a/samples/XamlTestApplicationPcl/packages.config b/samples/XamlTestApplicationPcl/packages.config index 2115574cc5..749731a770 100644 --- a/samples/XamlTestApplicationPcl/packages.config +++ b/samples/XamlTestApplicationPcl/packages.config @@ -1,4 +1,9 @@  + + + + + \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 55b80f5d40..8a254cbf6e 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -71,21 +71,21 @@ namespace Perspex.Markup.Xaml.Data if (pathInfo.ElementName != null || ElementName != null) { - observer = CreateElementSubject( + observer = CreateElementObserver( (IControl)target, pathInfo.ElementName ?? ElementName, pathInfo.Path); } else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextSubject( + observer = CreateDataContexObserver( target, pathInfo.Path, targetIsDataContext); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentSubject( + observer = CreateTemplatedParentObserver( target, pathInfo.Path); } @@ -148,7 +148,7 @@ namespace Perspex.Markup.Xaml.Data } } - private ExpressionObserver CreateDataContextSubject( + private ExpressionObserver CreateDataContexObserver( IPerspexObject target, string path, bool targetIsDataContext) @@ -178,7 +178,7 @@ namespace Perspex.Markup.Xaml.Data } } - private ExpressionObserver CreateTemplatedParentSubject( + private ExpressionObserver CreateTemplatedParentObserver( IPerspexObject target, string path) { @@ -196,7 +196,7 @@ namespace Perspex.Markup.Xaml.Data return result; } - private ExpressionObserver CreateElementSubject( + private ExpressionObserver CreateElementObserver( IControl target, string elementName, string path) diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index e43ba9cb03..d8a668da08 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -27,6 +27,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions ElementName = ElementName, Mode = Mode, Path = Path, + Priority = Priority, }; } @@ -35,5 +36,6 @@ namespace Perspex.Markup.Xaml.MarkupExtensions public string ElementName { get; set; } public BindingMode Mode { get; set; } public string Path { get; set; } + public BindingPriority Priority { get; set; } = BindingPriority.LocalValue; } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs index 361b04c915..cec5146c71 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs @@ -25,9 +25,9 @@ namespace Perspex.Markup.Xaml.MarkupExtensions Converter = Converter, ElementName = ElementName, Mode = Mode, - Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), Path = Path, + Priority = Priority, }; } @@ -35,5 +35,6 @@ namespace Perspex.Markup.Xaml.MarkupExtensions public string ElementName { get; set; } public BindingMode Mode { get; set; } public string Path { get; set; } + public BindingPriority Priority { get; set; } = BindingPriority.TemplatedParent; } } \ No newline at end of file diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index 1ece57e81d..3a61fbc996 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -76,6 +76,8 @@ + + diff --git a/src/Perspex.Base/PerspexObjectExtensions.cs b/src/Perspex.Base/PerspexObjectExtensions.cs index c958ecbffc..6cd75b691d 100644 --- a/src/Perspex.Base/PerspexObjectExtensions.cs +++ b/src/Perspex.Base/PerspexObjectExtensions.cs @@ -121,7 +121,7 @@ namespace Perspex BindingPriority priority = BindingPriority.LocalValue) { // TODO: Subject.Create is not yet in stable Rx : once it is, remove the - // AnonymousSubject classes from this file and use Subject.Create. + // AnonymousSubject classes and use Subject.Create. var output = new Subject(); var result = new AnonymousSubject( Observer.Create( @@ -272,54 +272,5 @@ namespace Perspex handler(target)(e); } } - - class AnonymousSubject : ISubject - { - private readonly IObserver _observer; - private readonly IObservable _observable; - - public AnonymousSubject(IObserver observer, IObservable observable) - { - _observer = observer; - _observable = observable; - } - - public void OnCompleted() - { - _observer.OnCompleted(); - } - - public void OnError(Exception error) - { - if (error == null) - throw new ArgumentNullException("error"); - - _observer.OnError(error); - } - - public void OnNext(T value) - { - _observer.OnNext(value); - } - - public IDisposable Subscribe(IObserver observer) - { - if (observer == null) - throw new ArgumentNullException("observer"); - - // - // [OK] Use of unsafe Subscribe: non-pretentious wrapping of an observable sequence. - // - return _observable.Subscribe/*Unsafe*/(observer); - } - } - - class AnonymousSubject : AnonymousSubject, ISubject - { - public AnonymousSubject(IObserver observer, IObservable observable) - : base(observer, observable) - { - } - } } } diff --git a/src/Perspex.Base/Reactive/AnonymousSubject`1.cs b/src/Perspex.Base/Reactive/AnonymousSubject`1.cs new file mode 100644 index 0000000000..3bbf2295ff --- /dev/null +++ b/src/Perspex.Base/Reactive/AnonymousSubject`1.cs @@ -0,0 +1,16 @@ +// 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.Reactive.Subjects; + +namespace Perspex.Reactive +{ + public class AnonymousSubject : AnonymousSubject, ISubject + { + public AnonymousSubject(IObserver observer, IObservable observable) + : base(observer, observable) + { + } + } +} diff --git a/src/Perspex.Base/Reactive/AnonymousSubject`2.cs b/src/Perspex.Base/Reactive/AnonymousSubject`2.cs new file mode 100644 index 0000000000..04e58585b3 --- /dev/null +++ b/src/Perspex.Base/Reactive/AnonymousSubject`2.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.Reactive.Subjects; + +namespace Perspex.Reactive +{ + public class AnonymousSubject : ISubject + { + private readonly IObserver _observer; + private readonly IObservable _observable; + + public AnonymousSubject(IObserver observer, IObservable observable) + { + _observer = observer; + _observable = observable; + } + + public void OnCompleted() + { + _observer.OnCompleted(); + } + + public void OnError(Exception error) + { + if (error == null) + throw new ArgumentNullException("error"); + + _observer.OnError(error); + } + + public void OnNext(T value) + { + _observer.OnNext(value); + } + + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + throw new ArgumentNullException("observer"); + + // + // [OK] Use of unsafe Subscribe: non-pretentious wrapping of an observable sequence. + // + return _observable.Subscribe/*Unsafe*/(observer); + } + } +} diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index e630924a97..9feaab3d25 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -77,7 +77,6 @@ namespace Perspex.Controls.Generators result.SetValue(ContentProperty, template.Build(item)); result.SetValue(ItemsProperty, template.ItemsSelector(item)); - result.SetValue(IsExpandedProperty, template.IsExpanded(item)); if (!(item is IControl)) { diff --git a/src/Perspex.Controls/Primitives/ToggleButton.cs b/src/Perspex.Controls/Primitives/ToggleButton.cs index 601792ba1f..744af915c0 100644 --- a/src/Perspex.Controls/Primitives/ToggleButton.cs +++ b/src/Perspex.Controls/Primitives/ToggleButton.cs @@ -9,21 +9,22 @@ namespace Perspex.Controls.Primitives public class ToggleButton : Button { public static readonly PerspexProperty IsCheckedProperty = - PerspexProperty.Register("IsChecked"); + PerspexProperty.RegisterDirect( + "IsChecked", + o => o.IsChecked, + (o,v) => o.IsChecked = v); + + private bool _isChecked; static ToggleButton() { PseudoClass(IsCheckedProperty, ":checked"); } - public ToggleButton() - { - } - public bool IsChecked { - get { return GetValue(IsCheckedProperty); } - set { SetValue(IsCheckedProperty, value); } + get { return _isChecked; } + set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); } } protected override void OnClick(RoutedEventArgs e) diff --git a/src/Perspex.Styling/Styling/ActivatedSubject.cs b/src/Perspex.Styling/Styling/ActivatedSubject.cs index 99da010dae..3ccbfac6c0 100644 --- a/src/Perspex.Styling/Styling/ActivatedSubject.cs +++ b/src/Perspex.Styling/Styling/ActivatedSubject.cs @@ -19,7 +19,8 @@ namespace Perspex.Styling /// internal class ActivatedSubject : ActivatedObservable, ISubject, IDescription { - private bool _active; + private bool? _active; + private bool _completed; private object _value; /// @@ -34,7 +35,7 @@ namespace Perspex.Styling string description) : base(activator, source, description) { - Activator.Skip(1).Subscribe(ActivatorChanged); + Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted); } /// @@ -50,7 +51,7 @@ namespace Perspex.Styling /// public void OnCompleted() { - if (_active) + if (_active.Value && !_completed) { Source.OnCompleted(); } @@ -63,7 +64,7 @@ namespace Perspex.Styling /// is null. public void OnError(Exception error) { - if (_active) + if (_active.Value && !_completed) { Source.OnError(error); } @@ -77,7 +78,7 @@ namespace Perspex.Styling { _value = value; - if (_active) + if (_active.Value && !_completed) { Source.OnNext(value); } @@ -85,8 +86,26 @@ namespace Perspex.Styling private void ActivatorChanged(bool active) { + bool first = !_active.HasValue; + _active = active; - Source.OnNext(active ? _value : PerspexProperty.UnsetValue); + + if (!first) + { + Source.OnNext(active ? _value : PerspexProperty.UnsetValue); + } + } + + private void ActivatorCompleted() + { + _completed = true; + Source.OnCompleted(); + } + + private void ActivatorError(Exception e) + { + _completed = true; + Source.OnError(e); } } } diff --git a/src/Perspex.Styling/Styling/Setter.cs b/src/Perspex.Styling/Styling/Setter.cs index 103ebb6548..b224382f4a 100644 --- a/src/Perspex.Styling/Styling/Setter.cs +++ b/src/Perspex.Styling/Styling/Setter.cs @@ -5,6 +5,7 @@ using System; using System.Reactive.Subjects; using Perspex.Data; using Perspex.Metadata; +using Perspex.Reactive; namespace Perspex.Styling { @@ -63,6 +64,10 @@ namespace Perspex.Styling /// An optional activator. public void Apply(IStyle style, IStyleable control, IObservable activator) { + Contract.Requires(control != null); + + var description = style?.ToString(); + if (Property == null) { throw new InvalidOperationException("Setter.Property must be set."); @@ -79,7 +84,7 @@ namespace Perspex.Styling else { var subject = binding.CreateSubject(control, Property); - var activated = new ActivatedSubject(activator, subject, style.ToString()); + var activated = new ActivatedSubject(activator, subject, description); Bind(control, Property, binding, activated); } } @@ -91,7 +96,7 @@ namespace Perspex.Styling } else { - var activated = new ActivatedValue(activator, Value, style.ToString()); + var activated = new ActivatedValue(activator, Value, description); control.Bind(Property, activated, BindingPriority.StyleTrigger); } } diff --git a/tests/Perspex.Markup.Xaml.UnitTests/StyleTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/StyleTests.cs index 63c8751d82..a496f26fc5 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/StyleTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/StyleTests.cs @@ -2,7 +2,11 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Linq; +using System.Reactive.Linq; using Moq; +using Perspex.Controls; +using Perspex.Controls.Primitives; +using Perspex.Data; using Perspex.Markup.Xaml.Data; using Perspex.Platform; using Perspex.Styling; @@ -29,5 +33,88 @@ namespace Perspex.Markup.Xaml.UnitTests Assert.IsType(setter.Value); } } + + [Fact] + public void Setter_With_TwoWay_Binding_Should_Update_Source() + { + using (PerspexLocator.EnterScope()) + { + PerspexLocator.CurrentMutable + .Bind() + .ToConstant(Mock.Of(x => + x.CurrentThreadIsLoopThread == true)); + + var data = new Data + { + Foo = "foo", + }; + + var control = new TextBox + { + DataContext = data, + }; + + var setter = new Setter + { + Property = TextBox.TextProperty, + Value = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + } + }; + + setter.Apply(null, control, null); + Assert.Equal("foo", control.Text); + + control.Text = "bar"; + Assert.Equal("bar", data.Foo); + } + } + + [Fact] + public void Setter_With_TwoWay_Binding_And_Activator_Should_Update_Source() + { + using (PerspexLocator.EnterScope()) + { + PerspexLocator.CurrentMutable + .Bind() + .ToConstant(Mock.Of(x => + x.CurrentThreadIsLoopThread == true)); + + var data = new Data + { + Foo = "foo", + }; + + var control = new TextBox + { + DataContext = data, + }; + + var setter = new Setter + { + Property = TextBox.TextProperty, + Value = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + } + }; + + var activator = Observable.Never().StartWith(true); + + setter.Apply(null, control, activator); + Assert.Equal("foo", control.Text); + + control.Text = "bar"; + Assert.Equal("bar", data.Foo); + } + } + + private class Data + { + public string Foo { get; set; } + } } } diff --git a/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs b/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs index d4654dd199..58b7d9fd46 100644 --- a/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs +++ b/tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs @@ -1,8 +1,9 @@ // 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.Reactive.Disposables; using System.Reactive.Subjects; -using Perspex.Data; using Xunit; namespace Perspex.Styling.UnitTests @@ -12,23 +13,44 @@ namespace Perspex.Styling.UnitTests [Fact] public void Should_Set_Values() { - var data = new Class1 { Foo = "foo" }; var activator = new BehaviorSubject(false); - var source = data.GetSubject( - (PerspexProperty)Class1.FooProperty, - BindingPriority.LocalValue); + var source = new TestSubject(); var target = new ActivatedSubject(activator, source, string.Empty); target.OnNext("bar"); - Assert.Equal("foo", data.Foo); + Assert.Equal(PerspexProperty.UnsetValue, source.Value); activator.OnNext(true); target.OnNext("baz"); - Assert.Equal("baz", data.Foo); + Assert.Equal("baz", source.Value); activator.OnNext(false); - Assert.Equal("foo", data.Foo); + Assert.Equal(PerspexProperty.UnsetValue, source.Value); target.OnNext("bax"); activator.OnNext(true); - Assert.Equal("bax", data.Foo); + Assert.Equal("bax", source.Value); + } + + [Fact] + public void Should_Invoke_OnCompleted_On_Activator_Completed() + { + var activator = new BehaviorSubject(false); + var source = new TestSubject(); + var target = new ActivatedSubject(activator, source, string.Empty); + + activator.OnCompleted(); + + Assert.True(source.Completed); + } + + [Fact] + public void Should_Invoke_OnError_On_Activator_Error() + { + var activator = new BehaviorSubject(false); + var source = new TestSubject(); + var target = new ActivatedSubject(activator, source, string.Empty); + + activator.OnError(new Exception()); + + Assert.NotNull(source.Error); } private class Class1 : PerspexObject @@ -42,5 +64,35 @@ namespace Perspex.Styling.UnitTests set { SetValue(FooProperty, value); } } } + + private class TestSubject : ISubject + { + private IObserver _observer; + + public bool Completed { get; set; } + public Exception Error { get; set; } + public object Value { get; set; } = PerspexProperty.UnsetValue; + + public void OnCompleted() + { + Completed = true; + } + + public void OnError(Exception error) + { + Error = error; + } + + public void OnNext(object value) + { + Value = value; + } + + public IDisposable Subscribe(IObserver observer) + { + _observer = observer; + return Disposable.Empty; + } + } } } From bf30371721a37366356cebf4394d2dcdeedb8d5f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jan 2016 18:25:08 +0100 Subject: [PATCH 7/8] Removed ITreeDataTemplate.IsExpanded. --- samples/TestApplicationShared/App.cs | 5 +- .../Templates/FuncTreeDataTemplate.cs | 63 ------------------- .../Templates/FuncTreeDataTemplate`1.cs | 55 ---------------- .../Templates/ITreeDataTemplate.cs | 7 --- tests/Perspex.LeakTests/ControlTests.cs | 3 +- 5 files changed, 2 insertions(+), 131 deletions(-) diff --git a/samples/TestApplicationShared/App.cs b/samples/TestApplicationShared/App.cs index f8b64146d4..6429b76b32 100644 --- a/samples/TestApplicationShared/App.cs +++ b/samples/TestApplicationShared/App.cs @@ -21,11 +21,8 @@ namespace TestApplication { new FuncTreeDataTemplate( x => new TextBlock {Text = x.Name}, - x => x.Children, - x => true), + x => x.Children), }; } - - } } diff --git a/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs b/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs index d7ed589135..3d17cf5659 100644 --- a/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs @@ -14,8 +14,6 @@ namespace Perspex.Controls.Templates { private readonly Func _itemsSelector; - private readonly Func _isExpanded; - /// /// Initializes a new instance of the class. /// @@ -35,30 +33,6 @@ namespace Perspex.Controls.Templates { } - /// - /// Initializes a new instance of the class. - /// - /// The type of data which the data template matches. - /// - /// A function which when passed an object of returns a control. - /// - /// - /// A function which when passed an object of returns the child - /// items. - /// - /// - /// A function which when passed an object of returns the the - /// initial expanded state of the node. - /// - public FuncTreeDataTemplate( - Type type, - Func build, - Func itemsSelector, - Func isExpanded) - : this(o => IsInstance(o, type), build, itemsSelector, isExpanded) - { - } - /// /// Initializes a new instance of the class. /// @@ -75,46 +49,9 @@ namespace Perspex.Controls.Templates Func match, Func build, Func itemsSelector) - : this(match, build, itemsSelector, _ => false) - { - _itemsSelector = itemsSelector; - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// A function which determines whether the data template matches the specified data. - /// - /// - /// A function which when passed a matching object returns a control. - /// - /// - /// A function which when passed a matching object returns the child items. - /// - /// - /// A function which when passed a matching object returns the the initial expanded state - /// of the node. - /// - public FuncTreeDataTemplate( - Func match, - Func build, - Func itemsSelector, - Func isExpanded) : base(match, build) { _itemsSelector = itemsSelector; - _isExpanded = isExpanded; - } - - /// - /// Checks to see if the item should be initially expanded. - /// - /// The item. - /// True if the item should be initially expanded, otherwise false. - public bool IsExpanded(object item) - { - return this?._isExpanded(item) ?? false; } /// diff --git a/src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs index d55cfcf095..481eb673d7 100644 --- a/src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs +++ b/src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -32,32 +32,6 @@ namespace Perspex.Controls.Templates { } - /// - /// Initializes a new instance of the class. - /// - /// - /// A function which when passed an object of returns a control. - /// - /// - /// A function which when passed an object of returns the child - /// items. - /// - /// - /// A function which when passed an object of returns the the - /// initial expanded state of the node. - /// - public FuncTreeDataTemplate( - Func build, - Func itemsSelector, - Func isExpanded) - : base( - typeof(T), - Cast(build), - Cast(itemsSelector), - Cast(isExpanded)) - { - } - /// /// Initializes a new instance of the class. /// @@ -81,35 +55,6 @@ namespace Perspex.Controls.Templates { } - /// - /// Initializes a new instance of the class. - /// - /// - /// A function which determines whether the data template matches the specified data. - /// - /// - /// A function which when passed a matching object returns a control. - /// - /// - /// A function which when passed a matching object returns the child items. - /// - /// - /// A function which when passed a matching object returns the the initial expanded state - /// of the node. - /// - public FuncTreeDataTemplate( - Func match, - Func build, - Func itemsSelector, - Func isExpanded) - : base( - CastMatch(match), - Cast(build), - Cast(itemsSelector), - Cast(isExpanded)) - { - } - /// /// Casts a typed match function to an untyped match function. /// diff --git a/src/Perspex.Controls/Templates/ITreeDataTemplate.cs b/src/Perspex.Controls/Templates/ITreeDataTemplate.cs index ca967f60a4..6415ed37b2 100644 --- a/src/Perspex.Controls/Templates/ITreeDataTemplate.cs +++ b/src/Perspex.Controls/Templates/ITreeDataTemplate.cs @@ -10,13 +10,6 @@ namespace Perspex.Controls.Templates /// public interface ITreeDataTemplate : IDataTemplate { - /// - /// Checks to see if the item should be initially expanded. - /// - /// The item. - /// True if the item should be initially expanded, otherwise false. - bool IsExpanded(object item); - /// /// Selects the child items of an item. /// diff --git a/tests/Perspex.LeakTests/ControlTests.cs b/tests/Perspex.LeakTests/ControlTests.cs index a6275f452c..24bcda978b 100644 --- a/tests/Perspex.LeakTests/ControlTests.cs +++ b/tests/Perspex.LeakTests/ControlTests.cs @@ -273,8 +273,7 @@ namespace Perspex.LeakTests { new FuncTreeDataTemplate( x => new TextBlock { Text = x.Name }, - x => x.Children, - x => true) + x => x.Children) }, Items = nodes } From e08b9e3e4b28e017974ed0d40f5562aa1cc4fc47 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jan 2016 18:48:19 +0100 Subject: [PATCH 8/8] Removed PerspexObject.BindTwoWay. Instead use subjects and Bind. --- src/Perspex.Base/PerspexObject.cs | 64 +------------------ .../PerspexObjectTests_Binding.cs | 30 +-------- 2 files changed, 3 insertions(+), 91 deletions(-) diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 5c4271a57d..5310651514 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -185,7 +185,8 @@ namespace Perspex sourceBinding.Source.Bind(sourceBinding.Property, this.GetObservable(binding.Property), binding.Priority); break; case BindingMode.TwoWay: - BindTwoWay(binding.Property, sourceBinding.Source, sourceBinding.Property); + var subject = sourceBinding.Source.GetSubject(sourceBinding.Property, sourceBinding.Priority); + this.Bind(binding.Property, subject, BindingMode.TwoWay, sourceBinding.Priority); break; } } @@ -480,67 +481,6 @@ namespace Perspex } } - /// - /// Initiates a two-way binding between s. - /// - /// The property on this object. - /// The source object. - /// The property on the source object. - /// The priority of the binding. - /// - /// A disposable which can be used to terminate the binding. - /// - /// - /// The binding is first carried out from to this. - /// - public IDisposable BindTwoWay( - PerspexProperty property, - PerspexObject source, - PerspexProperty sourceProperty, - BindingPriority priority = BindingPriority.LocalValue) - { - VerifyAccess(); - _propertyLog.Verbose( - "Bound two way {Property} to {Binding} with priority {Priority}", - property, - source, - priority); - - return new CompositeDisposable( - Bind(property, source.GetObservable(sourceProperty)), - source.Bind(sourceProperty, this.GetObservable(property))); - } - - /// - /// Initiates a two-way binding between a and an - /// . - /// - /// The property on this object. - /// The subject to bind to. - /// The priority of the binding. - /// - /// A disposable which can be used to terminate the binding. - /// - /// - /// The binding is first carried out from to this. - /// - public IDisposable BindTwoWay( - PerspexProperty property, - ISubject source, - BindingPriority priority = BindingPriority.LocalValue) - { - VerifyAccess(); - _propertyLog.Verbose( - "Bound two way {Property} to {Binding} with priority {Priority}", - property, - GetDescription(source), - priority); - - return new CompositeDisposable( - Bind(property, source), - this.GetObservable(property).Subscribe(source)); - } - /// /// Forces the specified property to be revalidated. /// diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index 6788e509e2..3e7ee6f86d 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -82,7 +82,7 @@ namespace Perspex.Base.UnitTests } [Fact] - public void Two_Way_Binding_Works() + public void Two_Way_Separate_Binding_Works() { Class1 obj1 = new Class1(); Class1 obj2 = new Class1(); @@ -143,34 +143,6 @@ namespace Perspex.Base.UnitTests Assert.Equal("third", obj2.GetValue(Class1.FooProperty)); } - [Fact] - public void BindTwoWay_Gets_Initial_Value_From_Source() - { - Class1 source = new Class1(); - Class1 target = new Class1(); - - source.SetValue(Class1.FooProperty, "initial"); - target.BindTwoWay(Class1.FooProperty, source, Class1.FooProperty); - - Assert.Equal("initial", target.GetValue(Class1.FooProperty)); - } - - [Fact] - public void BindTwoWay_Updates_Values() - { - Class1 source = new Class1(); - Class1 target = new Class1(); - - source.SetValue(Class1.FooProperty, "first"); - target.BindTwoWay(Class1.FooProperty, source, Class1.FooProperty); - - Assert.Equal("first", target.GetValue(Class1.FooProperty)); - source.SetValue(Class1.FooProperty, "second"); - Assert.Equal("second", target.GetValue(Class1.FooProperty)); - target.SetValue(Class1.FooProperty, "third"); - Assert.Equal("third", source.GetValue(Class1.FooProperty)); - } - [Fact] public void Local_Binding_Overwrites_Local_Value() {