From ac24ccaebddf710be06f3b2be06016c20c374074 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 30 Aug 2015 11:05:46 +0200 Subject: [PATCH] Allow setters to use observables for their value. --- Tests/Perspex.Styling.UnitTests/StyleTests.cs | 72 +++++++++++++++++++ src/Perspex.Styling/Styling/Setter.cs | 52 +++++++++++++- src/Perspex.Styling/Styling/Style.cs | 31 +++++++- src/Perspex.Styling/Styling/StyleBinding.cs | 59 +++++++++++---- 4 files changed, 199 insertions(+), 15 deletions(-) diff --git a/Tests/Perspex.Styling.UnitTests/StyleTests.cs b/Tests/Perspex.Styling.UnitTests/StyleTests.cs index ad67cefaba..8a05455352 100644 --- a/Tests/Perspex.Styling.UnitTests/StyleTests.cs +++ b/Tests/Perspex.Styling.UnitTests/StyleTests.cs @@ -8,6 +8,7 @@ namespace Perspex.Styling.UnitTests { using System; using System.Collections.Generic; + using System.Reactive.Subjects; using Perspex.Controls; using Perspex.Styling; using Xunit; @@ -107,6 +108,77 @@ namespace Perspex.Styling.UnitTests Assert.Equal(new[] { "foodefault", "Foo", "Bar", "foodefault" }, values); } + [Fact] + public void Style_With_Value_And_Source_Should_Throw_Exception() + { + var source = new BehaviorSubject("Foo"); + + Style style = new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter + { + Property = Class1.FooProperty, + Source = source, + Value = "Foo", + }, + }, + }; + + var target = new Class1(); + + Assert.Throws(() => style.Attach(target)); + } + + [Fact] + public void Style_With_Source_Should_Update_Value() + { + var source = new BehaviorSubject("Foo"); + + Style style = new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(Class1.FooProperty, source), + }, + }; + + var target = new Class1(); + + style.Attach(target); + + Assert.Equal("Foo", target.Foo); + source.OnNext("Bar"); + Assert.Equal("Bar", target.Foo); + } + + [Fact] + public void Style_With_Source_Should_Update_And_Restore_Value() + { + var source = new BehaviorSubject("Foo"); + + Style style = new Style(x => x.OfType().Class("foo")) + { + Setters = new[] + { + new Setter(Class1.FooProperty, source), + }, + }; + + var target = new Class1(); + + style.Attach(target); + + Assert.Equal("foodefault", target.Foo); + target.Classes.Add("foo"); + Assert.Equal("Foo", target.Foo); + source.OnNext("Bar"); + Assert.Equal("Bar", target.Foo); + target.Classes.Remove("foo"); + Assert.Equal("foodefault", target.Foo); + } + private class Class1 : Control { public static readonly PerspexProperty FooProperty = diff --git a/src/Perspex.Styling/Styling/Setter.cs b/src/Perspex.Styling/Styling/Setter.cs index 334854a0d0..f62c20ea8f 100644 --- a/src/Perspex.Styling/Styling/Setter.cs +++ b/src/Perspex.Styling/Styling/Setter.cs @@ -1,29 +1,79 @@ // ----------------------------------------------------------------------- // -// Copyright 2014 MIT Licence. See licence.md for more information. +// Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Styling { + using System; + + /// + /// A setter for a . + /// + /// + /// A is used to set a value on a + /// depending on a condition. + /// public class Setter { + /// + /// Initializes a new instance of the class. + /// public Setter() { + } + /// + /// Initializes a new instance of the class. + /// + /// The property to set. + /// The property value. public Setter(PerspexProperty property, object value) { this.Property = property; this.Value = value; } + /// + /// Initializes a new instance of the class. + /// + /// The property to set. + /// An observable which produces the value for the property. + public Setter(PerspexProperty property, IObservable source) + { + this.Property = property; + this.Source = source; + } + + /// + /// Gets or sets the property to set. + /// public PerspexProperty Property { get; set; } + /// + /// Gets or sets an observable which produces the value for the property. + /// + /// + /// Only one of and should be set. + /// + public IObservable Source + { + get; + set; + } + + /// + /// Gets or sets the property value. + /// + /// + /// Only one of and should be set. + /// public object Value { get; diff --git a/src/Perspex.Styling/Styling/Style.cs b/src/Perspex.Styling/Styling/Style.cs index 74447338e7..9b0de60f60 100644 --- a/src/Perspex.Styling/Styling/Style.cs +++ b/src/Perspex.Styling/Styling/Style.cs @@ -46,7 +46,19 @@ namespace Perspex.Styling { foreach (Setter setter in this.Setters) { - control.SetValue(setter.Property, setter.Value, BindingPriority.Style); + if (setter.Source != null && setter.Value != null) + { + throw new InvalidOperationException("Cannot set both Source and Value on a Setter."); + } + + if (setter.Source == null) + { + control.SetValue(setter.Property, setter.Value, BindingPriority.Style); + } + else + { + control.Bind(setter.Property, setter.Source, BindingPriority.Style); + } } } } @@ -54,7 +66,22 @@ namespace Perspex.Styling { foreach (Setter setter in this.Setters) { - var binding = new StyleBinding(match.ObservableResult, setter.Value, description); + if (setter.Source != null && setter.Value != null) + { + throw new InvalidOperationException("Cannot set both Source and Value on a Setter."); + } + + StyleBinding binding; + + if (setter.Source == null) + { + binding = new StyleBinding(match.ObservableResult, setter.Value, description); + } + else + { + binding = new StyleBinding(match.ObservableResult, setter.Source, description); + } + control.Bind(setter.Property, binding, BindingPriority.StyleTrigger); } } diff --git a/src/Perspex.Styling/Styling/StyleBinding.cs b/src/Perspex.Styling/Styling/StyleBinding.cs index 8d58cc3911..6fba6d8729 100644 --- a/src/Perspex.Styling/Styling/StyleBinding.cs +++ b/src/Perspex.Styling/Styling/StyleBinding.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------- // -// Copyright 2013 MIT Licence. See licence.md for more information. +// Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- @@ -8,14 +8,16 @@ namespace Perspex.Styling { using System; using System.Reactive; + using System.Reactive.Linq; /// /// Provides an observable for a style. /// /// - /// This class takes an activator and a value. The activator is an observable which produces - /// a bool. When the activator produces true, this observable will produce - /// . When the activator produces false it will produce + /// 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 @@ -42,12 +44,19 @@ namespace Perspex.Styling } /// - /// Gets a description of the binding. + /// Initializes a new instance of the class. /// - public string Description + /// The activator. + /// An observable that produces the activated value. + /// The binding description. + public StyleBinding( + IObservable activator, + IObservable source, + string description) { - get; - private set; + this.activator = activator; + this.Description = description; + this.Source = source; } /// @@ -59,6 +68,22 @@ namespace Perspex.Styling private set; } + /// + /// 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. /// @@ -67,10 +92,20 @@ namespace Perspex.Styling protected override IDisposable SubscribeCore(IObserver observer) { Contract.Requires(observer != null); - return this.activator.Subscribe( - active => observer.OnNext(active ? this.ActivatedValue : PerspexProperty.UnsetValue), - observer.OnError, - observer.OnCompleted); + + if (this.Source == null) + { + return this.activator.Subscribe( + active => observer.OnNext(active ? this.ActivatedValue : PerspexProperty.UnsetValue), + observer.OnError, + observer.OnCompleted); + } + else + { + return Observable + .CombineLatest(this.activator, this.Source, (x, y) => new { Active = x, Value = y }) + .Subscribe(x => observer.OnNext(x.Active ? x.Value : PerspexProperty.UnsetValue)); + } } } }