diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index c32a08cb5d..1e996b2b2a 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -25,6 +25,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions Converter = Converter, ConverterParameter = ConverterParameter, ElementName = ElementName, + FallbackValue = FallbackValue, Mode = Mode, Path = Path, Priority = Priority, diff --git a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs index 95b8c7ac4d..46df3ce490 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Reactive.Linq; using System.Reactive.Subjects; using Perspex.Data; +using Perspex.Logging; using Perspex.Utilities; namespace Perspex.Markup.Data @@ -103,9 +104,44 @@ namespace Perspex.Markup.Data if (converted == PerspexProperty.UnsetValue) { converted = TypeUtilities.Default(type); + _inner.SetValue(converted, _priority); + } + else if (converted is BindingError) + { + var error = converted as BindingError; + + Logger.Error( + LogArea.Binding, + this, + "Error binding to {Expression}: {Message}", + _inner.Expression, + error.Exception.Message); + + if (_fallbackValue != null) + { + if (TypeUtilities.TryConvert( + type, + _fallbackValue, + CultureInfo.InvariantCulture, + out converted)) + { + _inner.SetValue(converted, _priority); + } + else + { + Logger.Error( + LogArea.Binding, + this, + "Could not convert FallbackValue {FallbackValue} to {Type}", + _fallbackValue, + type); + } + } + } + else + { + _inner.SetValue(converted, _priority); } - - _inner.SetValue(converted, _priority); } } @@ -117,15 +153,37 @@ namespace Perspex.Markup.Data private object ConvertValue(object value) { - var converted = Converter.Convert( + var converted = + value as BindingError ?? + Converter.Convert( value, _targetType, ConverterParameter, CultureInfo.CurrentUICulture); - if (converted == PerspexProperty.UnsetValue && _fallbackValue != null) + if (_fallbackValue != null && + (converted == PerspexProperty.UnsetValue || + converted is BindingError)) { - converted = _fallbackValue; + var error = converted as BindingError; + + if (TypeUtilities.TryConvert( + _targetType, + _fallbackValue, + CultureInfo.InvariantCulture, + out converted)) + { + if (error != null) + { + converted = new BindingError(error.Exception, converted); + } + } + else + { + converted = new BindingError( + new InvalidCastException( + $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'")); + } } return converted; 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 9f535081a7..0cc5f0df8e 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/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/src/Perspex.Base/Data/BindingError.cs b/src/Perspex.Base/Data/BindingError.cs new file mode 100644 index 0000000000..b9330bd278 --- /dev/null +++ b/src/Perspex.Base/Data/BindingError.cs @@ -0,0 +1,59 @@ +// 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 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 + { + /// + /// Initializes a new instance of the class. + /// + /// An exception describing the binding error. + public BindingError(Exception exception) + { + 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/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/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..df240ee3b8 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -43,6 +43,7 @@ Properties\SharedAssemblyInfo.cs + @@ -54,6 +55,7 @@ + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 32f8eedc91..0f28f598d8 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 @@ -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); @@ -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() { @@ -587,6 +615,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 . /// @@ -604,35 +653,42 @@ namespace Perspex PriorityValue result = new PriorityValue( this, - property.Name, + property, property.PropertyType, validate2); - result.Changed.Subscribe(x => + 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 { - object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ? - GetDefaultValue(property) : - x.Item1; - object newValue = (x.Item2 == PerspexProperty.UnsetValue) ? - GetDefaultValue(property) : - x.Item2; - - if (!Equals(oldValue, newValue)) + if (error.UseFallbackValue) { - 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); + SetValue(property, error.FallbackValue); } - }); - return result; + Logger.Error( + LogArea.Binding, + this, + "Error binding to {Target}.{Property}: {Message}", + this, + property, + error.Exception.Message); + } } /// 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/PriorityBindingEntry.cs b/src/Perspex.Base/PriorityBindingEntry.cs index a17080f0f4..be6c451a71 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 { @@ -10,19 +11,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 +62,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 +79,7 @@ namespace Perspex Description = ((IDescription)binding).Description; } - _subscription = binding.Subscribe( - value => - { - Value = value; - changed(this); - }, - () => completed(this)); + _subscription = binding.Subscribe(ValueChanged, Completed); } /// @@ -101,5 +89,26 @@ namespace Perspex { _subscription?.Dispose(); } + + private void ValueChanged(object value) + { + var bindingError = value as BindingError; + + if (bindingError != null) + { + _owner.Error(this, bindingError); + } + + if (bindingError == null || bindingError.UseFallbackValue) + { + Value = bindingError == null ? value : bindingError.FallbackValue; + _owner.Changed(this); + } + } + + private void Completed() + { + _owner.Completed(this); + } } } diff --git a/src/Perspex.Base/PriorityLevel.cs b/src/Perspex.Base/PriorityLevel.cs index 3c47199525..a3167f1aa3 100644 --- a/src/Perspex.Base/PriorityLevel.cs +++ b/src/Perspex.Base/PriorityLevel.cs @@ -4,25 +4,11 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; +using Perspex.Data; +using Perspex.Logging; 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 . /// @@ -47,38 +33,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; - private readonly LevelPrecedenceMode _mode; - /// /// 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, - LevelPrecedenceMode mode, - Action changed) + PriorityValue owner, + int priority) { - Contract.Requires(changed != null); + Contract.Requires(owner != null); - _mode = mode; - _changed = changed; + _owner = owner; Priority = priority; Value = _directValue = PerspexProperty.UnsetValue; ActiveBindingIndex = -1; @@ -103,7 +73,7 @@ namespace Perspex set { Value = _directValue = value; - _changed(this); + _owner.LevelValueChanged(this); } } @@ -132,10 +102,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(() => { @@ -153,15 +123,15 @@ namespace Perspex /// Invoked when an entry in changes value. /// /// The entry that changed. - private void Changed(PriorityBindingEntry entry) + public void Changed(PriorityBindingEntry entry) { - if (_mode == LevelPrecedenceMode.Latest || entry.Index >= ActiveBindingIndex) + if (entry.Index >= ActiveBindingIndex) { if (entry.Value != PerspexProperty.UnsetValue) { Value = entry.Value; ActiveBindingIndex = entry.Index; - _changed(this); + _owner.LevelValueChanged(this); } else { @@ -174,7 +144,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); @@ -184,6 +154,16 @@ namespace Perspex } } + /// + /// Invoked when an entry in encounters a recoverable error. + /// + /// The entry that completed. + /// The error. + public void Error(PriorityBindingEntry entry, BindingError error) + { + _owner.LevelError(this, error); + } + /// /// Activates the first binding that has a value. /// @@ -195,14 +175,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 a8b72a376d..7a81466d17 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Subjects; using System.Text; +using Perspex.Data; using Perspex.Logging; using Perspex.Utilities; @@ -20,61 +20,33 @@ 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; - - /// - /// The value type. - /// + private readonly IPriorityValueOwner _owner; private readonly Type _valueType; - - /// - /// The currently registered bindings organised by priority. - /// private readonly Dictionary _levels = new Dictionary(); - - /// - /// The changed observable. - /// - private readonly Subject> _changed = new Subject>(); - - /// - /// The current value. - /// private object _value; - - /// - /// The function used to validate the value, if any. - /// private readonly Func _validate; /// /// 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 +54,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. @@ -181,6 +150,51 @@ 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); + } + } + } + + /// + /// Called when a priority level encounters an error. + /// + /// The priority level of the changed entry. + /// The binding error. + public void LevelError(PriorityLevel level, BindingError error) + { + Logger.Log( + LogEventLevel.Error, + LogArea.Binding, + _owner, + "Error binding to {Target}.{Property}: {Message}", + _owner, + Property, + error.Exception.Message); + } + /// /// Causes a revalidation of the value. /// @@ -209,8 +223,7 @@ namespace Perspex if (!_levels.TryGetValue(priority, out result)) { - var mode = (LevelPrecedenceMode)(priority % 2); - result = new PriorityLevel(priority, mode, ValueChanged); + result = new PriorityLevel(this, priority); _levels.Add(priority, result); } @@ -237,47 +250,19 @@ namespace Perspex ValuePriority = priority; _value = castValue; - _changed.OnNext(Tuple.Create(old, _value)); + _owner?.Changed(this, old, _value); } else { Logger.Error( - LogArea.Property, + LogArea.Binding, _owner, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", - _name, + Property.Name, _valueType, value, 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); - } - } - } } } 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/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)); } 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}"/> diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index 879022611c..38b2255d79 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 @@ -61,10 +65,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 +124,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..fb720a30c0 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,61 @@ 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() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + var expectedMessageTemplate = "Error binding to {Target}.{Property}: {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..09f42ee045 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,63 @@ 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() + { + var target = new Class1(); + var source = new Subject(); + var called = false; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Error && + area == LogArea.Binding && + mt == "Error binding to {Target}.{Property}: {Message}" && + pv.Length == 3 && + pv[0] is Class1 && + object.ReferenceEquals(pv[1], Class1.FooProperty) && + (string)pv[2] == "Binding Error Message") + { + called = true; + } + }; + + using (TestLogSink.Start(checkLogMessage)) + { + target.Bind(Class1.FooProperty, source); + source.OnNext("baz"); + source.OnNext(new BindingError(new InvalidOperationException("Binding Error Message"))); + } + + Assert.True(called); + } + private class Class1 : PerspexObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Perspex.Base.UnitTests/PriorityValueTests.cs b/tests/Perspex.Base.UnitTests/PriorityValueTests.cs index 5894ea12c3..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); @@ -67,25 +74,9 @@ 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 target = new PriorityValue(null, TestProperty, typeof(string)); var nonActive = new BehaviorSubject("na"); var source = new BehaviorSubject("initial"); @@ -101,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); @@ -117,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); @@ -129,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); @@ -142,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); @@ -155,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); @@ -171,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); @@ -210,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); @@ -223,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); @@ -237,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); @@ -251,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); @@ -265,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); @@ -276,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); @@ -290,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 @@  + 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(); diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs index cddc71f88a..0079aa7d57 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Globalization; using System.Reactive.Linq; using Moq; +using Perspex.Data; using Perspex.Markup.Data; using Xunit; @@ -47,13 +48,13 @@ namespace Perspex.Markup.UnitTests.Data } [Fact] - public async void Should_Convert_Get_Invalid_Double_String_To_UnsetValue() + public async void Getting_Invalid_Double_String_Should_Return_BindingError() { var data = new Class1 { StringValue = "foo" }; var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double)); var result = await target.Take(1); - Assert.Equal(PerspexProperty.UnsetValue, result); + Assert.IsType(result); } [Fact] @@ -105,14 +106,29 @@ namespace Perspex.Markup.UnitTests.Data } [Fact] - public void Should_Coerce_Set_Invalid_Double_String_To_Default_Value() + public void Setting_Invalid_Double_String_Should_Not_Change_Target() { var data = new Class1 { DoubleValue = 5.6 }; var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string)); target.OnNext("foo"); - Assert.Equal(0, data.DoubleValue); + Assert.Equal(5.6, data.DoubleValue); + } + + [Fact] + public void Setting_Invalid_Double_String_Should_Use_FallbackValue() + { + var data = new Class1 { DoubleValue = 5.6 }; + var target = new ExpressionSubject( + new ExpressionObserver(data, "DoubleValue"), + typeof(string), + DefaultValueConverter.Instance, + fallbackValue: "9.8"); + + target.OnNext("foo"); + + Assert.Equal(9.8, data.DoubleValue); } [Fact] 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 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 @@ + diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs new file mode 100644 index 0000000000..7e4656fcc6 --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; +using Perspex.Logging; +using Perspex.UnitTests; +using Xunit; + +namespace Perspex.Markup.Xaml.UnitTests.Xaml +{ + public class ControlBindingTests + { + [Fact] + public void Binding_ProgressBar_Value_To_Invalid_Value_Uses_FallbackValue() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new PerspexXamlLoader(); + var window = (Window)loader.Load(xaml); + var progressBar = (ProgressBar)window.Content; + + window.DataContext = new { Value = "foo" }; + window.ApplyTemplate(); + + Assert.Equal(3, progressBar.Value); + } + } + + [Fact] + public void Invalid_FallbackValue_Logs_Error() + { + var called = false; + + LogCallback checkLogMessage = (level, area, src, mt, pv) => + { + if (level == LogEventLevel.Error && + area == LogArea.Binding && + mt == "Error binding to {Target}.{Property}: {Message}" && + pv.Length == 3 && + pv[0] is ProgressBar && + object.ReferenceEquals(pv[1], ProgressBar.ValueProperty) && + (string)pv[2] == "Could not convert FallbackValue 'bar' to 'System.Double'") + { + called = true; + } + }; + + using (UnitTestApplication.Start(TestServices.StyledWindow)) + using (TestLogSink.Start(checkLogMessage)) + { + var xaml = @" + + +"; + var loader = new PerspexXamlLoader(); + var window = (Window)loader.Load(xaml); + var progressBar = (ProgressBar)window.Content; + + window.DataContext = new { Value = "foo" }; + window.ApplyTemplate(); + + Assert.Equal(0, progressBar.Value); + Assert.True(called); + } + } + } +} 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); + } + } +}