From 3bb9fe0b58a2fb971ce9ff60fa94d538570524e5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Mar 2016 21:29:57 +0200 Subject: [PATCH 01/22] Added BindingError class. --- src/Perspex.Base/Data/BindingError.cs | 32 ++++++++++++ src/Perspex.Base/Perspex.Base.csproj | 1 + src/Perspex.Base/PerspexObject.cs | 52 ++++++++++++++++++- src/Perspex.Base/PriorityBindingEntry.cs | 18 +++++-- src/Perspex.Base/PriorityLevel.cs | 24 ++++++++- src/Perspex.Base/PriorityValue.cs | 22 +++++++- .../Perspex.Base.UnitTests.csproj | 8 +-- .../PerspexObjectTests_Binding.cs | 33 ++++++++++++ .../PerspexObjectTests_Direct.cs | 30 +++++++++++ .../Perspex.UnitTests.csproj | 1 + tests/Perspex.UnitTests/TestLogSink.cs | 38 ++++++++++++++ 11 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 src/Perspex.Base/Data/BindingError.cs create mode 100644 tests/Perspex.UnitTests/TestLogSink.cs diff --git a/src/Perspex.Base/Data/BindingError.cs b/src/Perspex.Base/Data/BindingError.cs new file mode 100644 index 0000000000..d29b06fea9 --- /dev/null +++ b/src/Perspex.Base/Data/BindingError.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Perspex.Data +{ + /// + /// Represents a recoverable binding error. + /// + /// + /// When produced by a binding source observable, informs the binding system that an error + /// occurred. It causes a binding error to be logged: the value of the bound property will not + /// change. + /// + public class BindingError + { + /// + /// Initializes a new instance of the class. + /// + /// An exception describing the binding error. + public BindingError(Exception exception) + { + Exception = exception; + } + + /// + /// Gets the exception describing the binding error. + /// + public Exception Exception { get; } + } +} diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index ae185b3abf..84dbd9a0d3 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -43,6 +43,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 32f8eedc91..86ebb45921 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -403,9 +403,9 @@ namespace Perspex IDisposable subscription = null; subscription = source - .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType)) + .Select(x => CastOrDefault(x, property.PropertyType)) .Do(_ => { }, () => s_directBindings.Remove(subscription)) - .Subscribe(x => SetValue(property, x)); + .Subscribe(x => DirectBindingSet(property, x)); s_directBindings.Add(subscription); @@ -587,6 +587,27 @@ namespace Perspex } } + /// + /// Tries to cast a value to a type, taking into account that the value may be a + /// . + /// + /// The value. + /// The type. + /// The cast value, or a . + private static object CastOrDefault(object value, Type type) + { + var error = value as BindingError; + + if (error == null) + { + return TypeUtilities.CastOrDefault(value, type); + } + else + { + return error; + } + } + /// /// Creates a for a . /// @@ -635,6 +656,33 @@ namespace Perspex return result; } + /// + /// Sets a property value for a direct property binding. + /// + /// The property. + /// The value. + /// + private void DirectBindingSet(PerspexProperty property, object value) + { + var error = value as BindingError; + + if (error == null) + { + SetValue(property, value); + } + else + { + Logger.Error( + LogArea.Binding, + this, + "Error binding to {Target}.{PropertyName}: {Message}", + property.Name, + property.PropertyType, + value, + value.GetType()); + } + } + /// /// Converts an unset value to the default value for a direct property. /// diff --git a/src/Perspex.Base/PriorityBindingEntry.cs b/src/Perspex.Base/PriorityBindingEntry.cs index a17080f0f4..3fcb77f950 100644 --- a/src/Perspex.Base/PriorityBindingEntry.cs +++ b/src/Perspex.Base/PriorityBindingEntry.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 Perspex.Data; namespace Perspex { @@ -63,10 +64,12 @@ namespace Perspex /// The binding. /// Called when the binding changes. /// Called when the binding completes. + /// Called when a binding error occurs. public void Start( IObservable binding, Action changed, - Action completed) + Action completed, + Action error) { Contract.Requires(binding != null); Contract.Requires(changed != null); @@ -88,8 +91,17 @@ namespace Perspex _subscription = binding.Subscribe( value => { - Value = value; - changed(this); + var bindingError = value as BindingError; + + if (bindingError == null) + { + Value = value; + changed(this); + } + else if (error != null) + { + error(this, bindingError); + } }, () => completed(this)); } diff --git a/src/Perspex.Base/PriorityLevel.cs b/src/Perspex.Base/PriorityLevel.cs index 3c47199525..25cce3800b 100644 --- a/src/Perspex.Base/PriorityLevel.cs +++ b/src/Perspex.Base/PriorityLevel.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Perspex.Data; +using Perspex.Logging; namespace Perspex { @@ -52,6 +54,11 @@ namespace Perspex /// private readonly Action _changed; + /// + /// Method called when a binding error occurs. + /// + private readonly Action _error; + /// /// The current direct value. /// @@ -70,15 +77,18 @@ namespace Perspex /// The priority. /// The precedence mode. /// A method to be called when the current value changes. + /// A method to be called when a binding error occurs. public PriorityLevel( int priority, LevelPrecedenceMode mode, - Action changed) + Action changed, + Action error) { Contract.Requires(changed != null); _mode = mode; _changed = changed; + _error = error; Priority = priority; Value = _directValue = PerspexProperty.UnsetValue; ActiveBindingIndex = -1; @@ -135,7 +145,7 @@ namespace Perspex var entry = new PriorityBindingEntry(_nextIndex++); var node = Bindings.AddFirst(entry); - entry.Start(binding, Changed, Completed); + entry.Start(binding, Changed, Completed, Error); return Disposable.Create(() => { @@ -184,6 +194,16 @@ namespace Perspex } } + /// + /// Invoked when an entry in encounters a recoverable error. + /// + /// The entry that completed. + /// The error. + private void Error(PriorityBindingEntry entry, BindingError error) + { + _error(this, error); + } + /// /// Activates the first binding that has a value. /// diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index a8b72a376d..5c469003fb 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using System.Text; +using Perspex.Data; using Perspex.Logging; using Perspex.Utilities; @@ -210,7 +211,7 @@ namespace Perspex if (!_levels.TryGetValue(priority, out result)) { var mode = (LevelPrecedenceMode)(priority % 2); - result = new PriorityLevel(priority, mode, ValueChanged); + result = new PriorityLevel(priority, mode, ValueChanged, Error); _levels.Add(priority, result); } @@ -242,7 +243,7 @@ namespace Perspex else { Logger.Error( - LogArea.Property, + LogArea.Binding, _owner, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", _name, @@ -279,5 +280,22 @@ namespace Perspex } } } + + /// + /// Called when a priority level encounters an error. + /// + /// The priority level of the changed entry. + /// The binding error. + private void Error(PriorityLevel level, BindingError error) + { + Logger.Log( + LogEventLevel.Error, + LogArea.Binding, + _owner, + "Error binding to {Target}.{PropertyName}: {Message}", + _owner, + _name, + error.Exception.Message); + } } } diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index 879022611c..7e65cf4b1f 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -61,10 +61,6 @@ ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll True - - ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll - True - ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll @@ -124,6 +120,10 @@ {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Perspex.Base + + {88060192-33d5-4932-b0f9-8bd2763e857d} + Perspex.UnitTests + diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index caa36f38a8..097614013d 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -2,10 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Generic; using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.Reactive.Testing; using Perspex.Data; +using Perspex.Logging; +using Perspex.UnitTests; using Xunit; namespace Perspex.Base.UnitTests @@ -263,6 +267,35 @@ namespace Perspex.Base.UnitTests Assert.Equal("first", target2.GetValue(Class1.FooProperty)); } + [Fact] + public void Bind_Logs_BindingError() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + var expectedMessageTemplate = "Error binding to {Target}.{PropertyName}: {Message}"; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Error && + area == LogArea.Binding && + mt == expectedMessageTemplate) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + Assert.True(called); + } + } + /// /// Returns an observable that returns a single value but does not complete. /// diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index ebf0484ba8..48c6d2c9da 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Reactive.Subjects; using Perspex.Data; +using Perspex.Logging; +using Perspex.UnitTests; using Xunit; namespace Perspex.Base.UnitTests @@ -394,6 +396,34 @@ namespace Perspex.Base.UnitTests Assert.True(raised); } + [Fact] + public void Binding_To_Direct_Property_Logs_BindingError() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + var expectedMessageTemplate = "Error binding to {Target}.{PropertyName}: {Message}"; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Error && + area == LogArea.Binding && + mt == expectedMessageTemplate) + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.FooProperty, source); + source.OnNext("baz"); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + } + + Assert.True(called); + } + private class Class1 : PerspexObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Perspex.UnitTests/Perspex.UnitTests.csproj b/tests/Perspex.UnitTests/Perspex.UnitTests.csproj index d06a4e98da..44f6d01b26 100644 --- a/tests/Perspex.UnitTests/Perspex.UnitTests.csproj +++ b/tests/Perspex.UnitTests/Perspex.UnitTests.csproj @@ -63,6 +63,7 @@ + diff --git a/tests/Perspex.UnitTests/TestLogSink.cs b/tests/Perspex.UnitTests/TestLogSink.cs new file mode 100644 index 0000000000..d2ef00dce9 --- /dev/null +++ b/tests/Perspex.UnitTests/TestLogSink.cs @@ -0,0 +1,38 @@ +// 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 Perspex.Logging; + +namespace Perspex.UnitTests +{ + public delegate void LogCallback( + LogEventLevel level, + string area, + object source, + string messageTemplate, + params object[] propertyValues); + + public class TestLogSink : ILogSink + { + private LogCallback _callback; + + public TestLogSink(LogCallback callback) + { + _callback = callback; + } + + public static IDisposable Start(LogCallback callback) + { + var sink = new TestLogSink(callback); + Logger.Sink = sink; + return Disposable.Create(() => Logger.Sink = null); + } + + public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues) + { + _callback(level, area, source, messageTemplate, propertyValues); + } + } +} From b63a5b3e7c2590d9f2e25b3ae7ed832572515b90 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Mar 2016 21:39:19 +0200 Subject: [PATCH 02/22] WIP --- .../Perspex.Markup/DefaultValueConverter.cs | 9 ++---- src/Perspex.Base/Data/BindingError.cs | 31 +++++++++++++++++-- src/Perspex.Base/PerspexObject.cs | 5 +++ .../PerspexObjectTests_Binding.cs | 26 ++++++++++++++++ .../PerspexObjectTests_Direct.cs | 26 ++++++++++++++++ .../DefaultValueConverterTests.cs | 3 +- 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/Markup/Perspex.Markup/DefaultValueConverter.cs b/src/Markup/Perspex.Markup/DefaultValueConverter.cs index 9f535081a7..8a24b53612 100644 --- a/src/Markup/Perspex.Markup/DefaultValueConverter.cs +++ b/src/Markup/Perspex.Markup/DefaultValueConverter.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using System.Linq; using System.Reflection; +using Perspex.Data; using Perspex.Logging; using Perspex.Utilities; @@ -42,12 +43,8 @@ namespace Perspex.Markup if (value != null) { - Logger.Error( - LogArea.Binding, - this, - "Could not convert {Value} to {Type}", - value, - targetType); + var message = $"Could not convert {value} to {targetType}"; + return new BindingError(new InvalidCastException(message)); } return PerspexProperty.UnsetValue; diff --git a/src/Perspex.Base/Data/BindingError.cs b/src/Perspex.Base/Data/BindingError.cs index d29b06fea9..b9330bd278 100644 --- a/src/Perspex.Base/Data/BindingError.cs +++ b/src/Perspex.Base/Data/BindingError.cs @@ -10,8 +10,12 @@ namespace Perspex.Data /// /// /// When produced by a binding source observable, informs the binding system that an error - /// occurred. It causes a binding error to be logged: the value of the bound property will not - /// change. + /// occurred. It can also provide an optional fallback value to be pushed to the binding + /// target. + /// + /// Instead of using , one could simply not push a value (in the + /// case of a no fallback value) or push a fallback value, but BindingError also causes an + /// error to be logged with the correct binding target. /// public class BindingError { @@ -24,9 +28,32 @@ namespace Perspex.Data Exception = exception; } + /// + /// Initializes a new instance of the class. + /// + /// An exception describing the binding error. + /// The fallback value. + public BindingError(Exception exception, object fallbackValue) + { + Exception = exception; + FallbackValue = fallbackValue; + UseFallbackValue = true; + } + /// /// Gets the exception describing the binding error. /// public Exception Exception { get; } + + /// + /// Get the fallback value. + /// + public object FallbackValue { get; } + + /// + /// Get a value indicating whether the fallback value should be pushed to the binding + /// target. + /// + public bool UseFallbackValue { get; } } } diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 86ebb45921..34bf428cc1 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -672,6 +672,11 @@ namespace Perspex } else { + if (error.UseFallbackValue) + { + SetValue(property, error.FallbackValue); + } + Logger.Error( LogArea.Binding, this, diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index 097614013d..78292a5940 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -267,6 +267,32 @@ namespace Perspex.Base.UnitTests Assert.Equal("first", target2.GetValue(Class1.FooProperty)); } + [Fact] + public void BindingError_Does_Not_Cause_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + } + + [Fact] + public void BindingError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, source); + source.OnNext(6.7); + source.OnNext(new BindingError(new InvalidOperationException("Foo"), 8.9)); + + Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); + } + [Fact] public void Bind_Logs_BindingError() { diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index 48c6d2c9da..ceca0cdc38 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -396,6 +396,32 @@ namespace Perspex.Base.UnitTests Assert.True(raised); } + [Fact] + public void BindingError_Does_Not_Cause_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + + Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void BindingError_With_FallbackValue_Causes_Target_Update() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source); + source.OnNext("initial"); + source.OnNext(new BindingError(new InvalidOperationException("Foo"), "fallback")); + + Assert.Equal("fallback", target.GetValue(Class1.FooProperty)); + } + [Fact] public void Binding_To_Direct_Property_Logs_BindingError() { diff --git a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs index 55034a3c53..4b203974db 100644 --- a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs +++ b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs @@ -3,6 +3,7 @@ using System.Globalization; using Perspex.Controls; +using Perspex.Data; using Xunit; namespace Perspex.Markup.UnitTests @@ -114,7 +115,7 @@ namespace Perspex.Markup.UnitTests null, CultureInfo.InvariantCulture); - Assert.Equal(PerspexProperty.UnsetValue, result); + Assert.IsType(result); } private enum TestEnum From 87fa6327077debfce521036b14034cdea175ac1c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Apr 2016 20:01:32 +0200 Subject: [PATCH 03/22] Removed LevelPrecedenceMode. You should use direct properties if you want Latest mode. --- src/Perspex.Base/PriorityLevel.cs | 22 +------------------ src/Perspex.Base/PriorityValue.cs | 3 +-- .../PriorityValueTests.cs | 18 +-------------- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/src/Perspex.Base/PriorityLevel.cs b/src/Perspex.Base/PriorityLevel.cs index 3c47199525..0ca18e45bd 100644 --- a/src/Perspex.Base/PriorityLevel.cs +++ b/src/Perspex.Base/PriorityLevel.cs @@ -7,22 +7,6 @@ using System.Reactive.Disposables; namespace Perspex { - /// - /// Determines how the current binding is selected for a . - /// - internal enum LevelPrecedenceMode - { - /// - /// The latest fired binding is used as the current value. - /// - Latest, - - /// - /// The latest added binding is used as the current value. - /// - Newest, - } - /// /// Stores bindings for a priority level in a . /// @@ -62,8 +46,6 @@ namespace Perspex /// private int _nextIndex; - private readonly LevelPrecedenceMode _mode; - /// /// Initializes a new instance of the class. /// @@ -72,12 +54,10 @@ namespace Perspex /// A method to be called when the current value changes. public PriorityLevel( int priority, - LevelPrecedenceMode mode, Action changed) { Contract.Requires(changed != null); - _mode = mode; _changed = changed; Priority = priority; Value = _directValue = PerspexProperty.UnsetValue; @@ -155,7 +135,7 @@ namespace Perspex /// The entry that changed. private void Changed(PriorityBindingEntry entry) { - if (_mode == LevelPrecedenceMode.Latest || entry.Index >= ActiveBindingIndex) + if (entry.Index >= ActiveBindingIndex) { if (entry.Value != PerspexProperty.UnsetValue) { diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index a8b72a376d..7e10c6ca72 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -209,8 +209,7 @@ namespace Perspex if (!_levels.TryGetValue(priority, out result)) { - var mode = (LevelPrecedenceMode)(priority % 2); - result = new PriorityLevel(priority, mode, ValueChanged); + result = new PriorityLevel(priority, ValueChanged); _levels.Add(priority, result); } diff --git a/tests/Perspex.Base.UnitTests/PriorityValueTests.cs b/tests/Perspex.Base.UnitTests/PriorityValueTests.cs index 5894ea12c3..10f3bdb4db 100644 --- a/tests/Perspex.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Perspex.Base.UnitTests/PriorityValueTests.cs @@ -67,23 +67,7 @@ namespace Perspex.Base.UnitTests } [Fact] - public void Earlier_Binding_Firing_Should_Override_Later_Priority_0() - { - var target = new PriorityValue(null, "Test", typeof(string)); - var nonActive = new BehaviorSubject("na"); - var source = new BehaviorSubject("initial"); - - target.Add(nonActive, 0); - target.Add(source, 0); - Assert.Equal("initial", target.Value); - target.SetValue("first", 0); - Assert.Equal("first", target.Value); - nonActive.OnNext("second"); - Assert.Equal("second", target.Value); - } - - [Fact] - public void Earlier_Binding_Firing_Should_Not_Override_Later_Priority_1() + public void Earlier_Binding_Firing_Should_Not_Override_Later() { var target = new PriorityValue(null, "Test", typeof(string)); var nonActive = new BehaviorSubject("na"); From a91d21d0a4e1454c1c501be4b8d1973f070283b7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Apr 2016 21:14:47 +0200 Subject: [PATCH 04/22] Make some TextBox/Block properties direct. --- src/Perspex.Base/DirectProperty.cs | 20 ++++- src/Perspex.Base/PerspexProperty.cs | 11 ++- src/Perspex.Base/PerspexProperty`1.cs | 8 +- src/Perspex.Base/StyledPropertyBase.cs | 2 +- .../Presenters/TextPresenter.cs | 69 ++++++++++++---- src/Perspex.Controls/TextBlock.cs | 19 ++--- src/Perspex.Controls/TextBox.cs | 81 +++++++++++++------ 7 files changed, 151 insertions(+), 59 deletions(-) diff --git a/src/Perspex.Base/DirectProperty.cs b/src/Perspex.Base/DirectProperty.cs index 7ae19ec520..82d47e7c1d 100644 --- a/src/Perspex.Base/DirectProperty.cs +++ b/src/Perspex.Base/DirectProperty.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 Perspex.Data; namespace Perspex { @@ -44,11 +45,13 @@ namespace Perspex /// The property to copy. /// Gets the current value of the property. /// Sets the value of the property. May be null. + /// Optional overridden metadata. private DirectProperty( PerspexProperty source, Func getter, - Action setter) - : base(source, typeof(TOwner)) + Action setter, + PropertyMetadata metadata) + : base(source, typeof(TOwner), metadata) { Contract.Requires(getter != null); @@ -76,16 +79,25 @@ namespace Perspex /// Registers the direct property on another type. /// /// The type of the additional owner. + /// Gets the current value of the property. + /// Sets the value of the property. + /// + /// The value to use when the property is set to + /// + /// The default binding mode for the property. /// The property. public DirectProperty AddOwner( Func getter, - Action setter = null) + Action setter = null, + TValue unsetValue = default(TValue), + BindingMode defaultBindingMode = BindingMode.OneWay) where TNewOwner : PerspexObject { var result = new DirectProperty( this, getter, - setter); + setter, + new DirectPropertyMetadata(unsetValue, defaultBindingMode)); PerspexPropertyRegistry.Instance.Register(typeof(TNewOwner), result); return result; diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index e88c7f4418..36c534266e 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -71,7 +71,11 @@ namespace Perspex /// /// The direct property to copy. /// The new owner type. - protected PerspexProperty(PerspexProperty source, Type ownerType) + /// Optional overridden metadata. + protected PerspexProperty( + PerspexProperty source, + Type ownerType, + PropertyMetadata metadata) { Contract.Requires(source != null); Contract.Requires(ownerType != null); @@ -86,6 +90,11 @@ namespace Perspex Notifying = source.Notifying; Id = source.Id; _defaultMetadata = source._defaultMetadata; + + if (metadata != null) + { + _metadata.Add(ownerType, metadata); + } } /// diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs index 101aeaed22..1ad1cef2e1 100644 --- a/src/Perspex.Base/PerspexProperty`1.cs +++ b/src/Perspex.Base/PerspexProperty`1.cs @@ -32,8 +32,12 @@ namespace Perspex /// /// The property to copy. /// The new owner type. - protected PerspexProperty(PerspexProperty source, Type ownerType) - : base(source, ownerType) + /// Optional overridden metadata. + protected PerspexProperty( + PerspexProperty source, + Type ownerType, + PropertyMetadata metadata) + : base(source, ownerType, metadata) { } } diff --git a/src/Perspex.Base/StyledPropertyBase.cs b/src/Perspex.Base/StyledPropertyBase.cs index 2f5311d706..e77c5704eb 100644 --- a/src/Perspex.Base/StyledPropertyBase.cs +++ b/src/Perspex.Base/StyledPropertyBase.cs @@ -46,7 +46,7 @@ namespace Perspex /// The property to add the owner to. /// The type of the class that registers the property. protected StyledPropertyBase(StyledPropertyBase source, Type ownerType) - : base(source, ownerType) + : base(source, ownerType, null) { _inherits = source.Inherits; } diff --git a/src/Perspex.Controls/Presenters/TextPresenter.cs b/src/Perspex.Controls/Presenters/TextPresenter.cs index a08c1c7b5e..3bcc07a9e8 100644 --- a/src/Perspex.Controls/Presenters/TextPresenter.cs +++ b/src/Perspex.Controls/Presenters/TextPresenter.cs @@ -12,24 +12,28 @@ namespace Perspex.Controls.Presenters { public class TextPresenter : TextBlock { - public static readonly StyledProperty CaretIndexProperty = - TextBox.CaretIndexProperty.AddOwner(); + public static readonly DirectProperty CaretIndexProperty = + TextBox.CaretIndexProperty.AddOwner( + o => o.CaretIndex, + (o, v) => o.CaretIndex = v); - public static readonly StyledProperty SelectionStartProperty = - TextBox.SelectionStartProperty.AddOwner(); + public static readonly DirectProperty SelectionStartProperty = + TextBox.SelectionStartProperty.AddOwner( + o => o.SelectionStart, + (o, v) => o.SelectionStart = v); - public static readonly StyledProperty SelectionEndProperty = - TextBox.SelectionEndProperty.AddOwner(); + public static readonly DirectProperty SelectionEndProperty = + TextBox.SelectionEndProperty.AddOwner( + o => o.SelectionEnd, + (o, v) => o.SelectionEnd = v); private readonly DispatcherTimer _caretTimer; + private int _caretIndex; + private int _selectionStart; + private int _selectionEnd; private bool _caretBlink; private IBrush _highlightBrush; - static TextPresenter() - { - CaretIndexProperty.OverrideValidation((o, v) => v); - } - public TextPresenter() { _caretTimer = new DispatcherTimer(); @@ -47,20 +51,44 @@ namespace Perspex.Controls.Presenters public int CaretIndex { - get { return GetValue(CaretIndexProperty); } - set { SetValue(CaretIndexProperty, value); } + get + { + return _caretIndex; + } + + set + { + value = CoerceCaretIndex(value); + SetAndRaise(CaretIndexProperty, ref _caretIndex, value); + } } public int SelectionStart { - get { return GetValue(SelectionStartProperty); } - set { SetValue(SelectionStartProperty, value); } + get + { + return _selectionStart; + } + + set + { + value = CoerceCaretIndex(value); + SetAndRaise(SelectionStartProperty, ref _selectionStart, value); + } } public int SelectionEnd { - get { return GetValue(SelectionEndProperty); } - set { SetValue(SelectionEndProperty, value); } + get + { + return _selectionEnd; + } + + set + { + value = CoerceCaretIndex(value); + SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); + } } public int GetCaretIndex(Point point) @@ -206,6 +234,13 @@ namespace Perspex.Controls.Presenters } } + private int CoerceCaretIndex(int value) + { + var text = Text; + var length = text?.Length ?? 0; + return Math.Max(0, Math.Min(length, value)); + } + private void CaretTimerTick(object sender, EventArgs e) { _caretBlink = !_caretBlink; diff --git a/src/Perspex.Controls/TextBlock.cs b/src/Perspex.Controls/TextBlock.cs index 0ddc3c30ee..162774e732 100644 --- a/src/Perspex.Controls/TextBlock.cs +++ b/src/Perspex.Controls/TextBlock.cs @@ -72,8 +72,11 @@ namespace Perspex.Controls /// /// Defines the property. /// - public static readonly StyledProperty TextProperty = - PerspexProperty.Register(nameof(Text)); + public static readonly DirectProperty TextProperty = + PerspexProperty.RegisterDirect( + nameof(Text), + o => o.Text, + (o, v) => o.Text = v); /// /// Defines the property. @@ -87,14 +90,8 @@ namespace Perspex.Controls public static readonly StyledProperty TextWrappingProperty = PerspexProperty.Register(nameof(TextWrapping)); - /// - /// The formatted text used for rendering. - /// + private string _text; private FormattedText _formattedText; - - /// - /// Stores the last constraint passed to MeasureOverride. - /// private Size _constraint; /// @@ -140,8 +137,8 @@ namespace Perspex.Controls [Content] public string Text { - get { return GetValue(TextProperty); } - set { SetValue(TextProperty, value); } + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } } /// diff --git a/src/Perspex.Controls/TextBox.cs b/src/Perspex.Controls/TextBox.cs index d44401b1a3..3985dbef60 100644 --- a/src/Perspex.Controls/TextBox.cs +++ b/src/Perspex.Controls/TextBox.cs @@ -29,18 +29,29 @@ namespace Perspex.Controls public static readonly DirectProperty CanScrollHorizontallyProperty = PerspexProperty.RegisterDirect("CanScrollHorizontally", o => o.CanScrollHorizontally); - // TODO: Should CaretIndex, SelectionStart/End and Text be direct properties? - public static readonly StyledProperty CaretIndexProperty = - PerspexProperty.Register("CaretIndex", validate: ValidateCaretIndex); - - public static readonly StyledProperty SelectionStartProperty = - PerspexProperty.Register("SelectionStart", validate: ValidateCaretIndex); - - public static readonly StyledProperty SelectionEndProperty = - PerspexProperty.Register("SelectionEnd", validate: ValidateCaretIndex); - - public static readonly StyledProperty TextProperty = - TextBlock.TextProperty.AddOwner(); + public static readonly DirectProperty CaretIndexProperty = + PerspexProperty.RegisterDirect( + nameof(CaretIndex), + o => o.CaretIndex, + (o, v) => o.CaretIndex = v); + + public static readonly DirectProperty SelectionStartProperty = + PerspexProperty.RegisterDirect( + nameof(SelectionStart), + o => o.SelectionStart, + (o, v) => o.SelectionStart = v); + + public static readonly DirectProperty SelectionEndProperty = + PerspexProperty.RegisterDirect( + nameof(SelectionEnd), + o => o.SelectionEnd, + (o, v) => o.SelectionEnd = v); + + public static readonly DirectProperty TextProperty = + TextBlock.TextProperty.AddOwner( + o => o.Text, + (o, v) => o.Text = v, + defaultBindingMode: BindingMode.TwoWay); public static readonly StyledProperty TextAlignmentProperty = TextBlock.TextAlignmentProperty.AddOwner(); @@ -71,6 +82,10 @@ namespace Perspex.Controls public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); } + private string _text; + private int _caretIndex; + private int _selectionStart; + private int _selectionEnd; private bool _canScrollHorizontally; private TextPresenter _presenter; private UndoRedoHelper _undoRedoHelper; @@ -78,7 +93,6 @@ namespace Perspex.Controls static TextBox() { FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); - TextProperty.OverrideMetadata(new StyledPropertyMetadata(defaultBindingMode: BindingMode.TwoWay)); } public TextBox() @@ -117,10 +131,15 @@ namespace Perspex.Controls public int CaretIndex { - get { return GetValue(CaretIndexProperty); } + get + { + return _caretIndex; + } + set { - SetValue(CaretIndexProperty, value); + value = CoerceCaretIndex(value); + SetAndRaise(CaretIndexProperty, ref _caretIndex, value); if (_undoRedoHelper.IsLastState && _undoRedoHelper.LastState.Text == Text) _undoRedoHelper.UpdateLastState(); } @@ -128,21 +147,37 @@ namespace Perspex.Controls public int SelectionStart { - get { return GetValue(SelectionStartProperty); } - set { SetValue(SelectionStartProperty, value); } + get + { + return _selectionStart; + } + + set + { + value = CoerceCaretIndex(value); + SetAndRaise(SelectionStartProperty, ref _selectionStart, value); + } } public int SelectionEnd { - get { return GetValue(SelectionEndProperty); } - set { SetValue(SelectionEndProperty, value); } + get + { + return _selectionEnd; + } + + set + { + value = CoerceCaretIndex(value); + SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); + } } [Content] public string Text { - get { return GetValue(TextProperty); } - set { SetValue(TextProperty, value); } + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } } public TextAlignment TextAlignment @@ -426,9 +461,9 @@ namespace Perspex.Controls } } - private static int ValidateCaretIndex(PerspexObject o, int value) + private int CoerceCaretIndex(int value) { - var text = o.GetValue(TextProperty); + var text = Text; var length = text?.Length ?? 0; return Math.Max(0, Math.Min(length, value)); } From c984a504a2841c94047012342f6e81ca6e3aa26b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Apr 2016 22:18:19 +0200 Subject: [PATCH 05/22] Change the way PriorityValue notifies. Directly call a Changed method on the PriorityValue owner rather than using an observable. --- src/Perspex.Base/IPriorityValueOwner.cs | 19 ++++++ src/Perspex.Base/Perspex.Base.csproj | 1 + src/Perspex.Base/PerspexObject.cs | 56 ++++++++++-------- src/Perspex.Base/PriorityValue.cs | 36 ++++------- src/Perspex.Base/Properties/AssemblyInfo.cs | 3 +- .../Perspex.Base.UnitTests.csproj | 4 ++ .../PriorityValueTests.cs | 59 ++++++++++--------- tests/Perspex.Base.UnitTests/packages.config | 1 + 8 files changed, 101 insertions(+), 78 deletions(-) create mode 100644 src/Perspex.Base/IPriorityValueOwner.cs diff --git a/src/Perspex.Base/IPriorityValueOwner.cs b/src/Perspex.Base/IPriorityValueOwner.cs new file mode 100644 index 0000000000..aa79864794 --- /dev/null +++ b/src/Perspex.Base/IPriorityValueOwner.cs @@ -0,0 +1,19 @@ +// 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. + +namespace Perspex +{ + /// + /// An owner of a . + /// + internal interface IPriorityValueOwner + { + /// + /// Called when a 's value changes. + /// + /// The source of the change. + /// The old value. + /// The new value. + void Changed(PriorityValue sender, object oldValue, object newValue); + } +} diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index ae185b3abf..5718c9e217 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -54,6 +54,7 @@ + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 32f8eedc91..e787c97f57 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -21,7 +21,7 @@ namespace Perspex /// /// This class is analogous to DependencyObject in WPF. /// - public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged + public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged, IPriorityValueOwner { /// /// Maintains a list of direct property binding subscriptions so that the binding source @@ -477,6 +477,34 @@ namespace Perspex } } + /// + void IPriorityValueOwner.Changed(PriorityValue sender, object oldValue, object newValue) + { + var property = sender.Property; + var priority = (BindingPriority)sender.ValuePriority; + + oldValue = (oldValue == PerspexProperty.UnsetValue) ? + GetDefaultValue(property) : + oldValue; + newValue = (newValue == PerspexProperty.UnsetValue) ? + GetDefaultValue(property) : + newValue; + + if (!Equals(oldValue, newValue)) + { + RaisePropertyChanged(property, oldValue, newValue, priority); + + Logger.Verbose( + LogArea.Property, + this, + "{Property} changed from {$Old} to {$Value} with priority {Priority}", + property, + oldValue, + newValue, + priority); + } + } + /// Delegate[] IPerspexObjectDebug.GetPropertyChangedSubscribers() { @@ -604,34 +632,10 @@ namespace Perspex PriorityValue result = new PriorityValue( this, - property.Name, + property, property.PropertyType, validate2); - result.Changed.Subscribe(x => - { - object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ? - GetDefaultValue(property) : - x.Item1; - object newValue = (x.Item2 == PerspexProperty.UnsetValue) ? - GetDefaultValue(property) : - x.Item2; - - if (!Equals(oldValue, newValue)) - { - RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority); - - Logger.Verbose( - LogArea.Property, - this, - "{Property} changed from {$Old} to {$Value} with priority {Priority}", - property, - oldValue, - newValue, - (BindingPriority)result.ValuePriority); - } - }); - return result; } diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index 7e10c6ca72..5a1ae7a4e0 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -20,20 +20,16 @@ namespace Perspex /// represent higher priorites. The current is selected from the highest /// priority binding that doesn't return . Where there /// are multiple bindings registered with the same priority, the most recently added binding - /// has a higher priority. Each time the value changes, the observable is - /// fired with the old and new values. + /// has a higher priority. Each time the value changes, the + /// method on the + /// owner object is fired with the old and new values. /// internal class PriorityValue { /// /// The owner of the object. /// - private readonly PerspexObject _owner; - - /// - /// The name of the property. - /// - private readonly string _name; + private readonly IPriorityValueOwner _owner; /// /// The value type. @@ -45,11 +41,6 @@ namespace Perspex /// private readonly Dictionary _levels = new Dictionary(); - /// - /// The changed observable. - /// - private readonly Subject> _changed = new Subject>(); - /// /// The current value. /// @@ -64,17 +55,17 @@ namespace Perspex /// Initializes a new instance of the class. /// /// The owner of the object. - /// The name of the property. + /// The property that the value represents. /// The value type. /// An optional validation function. public PriorityValue( - PerspexObject owner, - string name, + IPriorityValueOwner owner, + PerspexProperty property, Type valueType, Func validate = null) { _owner = owner; - _name = name; + Property = property; _valueType = valueType; _value = PerspexProperty.UnsetValue; ValuePriority = int.MaxValue; @@ -82,12 +73,9 @@ namespace Perspex } /// - /// Fired whenever the current changes. + /// Gets the property that the value represents. /// - /// - /// The old and new values may be the same, this class does not check for distinct values. - /// - public IObservable> Changed => _changed; + public PerspexProperty Property { get; } /// /// Gets the current value. @@ -236,7 +224,7 @@ namespace Perspex ValuePriority = priority; _value = castValue; - _changed.OnNext(Tuple.Create(old, _value)); + _owner?.Changed(this, old, _value); } else { @@ -244,7 +232,7 @@ namespace Perspex LogArea.Property, _owner, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", - _name, + Property.Name, _valueType, value, value.GetType()); diff --git a/src/Perspex.Base/Properties/AssemblyInfo.cs b/src/Perspex.Base/Properties/AssemblyInfo.cs index 44faed797a..f3c46631b7 100644 --- a/src/Perspex.Base/Properties/AssemblyInfo.cs +++ b/src/Perspex.Base/Properties/AssemblyInfo.cs @@ -5,4 +5,5 @@ using System.Reflection; using System.Runtime.CompilerServices; [assembly: AssemblyTitle("Perspex.Base")] -[assembly: InternalsVisibleTo("Perspex.Base.UnitTests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Perspex.Base.UnitTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index 879022611c..3f1afa52f3 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -44,6 +44,10 @@ True + + ..\..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll + True + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll diff --git a/tests/Perspex.Base.UnitTests/PriorityValueTests.cs b/tests/Perspex.Base.UnitTests/PriorityValueTests.cs index 10f3bdb4db..7ae08c1c4c 100644 --- a/tests/Perspex.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Perspex.Base.UnitTests/PriorityValueTests.cs @@ -5,16 +5,23 @@ using System; using System.Linq; using System.Reactive.Linq; using System.Reactive.Subjects; +using Moq; using Xunit; namespace Perspex.Base.UnitTests { public class PriorityValueTests { + private static readonly PerspexProperty TestProperty = + new StyledProperty( + "Test", + typeof(PriorityValueTests), + new StyledPropertyMetadata()); + [Fact] public void Initial_Value_Should_Be_UnsetValue() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); Assert.Same(PerspexProperty.UnsetValue, target.Value); } @@ -22,7 +29,7 @@ namespace Perspex.Base.UnitTests [Fact] public void First_Binding_Sets_Value() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); target.Add(Single("foo"), 0); @@ -32,7 +39,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Changing_Binding_Should_Set_Value() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var subject = new BehaviorSubject("foo"); target.Add(subject, 0); @@ -44,7 +51,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Setting_Direct_Value_Should_Override_Binding() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); target.Add(Single("foo"), 0); target.SetValue("bar", 0); @@ -55,7 +62,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Binding_Firing_Should_Override_Direct_Value() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var source = new BehaviorSubject("initial"); target.Add(source, 0); @@ -69,7 +76,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Earlier_Binding_Firing_Should_Not_Override_Later() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var nonActive = new BehaviorSubject("na"); var source = new BehaviorSubject("initial"); @@ -85,7 +92,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Binding_Completing_Should_Revert_To_Direct_Value() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var source = new BehaviorSubject("initial"); target.Add(source, 0); @@ -101,7 +108,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Binding_With_Lower_Priority_Has_Precedence() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); target.Add(Single("foo"), 1); target.Add(Single("bar"), 0); @@ -113,7 +120,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Later_Binding_With_Same_Priority_Should_Take_Precedence() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); target.Add(Single("foo"), 1); target.Add(Single("bar"), 0); @@ -126,7 +133,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var subject = new BehaviorSubject("bar"); target.Add(Single("foo"), 0); @@ -139,7 +146,7 @@ namespace Perspex.Base.UnitTests [Fact] public void UnsetValue_Should_Fall_Back_To_Next_Binding() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var subject = new BehaviorSubject("bar"); target.Add(subject, 0); @@ -155,33 +162,31 @@ namespace Perspex.Base.UnitTests [Fact] public void Adding_Value_Should_Call_OnNext() { - var target = new PriorityValue(null, "Test", typeof(string)); - bool called = false; + var owner = new Mock(); + var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); - target.Changed.Subscribe(value => called = value.Item1 == PerspexProperty.UnsetValue && (string)value.Item2 == "foo"); target.Add(Single("foo"), 0); - Assert.True(called); + owner.Verify(x => x.Changed(target, PerspexProperty.UnsetValue, "foo")); } [Fact] public void Changing_Value_Should_Call_OnNext() { - var target = new PriorityValue(null, "Test", typeof(string)); + var owner = new Mock(); + var target = new PriorityValue(owner.Object, TestProperty, typeof(string)); var subject = new BehaviorSubject("foo"); - bool called = false; target.Add(subject, 0); - target.Changed.Subscribe(value => called = (string)value.Item1 == "foo" && (string)value.Item2 == "bar"); subject.OnNext("bar"); - Assert.True(called); + owner.Verify(x => x.Changed(target, "foo", "bar")); } [Fact] public void Disposing_A_Binding_Should_Revert_To_Next_Value() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); target.Add(Single("foo"), 0); var disposable = target.Add(Single("bar"), 0); @@ -194,7 +199,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Disposing_A_Binding_Should_Remove_BindingEntry() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); target.Add(Single("foo"), 0); var disposable = target.Add(Single("bar"), 0); @@ -207,7 +212,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Completing_A_Binding_Should_Revert_To_Previous_Binding() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var source = new BehaviorSubject("bar"); target.Add(Single("foo"), 0); @@ -221,7 +226,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Completing_A_Binding_Should_Revert_To_Lower_Priority() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var source = new BehaviorSubject("bar"); target.Add(Single("foo"), 1); @@ -235,7 +240,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Completing_A_Binding_Should_Remove_BindingEntry() { - var target = new PriorityValue(null, "Test", typeof(string)); + var target = new PriorityValue(null, TestProperty, typeof(string)); var subject = new BehaviorSubject("bar"); target.Add(Single("foo"), 0); @@ -249,7 +254,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Direct_Value_Should_Be_Coerced() { - var target = new PriorityValue(null, "Test", typeof(int), x => Math.Min((int)x, 10)); + var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10)); target.SetValue(5, 0); Assert.Equal(5, target.Value); @@ -260,7 +265,7 @@ namespace Perspex.Base.UnitTests [Fact] public void Bound_Value_Should_Be_Coerced() { - var target = new PriorityValue(null, "Test", typeof(int), x => Math.Min((int)x, 10)); + var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10)); var source = new Subject(); target.Add(source, 0); @@ -274,7 +279,7 @@ namespace Perspex.Base.UnitTests public void Revalidate_Should_ReCoerce_Value() { var max = 10; - var target = new PriorityValue(null, "Test", typeof(int), x => Math.Min((int)x, max)); + var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, max)); var source = new Subject(); target.Add(source, 0); diff --git a/tests/Perspex.Base.UnitTests/packages.config b/tests/Perspex.Base.UnitTests/packages.config index be674ecc80..c5e3427b0d 100644 --- a/tests/Perspex.Base.UnitTests/packages.config +++ b/tests/Perspex.Base.UnitTests/packages.config @@ -1,5 +1,6 @@  + From 7b371f8d2d7de95f2f920fec051a9ff78af7e848 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Apr 2016 22:33:22 +0200 Subject: [PATCH 06/22] Give PriorityLevel a reference to its PriorityValue. And directly call the LevelValueChanged method on it rather than using callbacks. --- src/Perspex.Base/PriorityLevel.cs | 32 ++++--------- src/Perspex.Base/PriorityValue.cs | 80 ++++++++++++------------------- 2 files changed, 40 insertions(+), 72 deletions(-) diff --git a/src/Perspex.Base/PriorityLevel.cs b/src/Perspex.Base/PriorityLevel.cs index 0ca18e45bd..e0d096a835 100644 --- a/src/Perspex.Base/PriorityLevel.cs +++ b/src/Perspex.Base/PriorityLevel.cs @@ -31,34 +31,22 @@ namespace Perspex /// internal class PriorityLevel { - /// - /// Method called when current value changes. - /// - private readonly Action _changed; - - /// - /// The current direct value. - /// + private PriorityValue _owner; private object _directValue; - - /// - /// The index of the next . - /// private int _nextIndex; /// /// Initializes a new instance of the class. /// + /// The owner. /// The priority. - /// The precedence mode. - /// A method to be called when the current value changes. public PriorityLevel( - int priority, - Action changed) + PriorityValue owner, + int priority) { - Contract.Requires(changed != null); + Contract.Requires(owner != null); - _changed = changed; + _owner = owner; Priority = priority; Value = _directValue = PerspexProperty.UnsetValue; ActiveBindingIndex = -1; @@ -83,7 +71,7 @@ namespace Perspex set { Value = _directValue = value; - _changed(this); + _owner.LevelValueChanged(this); } } @@ -141,7 +129,7 @@ namespace Perspex { Value = entry.Value; ActiveBindingIndex = entry.Index; - _changed(this); + _owner.LevelValueChanged(this); } else { @@ -175,14 +163,14 @@ namespace Perspex { Value = binding.Value; ActiveBindingIndex = binding.Index; - _changed(this); + _owner.LevelValueChanged(this); return; } } Value = DirectValue; ActiveBindingIndex = -1; - _changed(this); + _owner.LevelValueChanged(this); } } } diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index 5a1ae7a4e0..50dbc5c0f7 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Subjects; using System.Text; using Perspex.Logging; using Perspex.Utilities; @@ -21,34 +20,15 @@ namespace Perspex /// priority binding that doesn't return . Where there /// are multiple bindings registered with the same priority, the most recently added binding /// has a higher priority. Each time the value changes, the - /// method on the + /// method on the /// owner object is fired with the old and new values. /// internal class PriorityValue { - /// - /// The owner of the object. - /// private readonly IPriorityValueOwner _owner; - - /// - /// The value type. - /// private readonly Type _valueType; - - /// - /// The currently registered bindings organised by priority. - /// private readonly Dictionary _levels = new Dictionary(); - - /// - /// The current value. - /// private object _value; - - /// - /// The function used to validate the value, if any. - /// private readonly Func _validate; /// @@ -169,6 +149,34 @@ namespace Perspex return b.ToString(); } + /// + /// Called when the value for a priority level changes. + /// + /// The priority level of the changed entry. + public void LevelValueChanged(PriorityLevel level) + { + if (level.Priority <= ValuePriority) + { + if (level.Value != PerspexProperty.UnsetValue) + { + UpdateValue(level.Value, level.Priority); + } + else + { + foreach (var i in _levels.Values.OrderBy(x => x.Priority)) + { + if (i.Value != PerspexProperty.UnsetValue) + { + UpdateValue(i.Value, i.Priority); + return; + } + } + + UpdateValue(PerspexProperty.UnsetValue, int.MaxValue); + } + } + } + /// /// Causes a revalidation of the value. /// @@ -197,7 +205,7 @@ namespace Perspex if (!_levels.TryGetValue(priority, out result)) { - result = new PriorityLevel(priority, ValueChanged); + result = new PriorityLevel(this, priority); _levels.Add(priority, result); } @@ -238,33 +246,5 @@ namespace Perspex value.GetType()); } } - - /// - /// Called when the value for a priority level changes. - /// - /// The priority level of the changed entry. - private void ValueChanged(PriorityLevel level) - { - if (level.Priority <= ValuePriority) - { - if (level.Value != PerspexProperty.UnsetValue) - { - UpdateValue(level.Value, level.Priority); - } - else - { - foreach (var i in _levels.Values.OrderBy(x => x.Priority)) - { - if (i.Value != PerspexProperty.UnsetValue) - { - UpdateValue(i.Value, i.Priority); - return; - } - } - - UpdateValue(PerspexProperty.UnsetValue, int.MaxValue); - } - } - } } } From 4f7e7cad1b116e89c51f1fca3e3d811f8399c309 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Apr 2016 22:50:31 +0200 Subject: [PATCH 07/22] Make PriorityBindingEntry have ref to owner. --- src/Perspex.Base/PriorityBindingEntry.cs | 36 +++++++++++------------- src/Perspex.Base/PriorityLevel.cs | 8 +++--- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Perspex.Base/PriorityBindingEntry.cs b/src/Perspex.Base/PriorityBindingEntry.cs index a17080f0f4..81983eb64e 100644 --- a/src/Perspex.Base/PriorityBindingEntry.cs +++ b/src/Perspex.Base/PriorityBindingEntry.cs @@ -10,19 +10,19 @@ namespace Perspex /// internal class PriorityBindingEntry : IDisposable { - /// - /// The binding subscription. - /// + private PriorityLevel _owner; private IDisposable _subscription; /// /// Initializes a new instance of the class. /// + /// The owner. /// /// The binding index. Later bindings should have higher indexes. /// - public PriorityBindingEntry(int index) + public PriorityBindingEntry(PriorityLevel owner, int index) { + _owner = owner; Index = index; } @@ -61,16 +61,9 @@ namespace Perspex /// Starts listening to the binding. /// /// The binding. - /// Called when the binding changes. - /// Called when the binding completes. - public void Start( - IObservable binding, - Action changed, - Action completed) + public void Start(IObservable binding) { Contract.Requires(binding != null); - Contract.Requires(changed != null); - Contract.Requires(completed != null); if (_subscription != null) { @@ -85,13 +78,7 @@ namespace Perspex Description = ((IDescription)binding).Description; } - _subscription = binding.Subscribe( - value => - { - Value = value; - changed(this); - }, - () => completed(this)); + _subscription = binding.Subscribe(ValueChanged, Completed); } /// @@ -101,5 +88,16 @@ namespace Perspex { _subscription?.Dispose(); } + + private void ValueChanged(object value) + { + Value = value; + _owner.Changed(this); + } + + private void Completed() + { + _owner.Completed(this); + } } } diff --git a/src/Perspex.Base/PriorityLevel.cs b/src/Perspex.Base/PriorityLevel.cs index e0d096a835..b3b4c10478 100644 --- a/src/Perspex.Base/PriorityLevel.cs +++ b/src/Perspex.Base/PriorityLevel.cs @@ -100,10 +100,10 @@ namespace Perspex { Contract.Requires(binding != null); - var entry = new PriorityBindingEntry(_nextIndex++); + var entry = new PriorityBindingEntry(this, _nextIndex++); var node = Bindings.AddFirst(entry); - entry.Start(binding, Changed, Completed); + entry.Start(binding); return Disposable.Create(() => { @@ -121,7 +121,7 @@ namespace Perspex /// Invoked when an entry in changes value. /// /// The entry that changed. - private void Changed(PriorityBindingEntry entry) + public void Changed(PriorityBindingEntry entry) { if (entry.Index >= ActiveBindingIndex) { @@ -142,7 +142,7 @@ namespace Perspex /// Invoked when an entry in completes. /// /// The entry that completed. - private void Completed(PriorityBindingEntry entry) + public void Completed(PriorityBindingEntry entry) { Bindings.Remove(entry); From 3865198be4b92eeb07463948f6ed33442713fec1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Apr 2016 13:52:33 +0200 Subject: [PATCH 08/22] Use Logger.Error instead of .Log. --- src/Markup/Perspex.Markup/Data/ExpressionSubject.cs | 6 ++---- .../Perspex.Markup.Xaml.UnitTests.csproj | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs index 6196a69ed8..2a94e8ef53 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs @@ -110,8 +110,7 @@ namespace Perspex.Markup.Data { var error = converted as BindingError; - Logger.Log( - LogEventLevel.Error, + Logger.Error( LogArea.Binding, this, "Error binding to {Expression}: {Message}", @@ -130,8 +129,7 @@ namespace Perspex.Markup.Data } else { - Logger.Log( - LogEventLevel.Error, + Logger.Error( LogArea.Binding, this, "Could not convert FallbackValue {FallbackValue} to {Type}", diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index e52711ab46..e465378014 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -104,6 +104,7 @@ + From 6a5208621e5b58a343d86c7f8a2f50d3d681872a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 5 Apr 2016 22:59:12 +0200 Subject: [PATCH 09/22] Return BindingError from property plugins. --- .../Perspex.Markup/Data/ExpressionSubject.cs | 4 +- .../Data/Plugins/IPropertyAccessorPlugin.cs | 2 +- .../Plugins/InpcPropertyAccessorPlugin.cs | 13 ++----- .../Plugins/PerspexPropertyAccessorPlugin.cs | 14 +++---- .../Data/Plugins/PropertyError.cs | 39 +++++++++++++++++++ .../Data/PropertyAccessorNode.cs | 2 +- .../Perspex.Markup/DefaultValueConverter.cs | 2 +- .../Perspex.Markup/Perspex.Markup.csproj | 1 + .../Data/ExpressionObserverTests_Property.cs | 14 +++++-- 9 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs diff --git a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs index 2a94e8ef53..46df3ce490 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs @@ -153,7 +153,9 @@ namespace Perspex.Markup.Data private object ConvertValue(object value) { - var converted = Converter.Convert( + var converted = + value as BindingError ?? + Converter.Convert( value, _targetType, ConverterParameter, diff --git a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs index 7bbdb0ff68..8a431041d0 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs @@ -26,7 +26,7 @@ namespace Perspex.Markup.Data.Plugins /// A function to call when the property changes. /// /// An interface through which future interactions with the - /// property will be made, or null if the property was not found. + /// property will be made. /// IPropertyAccessor Start( WeakReference reference, diff --git a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs index ac38ee74f9..64421ddc57 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -38,7 +38,7 @@ namespace Perspex.Markup.Data.Plugins /// A function to call when the property changes. /// /// An interface through which future interactions with the - /// property will be made, or null if the property was not found. + /// property will be made. /// public IPropertyAccessor Start( WeakReference reference, @@ -58,14 +58,9 @@ namespace Perspex.Markup.Data.Plugins } else { - Logger.Error( - LogArea.Binding, - this, - "Could not find CLR property {Property} on {Source}", - propertyName, - instance); - - return null; + var message = $"Could not find CLR property '{propertyName}' on '{instance}'"; + var exception = new MissingMemberException(message); + return new PropertyError(new BindingError(exception)); } } diff --git a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs index 4d2f6c5cb0..bc16989a7a 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs @@ -33,7 +33,7 @@ namespace Perspex.Markup.Data.Plugins /// A function to call when the property changes. /// /// An interface through which future interactions with the - /// property will be made, or null if the property was not found. + /// property will be made. /// public IPropertyAccessor Start( WeakReference reference, @@ -52,14 +52,14 @@ namespace Perspex.Markup.Data.Plugins { return new Accessor(new WeakReference(o), p, changed); } + else if (instance != PerspexProperty.UnsetValue) + { + var message = $"Could not find PerspexProperty '{propertyName}' on '{instance}'"; + var exception = new MissingMemberException(message); + return new PropertyError(new BindingError(exception)); + } else { - Logger.Error( - LogArea.Binding, - this, - "Could not find PerspexProperty {Property} on {Source}", - propertyName, - instance); return null; } } diff --git a/src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs b/src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs new file mode 100644 index 0000000000..c147a315d5 --- /dev/null +++ b/src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs @@ -0,0 +1,39 @@ +using System; +using Perspex.Data; + +namespace Perspex.Markup.Data.Plugins +{ + /// + /// An that represents an error. + /// + public class PropertyError : IPropertyAccessor + { + private BindingError _error; + + /// + /// Initializes a new instance of the class. + /// + /// The error to report. + public PropertyError(BindingError error) + { + _error = error; + } + + /// + public Type PropertyType => null; + + /// + public object Value => _error; + + /// + public void Dispose() + { + } + + /// + public bool SetValue(object value, BindingPriority priority) + { + return false; + } + } +} diff --git a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs index 1aa703ff8f..14883e449a 100644 --- a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs @@ -48,7 +48,7 @@ namespace Perspex.Markup.Data { var instance = reference.Target; - if (instance != null) + if (instance != null && instance != PerspexProperty.UnsetValue) { var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference)); diff --git a/src/Markup/Perspex.Markup/DefaultValueConverter.cs b/src/Markup/Perspex.Markup/DefaultValueConverter.cs index 8a24b53612..0cc5f0df8e 100644 --- a/src/Markup/Perspex.Markup/DefaultValueConverter.cs +++ b/src/Markup/Perspex.Markup/DefaultValueConverter.cs @@ -43,7 +43,7 @@ namespace Perspex.Markup if (value != null) { - var message = $"Could not convert {value} to {targetType}"; + var message = $"Could not convert '{value}' to '{targetType}'"; return new BindingError(new InvalidCastException(message)); } diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index f5b61e2cea..5427d97346 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -56,6 +56,7 @@ + diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index 8586f89ab6..1a201dcd9c 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -7,6 +7,7 @@ using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.Reactive.Testing; +using Perspex.Data; using Perspex.Markup.Data; using Perspex.UnitTests; using Xunit; @@ -74,13 +75,17 @@ namespace Perspex.Markup.UnitTests.Data } [Fact] - public async void Should_Not_Have_Value_For_Broken_Chain() + public async void Should_Return_BindingError_For_Broken_Chain() { var data = new { Foo = new { Bar = 1 } }; var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var result = await target.Take(1); - Assert.Equal(PerspexProperty.UnsetValue, result); + Assert.IsType(result); + + var error = result as BindingError; + Assert.IsType(error.Exception); + Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Exception.Message); } [Fact] @@ -209,7 +214,10 @@ namespace Perspex.Markup.UnitTests.Data data.Next = breaking; data.Next = new Class2 { Bar = "baz" }; - Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue, "baz" }, result); + Assert.Equal(3, result.Count); + Assert.Equal("bar", result[0]); + Assert.IsType(result[1]); + Assert.Equal("baz", result[2]); sub.Dispose(); From df204ce01d708fc7c7331ba830dcc2a3133b636c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 5 Apr 2016 22:59:22 +0200 Subject: [PATCH 10/22] Fix TextBox template binding. --- src/Perspex.Themes.Default/TextBox.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Themes.Default/TextBox.xaml b/src/Perspex.Themes.Default/TextBox.xaml index a62c2f406a..badeb2a18d 100644 --- a/src/Perspex.Themes.Default/TextBox.xaml +++ b/src/Perspex.Themes.Default/TextBox.xaml @@ -40,7 +40,7 @@ CaretIndex="{TemplateBinding CaretIndex}" SelectionStart="{TemplateBinding SelectionStart}" SelectionEnd="{TemplateBinding SelectionEnd}" - Text="{TemplateBinding Text}" + Text="{TemplateBinding Text, Mode=TwoWay}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}"/> From 4365e9b9c0c395ab57f2ddedf99ce39914354cc2 Mon Sep 17 00:00:00 2001 From: donandren Date: Thu, 7 Apr 2016 17:46:09 +0300 Subject: [PATCH 11/22] canvas attached properties arrange improvement --- src/Perspex.Controls/Canvas.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Controls/Canvas.cs b/src/Perspex.Controls/Canvas.cs index 76f0d904cc..b7c7cee86d 100644 --- a/src/Perspex.Controls/Canvas.cs +++ b/src/Perspex.Controls/Canvas.cs @@ -226,7 +226,7 @@ namespace Perspex.Controls private static void AffectsCanvasArrangeInvalidate(PerspexPropertyChangedEventArgs e) { var control = e.Sender as IControl; - var canvas = control?.Parent as Canvas; + var canvas = control?.VisualParent as Canvas; canvas?.InvalidateArrange(); } } From 2acd1f5494b9c35d770b251732a4aa4a6e57adf4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Apr 2016 14:02:04 +0200 Subject: [PATCH 12/22] Close Popup with !StaysOpen on non-client click. --- src/Perspex.Controls/Primitives/Popup.cs | 15 +++++++++++ src/Perspex.Input/Raw/RawMouseEventArgs.cs | 1 + src/Windows/Perspex.Win32/WindowImpl.cs | 29 +++++++++++----------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/Perspex.Controls/Primitives/Popup.cs b/src/Perspex.Controls/Primitives/Popup.cs index 3b16b349c2..9fb9053688 100644 --- a/src/Perspex.Controls/Primitives/Popup.cs +++ b/src/Perspex.Controls/Primitives/Popup.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using Perspex.Input; +using Perspex.Input.Raw; using Perspex.Interactivity; using Perspex.LogicalTree; using Perspex.Metadata; @@ -53,6 +54,7 @@ namespace Perspex.Controls.Primitives private bool _isOpen; private PopupRoot _popupRoot; private TopLevel _topLevel; + private IDisposable _nonClientListener; /// /// Initializes static members of the class. @@ -181,6 +183,7 @@ namespace Perspex.Controls.Primitives { _topLevel.Deactivated += TopLevelDeactivated; _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); + _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick); } PopupRootCreated?.Invoke(this, EventArgs.Empty); @@ -201,6 +204,8 @@ namespace Perspex.Controls.Primitives { _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside); _topLevel.Deactivated -= TopLevelDeactivated; + _nonClientListener?.Dispose(); + _nonClientListener = null; } _popupRoot.Hide(); @@ -300,6 +305,16 @@ namespace Perspex.Controls.Primitives } } + private void ListenForNonClientClick(RawInputEventArgs e) + { + var mouse = e as RawMouseEventArgs; + + if (!StaysOpen && mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) + { + Close(); + } + } + private void PointerPressedOutside(object sender, PointerPressedEventArgs e) { if (!StaysOpen) diff --git a/src/Perspex.Input/Raw/RawMouseEventArgs.cs b/src/Perspex.Input/Raw/RawMouseEventArgs.cs index 80cd4716de..15ba35f8bf 100644 --- a/src/Perspex.Input/Raw/RawMouseEventArgs.cs +++ b/src/Perspex.Input/Raw/RawMouseEventArgs.cs @@ -16,6 +16,7 @@ namespace Perspex.Input.Raw MiddleButtonUp, Move, Wheel, + NonClientLeftButtonDown, } /// diff --git a/src/Windows/Perspex.Win32/WindowImpl.cs b/src/Windows/Perspex.Win32/WindowImpl.cs index f07839c09d..27f90d1ca9 100644 --- a/src/Windows/Perspex.Win32/WindowImpl.cs +++ b/src/Windows/Perspex.Win32/WindowImpl.cs @@ -436,20 +436,6 @@ namespace Perspex.Win32 break; - ////case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: - ////case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: - ////case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: - //// e = new RawMouseEventArgs( - //// WindowsMouseDevice.Instance, - //// timestamp, - //// _owner, - //// msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN - //// ? RawMouseEventType.LeftButtonDown - //// : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN - //// ? RawMouseEventType.RightButtonDown - //// : RawMouseEventType.MiddleButtonDown, - //// new Point(0, 0), GetMouseModifiers(wParam)); - //// break; case UnmanagedMethods.WindowsMessage.WM_LBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: @@ -522,6 +508,21 @@ namespace Perspex.Win32 new Point(), WindowsKeyboardDevice.Instance.Modifiers); break; + case UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN: + case UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN: + case UnmanagedMethods.WindowsMessage.WM_NCMBUTTONDOWN: + e = new RawMouseEventArgs( + WindowsMouseDevice.Instance, + timestamp, + _owner, + msg == (int)UnmanagedMethods.WindowsMessage.WM_NCLBUTTONDOWN + ? RawMouseEventType.NonClientLeftButtonDown + : msg == (int)UnmanagedMethods.WindowsMessage.WM_NCRBUTTONDOWN + ? RawMouseEventType.RightButtonDown + : RawMouseEventType.MiddleButtonDown, + new Point(0, 0), GetMouseModifiers(wParam)); + break; + case UnmanagedMethods.WindowsMessage.WM_PAINT: if (Paint != null) { From d4de5bb52061315d828ae31fb23f6df4fb7b0ae6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Apr 2016 14:06:54 +0200 Subject: [PATCH 13/22] Close menu on non-client clicks. --- src/Perspex.Controls/Menu.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Perspex.Controls/Menu.cs b/src/Perspex.Controls/Menu.cs index 099f4e2345..1d61f8add1 100644 --- a/src/Perspex.Controls/Menu.cs +++ b/src/Perspex.Controls/Menu.cs @@ -7,6 +7,7 @@ using System.Reactive.Disposables; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; +using Perspex.Input.Raw; using Perspex.Interactivity; using Perspex.LogicalTree; using Perspex.Rendering; @@ -112,7 +113,8 @@ namespace Perspex.Controls _subscription = new CompositeDisposable( pointerPress, - Disposable.Create(() => topLevel.Deactivated -= Deactivated)); + Disposable.Create(() => topLevel.Deactivated -= Deactivated), + InputManager.Instance.Process.Subscribe(ListenForNonClientClick)); var inputRoot = e.Root as IInputRoot; @@ -195,6 +197,20 @@ namespace Perspex.Controls Close(); } + /// + /// Listens for non-client clicks and closes the menu when one is detected. + /// + /// The raw event. + private void ListenForNonClientClick(RawInputEventArgs e) + { + var mouse = e as RawMouseEventArgs; + + if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown) + { + Close(); + } + } + /// /// Called when a submenu is clicked somewhere in the menu. /// From 23a3f81420d41fb656f1ff39029ba59d86322c63 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Apr 2016 15:19:08 +0200 Subject: [PATCH 14/22] Coerce CarouselPresenter.SelectedIndex. --- .../Presenters/CarouselPresenter.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Perspex.Controls/Presenters/CarouselPresenter.cs b/src/Perspex.Controls/Presenters/CarouselPresenter.cs index f5e16851e2..8d25729e74 100644 --- a/src/Perspex.Controls/Presenters/CarouselPresenter.cs +++ b/src/Perspex.Controls/Presenters/CarouselPresenter.cs @@ -11,6 +11,7 @@ using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Controls.Utils; +using Perspex.Data; namespace Perspex.Controls.Presenters { @@ -127,8 +128,22 @@ namespace Perspex.Controls.Presenters /// public int SelectedIndex { - get { return _selectedIndex; } - set { SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); } + get + { + return _selectedIndex; + } + + set + { + var old = SelectedIndex; + var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; + + if (old != effective) + { + _selectedIndex = effective; + RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); + } + } } /// From a06e3ffd73bda863d3f53ffa55c01037fb066890 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 9 Apr 2016 16:01:11 +0200 Subject: [PATCH 15/22] Fix ContentControlMixin. Correctly track the current ContentControl child. Fixes a bug in MenuItem where the first child MenuItem was not a logical child after the template got applied. --- src/Perspex.Controls/Mixins/ContentControlMixin.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Perspex.Controls/Mixins/ContentControlMixin.cs b/src/Perspex.Controls/Mixins/ContentControlMixin.cs index a381c7c773..a29b471241 100644 --- a/src/Perspex.Controls/Mixins/ContentControlMixin.cs +++ b/src/Perspex.Controls/Mixins/ContentControlMixin.cs @@ -74,7 +74,7 @@ namespace Perspex.Controls.Mixins UpdateLogicalChild( sender, logicalChildren, - logicalChildren.FirstOrDefault(), + null, presenter.GetValue(ContentPresenter.ChildProperty)); subscriptions.Value.Add(sender, subscription); @@ -143,7 +143,7 @@ namespace Perspex.Controls.Mixins child = newValue as IControl; - if (child != null) + if (child != null && !logicalChildren.Contains(child)) { child.SetValue(Control.TemplatedParentProperty, control.TemplatedParent); logicalChildren.Add(child); From c9df1415bb1196bccf6123d66540f28c46f10bf5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Apr 2016 14:51:38 +0200 Subject: [PATCH 16/22] Tried to reproduce #499. But can't... --- .../TestViewModel.cs | 44 +++++++++++++++++++ .../ViewModelMock.cs | 41 ----------------- .../Xaml/DataTemplateTests.cs | 40 +++++++++++++++++ tests/Perspex.UnitTests/NotifyingBase.cs | 3 +- 4 files changed, 86 insertions(+), 42 deletions(-) create mode 100644 tests/Perspex.Markup.Xaml.UnitTests/TestViewModel.cs delete mode 100644 tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs diff --git a/tests/Perspex.Markup.Xaml.UnitTests/TestViewModel.cs b/tests/Perspex.Markup.Xaml.UnitTests/TestViewModel.cs new file mode 100644 index 0000000000..5f8eda51b0 --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/TestViewModel.cs @@ -0,0 +1,44 @@ +// 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.UnitTests; + +namespace Perspex.Markup.Xaml.UnitTests +{ + public class TestViewModel : NotifyingBase + { + private string _string; + private int _integer; + private TestViewModel _child; + + public int Integer + { + get { return _integer; } + set + { + _integer = value; + RaisePropertyChanged(); + } + } + + public string String + { + get { return _string; } + set + { + _string = value; + RaisePropertyChanged(); + } + } + + public TestViewModel Child + { + get { return _child; } + set + { + _child = value; + RaisePropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs b/tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs deleted file mode 100644 index 723bf53311..0000000000 --- a/tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs +++ /dev/null @@ -1,41 +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.ComponentModel; -using System.Runtime.CompilerServices; - -namespace Perspex.Markup.Xaml.UnitTests -{ - internal class ViewModelMock : INotifyPropertyChanged - { - private string _str; - private int _intProp; - - public int IntProp - { - get { return _intProp; } - set - { - _intProp = value; - OnPropertyChanged(); - } - } - - public string StrProp - { - get { return _str; } - set - { - _str = value; - OnPropertyChanged(); - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - private void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } -} \ No newline at end of file diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index bf754914dc..62fe7617f7 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -36,5 +36,45 @@ namespace Perspex.Markup.Xaml.UnitTests.Xaml Assert.IsType(target.Presenter.Child); } } + + [Fact] + public void Can_Set_DataContext_In_DataTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new PerspexXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("target"); + + var viewModel = new TestViewModel + { + String = "Root", + Child = new TestViewModel + { + String = "Child", + }, + }; + + window.DataContext = viewModel; + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + var canvas = (Canvas)target.Presenter.Child; + Assert.Same(viewModel, target.DataContext); + Assert.Same(viewModel.Child, canvas.DataContext); + } + } } } diff --git a/tests/Perspex.UnitTests/NotifyingBase.cs b/tests/Perspex.UnitTests/NotifyingBase.cs index 0d2e1c968c..f077e1a62f 100644 --- a/tests/Perspex.UnitTests/NotifyingBase.cs +++ b/tests/Perspex.UnitTests/NotifyingBase.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Linq; +using System.Runtime.CompilerServices; namespace Perspex.UnitTests { @@ -34,7 +35,7 @@ namespace Perspex.UnitTests private set; } - public void RaisePropertyChanged(string propertyName) + public void RaisePropertyChanged([CallerMemberName] string propertyName = null) { _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } From 37273c2bce11d990a47bcc3c06ded4fef3a92507 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Apr 2016 15:23:36 +0200 Subject: [PATCH 17/22] Forgot to save csproj. Again. --- .../Perspex.Markup.Xaml.UnitTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index e465378014..243c0c53f4 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -111,7 +111,7 @@ - + From f6853de65c6f9ffa8b008024cc75079f9e9ab082 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Apr 2016 15:23:46 +0200 Subject: [PATCH 18/22] Managed to make test fail. --- .../Xaml/DataTemplateTests.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 62fe7617f7..88efe3f3b1 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -50,7 +50,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Xaml - + "; var loader = new PerspexXamlLoader(); var window = (Window)loader.Load(xaml); @@ -62,6 +62,10 @@ namespace Perspex.Markup.Xaml.UnitTests.Xaml Child = new TestViewModel { String = "Child", + Child = new TestViewModel + { + String = "Grandchild", + } }, }; @@ -73,7 +77,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Xaml var canvas = (Canvas)target.Presenter.Child; Assert.Same(viewModel, target.DataContext); - Assert.Same(viewModel.Child, canvas.DataContext); + Assert.Same(viewModel.Child.Child, canvas.DataContext); } } } From 4241fd8331c6122950b444260dc3dd2aae1e0fd2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 10 Apr 2016 16:25:48 +0200 Subject: [PATCH 19/22] Binding ContentPresenter's DataContext. To the owner control's Content property. In WPF this is done automatically, but from looking on the internet it seems that causes some confusion so do it explicitly. Fixes #499 . --- src/Perspex.Themes.Default/Button.xaml | 1 + src/Perspex.Themes.Default/CheckBox.xaml | 1 + src/Perspex.Themes.Default/ContentControl.xaml | 3 ++- src/Perspex.Themes.Default/DropDown.xaml | 1 + src/Perspex.Themes.Default/DropDownItem.xaml | 1 + src/Perspex.Themes.Default/Expander.xaml | 4 ++++ src/Perspex.Themes.Default/LayoutTransformControl.xaml | 1 + src/Perspex.Themes.Default/ListBoxItem.xaml | 1 + src/Perspex.Themes.Default/MenuItem.xaml | 3 +++ src/Perspex.Themes.Default/RadioButton.xaml | 1 + src/Perspex.Themes.Default/TabStripItem.xaml | 1 + src/Perspex.Themes.Default/ToggleButton.xaml | 1 + src/Perspex.Themes.Default/ToolTip.xaml | 1 + src/Perspex.Themes.Default/TreeViewItem.xaml | 1 + src/Perspex.Themes.Default/Window.xaml | 5 ++++- 15 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Perspex.Themes.Default/Button.xaml b/src/Perspex.Themes.Default/Button.xaml index 114849b131..93c372adc6 100644 --- a/src/Perspex.Themes.Default/Button.xaml +++ b/src/Perspex.Themes.Default/Button.xaml @@ -14,6 +14,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" diff --git a/src/Perspex.Themes.Default/CheckBox.xaml b/src/Perspex.Themes.Default/CheckBox.xaml index d05e8b5cf6..7452964213 100644 --- a/src/Perspex.Themes.Default/CheckBox.xaml +++ b/src/Perspex.Themes.Default/CheckBox.xaml @@ -22,6 +22,7 @@ diff --git a/src/Perspex.Themes.Default/ContentControl.xaml b/src/Perspex.Themes.Default/ContentControl.xaml index 90e34c7d3c..cce99a45e6 100644 --- a/src/Perspex.Themes.Default/ContentControl.xaml +++ b/src/Perspex.Themes.Default/ContentControl.xaml @@ -5,7 +5,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" + Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Perspex.Themes.Default/DropDown.xaml b/src/Perspex.Themes.Default/DropDown.xaml index 37dd47d49a..683d968520 100644 --- a/src/Perspex.Themes.Default/DropDown.xaml +++ b/src/Perspex.Themes.Default/DropDown.xaml @@ -11,6 +11,7 @@ BorderThickness="{TemplateBinding BorderThickness}"> diff --git a/src/Perspex.Themes.Default/DropDownItem.xaml b/src/Perspex.Themes.Default/DropDownItem.xaml index 4900c961e4..69742258a6 100644 --- a/src/Perspex.Themes.Default/DropDownItem.xaml +++ b/src/Perspex.Themes.Default/DropDownItem.xaml @@ -10,6 +10,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Perspex.Themes.Default/Expander.xaml b/src/Perspex.Themes.Default/Expander.xaml index 158f6514fd..87728c555b 100644 --- a/src/Perspex.Themes.Default/Expander.xaml +++ b/src/Perspex.Themes.Default/Expander.xaml @@ -17,6 +17,7 @@ Grid.Row="1" IsVisible="{TemplateBinding IsExpanded}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" /> @@ -34,6 +35,7 @@ Grid.Row="0" IsVisible="{TemplateBinding IsExpanded}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" /> @@ -51,6 +53,7 @@ Grid.Column="1" IsVisible="{TemplateBinding IsExpanded}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" /> @@ -68,6 +71,7 @@ Grid.Column="0" IsVisible="{TemplateBinding IsExpanded}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" /> diff --git a/src/Perspex.Themes.Default/LayoutTransformControl.xaml b/src/Perspex.Themes.Default/LayoutTransformControl.xaml index e393eedf83..3d59fd4482 100644 --- a/src/Perspex.Themes.Default/LayoutTransformControl.xaml +++ b/src/Perspex.Themes.Default/LayoutTransformControl.xaml @@ -6,6 +6,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Perspex.Themes.Default/ListBoxItem.xaml b/src/Perspex.Themes.Default/ListBoxItem.xaml index 784731d57b..7c22cd4b47 100644 --- a/src/Perspex.Themes.Default/ListBoxItem.xaml +++ b/src/Perspex.Themes.Default/ListBoxItem.xaml @@ -7,6 +7,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Perspex.Themes.Default/MenuItem.xaml b/src/Perspex.Themes.Default/MenuItem.xaml index 2011bb041b..7a50703a2f 100644 --- a/src/Perspex.Themes.Default/MenuItem.xaml +++ b/src/Perspex.Themes.Default/MenuItem.xaml @@ -13,6 +13,7 @@ @@ -81,6 +83,7 @@ diff --git a/src/Perspex.Themes.Default/RadioButton.xaml b/src/Perspex.Themes.Default/RadioButton.xaml index 8fc0b14179..6e5c958331 100644 --- a/src/Perspex.Themes.Default/RadioButton.xaml +++ b/src/Perspex.Themes.Default/RadioButton.xaml @@ -21,6 +21,7 @@ VerticalAlignment="Center"/> diff --git a/src/Perspex.Themes.Default/TabStripItem.xaml b/src/Perspex.Themes.Default/TabStripItem.xaml index e32de4613b..1ac77d6609 100644 --- a/src/Perspex.Themes.Default/TabStripItem.xaml +++ b/src/Perspex.Themes.Default/TabStripItem.xaml @@ -9,6 +9,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Perspex.Themes.Default/ToggleButton.xaml b/src/Perspex.Themes.Default/ToggleButton.xaml index 03354c9b27..5f26051058 100644 --- a/src/Perspex.Themes.Default/ToggleButton.xaml +++ b/src/Perspex.Themes.Default/ToggleButton.xaml @@ -14,6 +14,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" diff --git a/src/Perspex.Themes.Default/ToolTip.xaml b/src/Perspex.Themes.Default/ToolTip.xaml index 73bf2215ee..19af7c63bc 100644 --- a/src/Perspex.Themes.Default/ToolTip.xaml +++ b/src/Perspex.Themes.Default/ToolTip.xaml @@ -10,6 +10,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Perspex.Themes.Default/TreeViewItem.xaml b/src/Perspex.Themes.Default/TreeViewItem.xaml index 5e673d767f..fcb39fe621 100644 --- a/src/Perspex.Themes.Default/TreeViewItem.xaml +++ b/src/Perspex.Themes.Default/TreeViewItem.xaml @@ -13,6 +13,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Header}" + DataContext="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" TemplatedControl.IsTemplateFocusTarget="True" Grid.Column="1"/> diff --git a/src/Perspex.Themes.Default/Window.xaml b/src/Perspex.Themes.Default/Window.xaml index 4316626e58..3dca88b9f4 100644 --- a/src/Perspex.Themes.Default/Window.xaml +++ b/src/Perspex.Themes.Default/Window.xaml @@ -6,7 +6,10 @@ - + From 2feca0b691e122a9d1965fa92caa6e39fd18596d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 Apr 2016 08:14:32 +0200 Subject: [PATCH 20/22] Bind to correct property. --- src/Perspex.Themes.Default/DropDown.xaml | 2 +- src/Perspex.Themes.Default/MenuItem.xaml | 6 +++--- src/Perspex.Themes.Default/TreeViewItem.xaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Perspex.Themes.Default/DropDown.xaml b/src/Perspex.Themes.Default/DropDown.xaml index 683d968520..a0340099a5 100644 --- a/src/Perspex.Themes.Default/DropDown.xaml +++ b/src/Perspex.Themes.Default/DropDown.xaml @@ -11,7 +11,7 @@ BorderThickness="{TemplateBinding BorderThickness}"> diff --git a/src/Perspex.Themes.Default/MenuItem.xaml b/src/Perspex.Themes.Default/MenuItem.xaml index 7a50703a2f..5b2fc5ec9e 100644 --- a/src/Perspex.Themes.Default/MenuItem.xaml +++ b/src/Perspex.Themes.Default/MenuItem.xaml @@ -13,7 +13,7 @@ @@ -83,7 +83,7 @@ diff --git a/src/Perspex.Themes.Default/TreeViewItem.xaml b/src/Perspex.Themes.Default/TreeViewItem.xaml index fcb39fe621..072b5a0840 100644 --- a/src/Perspex.Themes.Default/TreeViewItem.xaml +++ b/src/Perspex.Themes.Default/TreeViewItem.xaml @@ -13,7 +13,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Header}" - DataContext="{TemplateBinding Content}" + DataContext="{TemplateBinding Header}" Padding="{TemplateBinding Padding}" TemplatedControl.IsTemplateFocusTarget="True" Grid.Column="1"/> From 43be59a7f936c6a399f3c3ae623260ae76c9a5e1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 Apr 2016 13:49:12 +0200 Subject: [PATCH 21/22] Added docs for DirectProperty.AddOwner. --- docs/spec/defining-properties.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/spec/defining-properties.md b/docs/spec/defining-properties.md index 532a2ede71..898efc8cef 100644 --- a/docs/spec/defining-properties.md +++ b/docs/spec/defining-properties.md @@ -41,7 +41,7 @@ the parent control. on which the property is being set and the value and returns the coerced value or throws an exception for an invalid value. -## Using a StyledProperty from Another Class +## Using a StyledProperty on Another Class Sometimes the property you want to add to your control already exists on another control, `Background` being a good example. To register a property defined on @@ -158,6 +158,27 @@ They don't support the following: - Overriding default values. - Inherited values +## Using a DirectProperty on Another Class + +In the same way that you can call `AddOwner` on a styled property, you can also +add an owner to a direct property. Because direct properties reference fields +on the control, you must also add a field for the property: + +```c# + public static readonly DirectProperty ItemsProperty = + ItemsControl.ItemsProperty.AddOwner( + o => o.Items, + (o, v) => o.Items = v); + + private IEnumerable _items = new PerspexList(); + + public IEnumerable Items + { + get { return _items; } + set { SetAndRaise(ItemsProperty, ref _items, value); } + } +``` + ## When to use a Direct vs a Styled Property Direct properties have advantages and disadvantages: From 07f854eb9a2bb110052236ac2362b1013ad13932 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 Apr 2016 17:21:26 +0200 Subject: [PATCH 22/22] Fixed Shape.AffectsGeometry. Make sure the object on which the property changed occurred is the correct type. Previously, e.g. the `AffectsGeometry(BoundsProperty, StrokeThicknessProperty)` call in `Rectangle` caused *all* shapes to be invalidated when `Bounds` changed, where it should only affect `Rectangle. --- src/Perspex.Controls/Shapes/Ellipse.cs | 2 +- src/Perspex.Controls/Shapes/Line.cs | 2 +- src/Perspex.Controls/Shapes/Path.cs | 2 +- src/Perspex.Controls/Shapes/Polygon.cs | 2 +- src/Perspex.Controls/Shapes/Polyline.cs | 2 +- src/Perspex.Controls/Shapes/Rectangle.cs | 2 +- src/Perspex.Controls/Shapes/Shape.cs | 15 +++++++++++++-- 7 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Perspex.Controls/Shapes/Ellipse.cs b/src/Perspex.Controls/Shapes/Ellipse.cs index 4610d07e3a..4c4461e9ec 100644 --- a/src/Perspex.Controls/Shapes/Ellipse.cs +++ b/src/Perspex.Controls/Shapes/Ellipse.cs @@ -9,7 +9,7 @@ namespace Perspex.Controls.Shapes { static Ellipse() { - AffectsGeometry(BoundsProperty, StrokeThicknessProperty); + AffectsGeometry(BoundsProperty, StrokeThicknessProperty); } protected override Geometry CreateDefiningGeometry() diff --git a/src/Perspex.Controls/Shapes/Line.cs b/src/Perspex.Controls/Shapes/Line.cs index ad2670a380..72d4246194 100644 --- a/src/Perspex.Controls/Shapes/Line.cs +++ b/src/Perspex.Controls/Shapes/Line.cs @@ -16,7 +16,7 @@ namespace Perspex.Controls.Shapes static Line() { StrokeThicknessProperty.OverrideDefaultValue(1); - AffectsGeometry(StartPointProperty, EndPointProperty); + AffectsGeometry(StartPointProperty, EndPointProperty); } public Point StartPoint diff --git a/src/Perspex.Controls/Shapes/Path.cs b/src/Perspex.Controls/Shapes/Path.cs index 91c2934c90..37a10d86c0 100644 --- a/src/Perspex.Controls/Shapes/Path.cs +++ b/src/Perspex.Controls/Shapes/Path.cs @@ -13,7 +13,7 @@ namespace Perspex.Controls.Shapes static Path() { - AffectsGeometry(DataProperty); + AffectsGeometry(DataProperty); } public Geometry Data diff --git a/src/Perspex.Controls/Shapes/Polygon.cs b/src/Perspex.Controls/Shapes/Polygon.cs index 8fd11c58c9..c25e87c7a0 100644 --- a/src/Perspex.Controls/Shapes/Polygon.cs +++ b/src/Perspex.Controls/Shapes/Polygon.cs @@ -13,7 +13,7 @@ namespace Perspex.Controls.Shapes static Polygon() { - AffectsGeometry(PointsProperty); + AffectsGeometry(PointsProperty); } public IList Points diff --git a/src/Perspex.Controls/Shapes/Polyline.cs b/src/Perspex.Controls/Shapes/Polyline.cs index 9b2c3eaa12..eec81cadea 100644 --- a/src/Perspex.Controls/Shapes/Polyline.cs +++ b/src/Perspex.Controls/Shapes/Polyline.cs @@ -14,7 +14,7 @@ namespace Perspex.Controls.Shapes static Polyline() { StrokeThicknessProperty.OverrideDefaultValue(1); - AffectsGeometry(PointsProperty); + AffectsGeometry(PointsProperty); } public IList Points diff --git a/src/Perspex.Controls/Shapes/Rectangle.cs b/src/Perspex.Controls/Shapes/Rectangle.cs index d9cf2d035a..ceee585fda 100644 --- a/src/Perspex.Controls/Shapes/Rectangle.cs +++ b/src/Perspex.Controls/Shapes/Rectangle.cs @@ -9,7 +9,7 @@ namespace Perspex.Controls.Shapes { static Rectangle() { - AffectsGeometry(BoundsProperty, StrokeThicknessProperty); + AffectsGeometry(BoundsProperty, StrokeThicknessProperty); } protected override Geometry CreateDefiningGeometry() diff --git a/src/Perspex.Controls/Shapes/Shape.cs b/src/Perspex.Controls/Shapes/Shape.cs index e7a84f2d41..29c48eba7d 100644 --- a/src/Perspex.Controls/Shapes/Shape.cs +++ b/src/Perspex.Controls/Shapes/Shape.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.Reflection; using Perspex.Collections; using Perspex.Controls; using Perspex.Media; @@ -123,11 +124,21 @@ namespace Perspex.Controls.Shapes /// After a call to this method in a control's static constructor, any change to the /// property will cause to be called on the element. /// - protected static void AffectsGeometry(params PerspexProperty[] properties) + protected static void AffectsGeometry(params PerspexProperty[] properties) + where TShape : Shape { foreach (var property in properties) { - property.Changed.Subscribe(AffectsGeometryInvalidate); + property.Changed.Subscribe(e => + { + var senderType = e.Sender.GetType().GetTypeInfo(); + var affectedType = typeof(TShape).GetTypeInfo(); + + if (affectedType.IsAssignableFrom(senderType)) + { + AffectsGeometryInvalidate(e); + } + }); } }