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: 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/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(); } } 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. /// 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); 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); + } + } } /// 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/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.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); + } + }); } } 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.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/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..a0340099a5 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..5b2fc5ec9e 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/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/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..072b5a0840 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 Header}" 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 @@ - + 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) { 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..243c0c53f4 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -104,13 +104,14 @@ + - + 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/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.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index bf754914dc..88efe3f3b1 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -36,5 +36,49 @@ 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", + Child = new TestViewModel + { + String = "Grandchild", + } + }, + }; + + 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.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)); } 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); + } + } +}