From 3e6402711c82b835b9462d1216bd671fa6333a50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 23 Jan 2016 18:19:20 +0100 Subject: [PATCH] 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; + } + } } }