Browse Source

Merge branch 'binding-changes'

pull/494/merge
Steven Kirk 10 years ago
parent
commit
47491612c1
  1. 1
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  2. 68
      src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
  3. 2
      src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  4. 13
      src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  5. 14
      src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs
  6. 39
      src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs
  7. 2
      src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
  8. 9
      src/Markup/Perspex.Markup/DefaultValueConverter.cs
  9. 1
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  10. 59
      src/Perspex.Base/Data/BindingError.cs
  11. 20
      src/Perspex.Base/DirectProperty.cs
  12. 19
      src/Perspex.Base/IPriorityValueOwner.cs
  13. 2
      src/Perspex.Base/Perspex.Base.csproj
  14. 106
      src/Perspex.Base/PerspexObject.cs
  15. 11
      src/Perspex.Base/PerspexProperty.cs
  16. 8
      src/Perspex.Base/PerspexProperty`1.cs
  17. 47
      src/Perspex.Base/PriorityBindingEntry.cs
  18. 74
      src/Perspex.Base/PriorityLevel.cs
  19. 135
      src/Perspex.Base/PriorityValue.cs
  20. 3
      src/Perspex.Base/Properties/AssemblyInfo.cs
  21. 2
      src/Perspex.Base/StyledPropertyBase.cs
  22. 69
      src/Perspex.Controls/Presenters/TextPresenter.cs
  23. 19
      src/Perspex.Controls/TextBlock.cs
  24. 81
      src/Perspex.Controls/TextBox.cs
  25. 2
      src/Perspex.Themes.Default/TextBox.xaml
  26. 12
      tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  27. 59
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs
  28. 59
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs
  29. 77
      tests/Perspex.Base.UnitTests/PriorityValueTests.cs
  30. 1
      tests/Perspex.Base.UnitTests/packages.config
  31. 14
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  32. 24
      tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs
  33. 3
      tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs
  34. 1
      tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj
  35. 71
      tests/Perspex.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
  36. 1
      tests/Perspex.UnitTests/Perspex.UnitTests.csproj
  37. 38
      tests/Perspex.UnitTests/TestLogSink.cs

1
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,

68
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;

2
src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs

@ -26,7 +26,7 @@ namespace Perspex.Markup.Data.Plugins
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found.
/// property will be made.
/// </returns>
IPropertyAccessor Start(
WeakReference reference,

13
src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs

@ -38,7 +38,7 @@ namespace Perspex.Markup.Data.Plugins
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found.
/// property will be made.
/// </returns>
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));
}
}

14
src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs

@ -33,7 +33,7 @@ namespace Perspex.Markup.Data.Plugins
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found.
/// property will be made.
/// </returns>
public IPropertyAccessor Start(
WeakReference reference,
@ -52,14 +52,14 @@ namespace Perspex.Markup.Data.Plugins
{
return new Accessor(new WeakReference<PerspexObject>(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;
}
}

39
src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs

@ -0,0 +1,39 @@
using System;
using Perspex.Data;
namespace Perspex.Markup.Data.Plugins
{
/// <summary>
/// An <see cref="IPropertyAccessor"/> that represents an error.
/// </summary>
public class PropertyError : IPropertyAccessor
{
private BindingError _error;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyError"/> class.
/// </summary>
/// <param name="error">The error to report.</param>
public PropertyError(BindingError error)
{
_error = error;
}
/// <inheritdoc/>
public Type PropertyType => null;
/// <inheritdoc/>
public object Value => _error;
/// <inheritdoc/>
public void Dispose()
{
}
/// <inheritdoc/>
public bool SetValue(object value, BindingPriority priority)
{
return false;
}
}
}

2
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));

9
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;

1
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -56,6 +56,7 @@
<Compile Include="Data\Parsers\IdentifierParser.cs" />
<Compile Include="Data\Parsers\ExpressionParser.cs" />
<Compile Include="Data\Parsers\Reader.cs" />
<Compile Include="Data\Plugins\PropertyError.cs" />
<Compile Include="Data\PropertyAccessorNode.cs" />
<Compile Include="Data\ExpressionNode.cs" />
<Compile Include="Data\ExpressionObserver.cs" />

59
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
{
/// <summary>
/// Represents a recoverable binding error.
/// </summary>
/// <remarks>
/// 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 <see cref="BindingError"/>, 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.
/// </remarks>
public class BindingError
{
/// <summary>
/// Initializes a new instance of the <see cref="BindingError"/> class.
/// </summary>
/// <param name="exception">An exception describing the binding error.</param>
public BindingError(Exception exception)
{
Exception = exception;
}
/// <summary>
/// Initializes a new instance of the <see cref="BindingError"/> class.
/// </summary>
/// <param name="exception">An exception describing the binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public BindingError(Exception exception, object fallbackValue)
{
Exception = exception;
FallbackValue = fallbackValue;
UseFallbackValue = true;
}
/// <summary>
/// Gets the exception describing the binding error.
/// </summary>
public Exception Exception { get; }
/// <summary>
/// Get the fallback value.
/// </summary>
public object FallbackValue { get; }
/// <summary>
/// Get a value indicating whether the fallback value should be pushed to the binding
/// target.
/// </summary>
public bool UseFallbackValue { get; }
}
}

20
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
/// <param name="source">The property to copy.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property. May be null.</param>
/// <param name="metadata">Optional overridden metadata.</param>
private DirectProperty(
PerspexProperty<TValue> source,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter)
: base(source, typeof(TOwner))
Action<TOwner, TValue> setter,
PropertyMetadata metadata)
: base(source, typeof(TOwner), metadata)
{
Contract.Requires<ArgumentNullException>(getter != null);
@ -76,16 +79,25 @@ namespace Perspex
/// Registers the direct property on another type.
/// </summary>
/// <typeparam name="TNewOwner">The type of the additional owner.</typeparam>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
/// <param name="unsetValue">
/// The value to use when the property is set to <see cref="PerspexProperty.UnsetValue"/>
/// </param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <returns>The property.</returns>
public DirectProperty<TNewOwner, TValue> AddOwner<TNewOwner>(
Func<TNewOwner, TValue> getter,
Action<TNewOwner, TValue> setter = null)
Action<TNewOwner, TValue> setter = null,
TValue unsetValue = default(TValue),
BindingMode defaultBindingMode = BindingMode.OneWay)
where TNewOwner : PerspexObject
{
var result = new DirectProperty<TNewOwner, TValue>(
this,
getter,
setter);
setter,
new DirectPropertyMetadata<TValue>(unsetValue, defaultBindingMode));
PerspexPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;

19
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
{
/// <summary>
/// An owner of a <see cref="PriorityValue"/>.
/// </summary>
internal interface IPriorityValueOwner
{
/// <summary>
/// Called when a <see cref="PriorityValue"/>'s value changes.
/// </summary>
/// <param name="sender">The source of the change.</param>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
void Changed(PriorityValue sender, object oldValue, object newValue);
}
}

2
src/Perspex.Base/Perspex.Base.csproj

@ -43,6 +43,7 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\BindingError.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" />
@ -54,6 +55,7 @@
<Compile Include="Diagnostics\IPerspexObjectDebug.cs" />
<Compile Include="Diagnostics\PerspexObjectExtensions.cs" />
<Compile Include="AttachedProperty.cs" />
<Compile Include="IPriorityValueOwner.cs" />
<Compile Include="IStyledPropertyAccessor.cs" />
<Compile Include="IDirectPropertyAccessor.cs" />
<Compile Include="DirectProperty.cs" />

106
src/Perspex.Base/PerspexObject.cs

@ -21,7 +21,7 @@ namespace Perspex
/// <remarks>
/// This class is analogous to DependencyObject in WPF.
/// </remarks>
public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged
public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
{
/// <summary>
/// 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
}
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
Delegate[] IPerspexObjectDebug.GetPropertyChangedSubscribers()
{
@ -587,6 +615,27 @@ namespace Perspex
}
}
/// <summary>
/// Tries to cast a value to a type, taking into account that the value may be a
/// <see cref="BindingError"/>.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="type">The type.</param>
/// <returns>The cast value, or a <see cref="BindingError"/>.</returns>
private static object CastOrDefault(object value, Type type)
{
var error = value as BindingError;
if (error == null)
{
return TypeUtilities.CastOrDefault(value, type);
}
else
{
return error;
}
}
/// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>.
/// </summary>
@ -604,35 +653,42 @@ namespace Perspex
PriorityValue result = new PriorityValue(
this,
property.Name,
property,
property.PropertyType,
validate2);
result.Changed.Subscribe(x =>
return result;
}
/// <summary>
/// Sets a property value for a direct property binding.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <returns></returns>
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);
}
}
/// <summary>

11
src/Perspex.Base/PerspexProperty.cs

@ -71,7 +71,11 @@ namespace Perspex
/// </summary>
/// <param name="source">The direct property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
protected PerspexProperty(PerspexProperty source, Type ownerType)
/// <param name="metadata">Optional overridden metadata.</param>
protected PerspexProperty(
PerspexProperty source,
Type ownerType,
PropertyMetadata metadata)
{
Contract.Requires<ArgumentNullException>(source != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
@ -86,6 +90,11 @@ namespace Perspex
Notifying = source.Notifying;
Id = source.Id;
_defaultMetadata = source._defaultMetadata;
if (metadata != null)
{
_metadata.Add(ownerType, metadata);
}
}
/// <summary>

8
src/Perspex.Base/PerspexProperty`1.cs

@ -32,8 +32,12 @@ namespace Perspex
/// </summary>
/// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param>
protected PerspexProperty(PerspexProperty source, Type ownerType)
: base(source, ownerType)
/// <param name="metadata">Optional overridden metadata.</param>
protected PerspexProperty(
PerspexProperty source,
Type ownerType,
PropertyMetadata metadata)
: base(source, ownerType, metadata)
{
}
}

47
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
/// </summary>
internal class PriorityBindingEntry : IDisposable
{
/// <summary>
/// The binding subscription.
/// </summary>
private PriorityLevel _owner;
private IDisposable _subscription;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="index">
/// The binding index. Later bindings should have higher indexes.
/// </param>
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.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="changed">Called when the binding changes.</param>
/// <param name="completed">Called when the binding completes.</param>
public void Start(
IObservable<object> binding,
Action<PriorityBindingEntry> changed,
Action<PriorityBindingEntry> completed)
public void Start(IObservable<object> binding)
{
Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null);
Contract.Requires<ArgumentNullException>(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);
}
/// <summary>
@ -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);
}
}
}

74
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
{
/// <summary>
/// Determines how the current binding is selected for a <see cref="PriorityLevel"/>.
/// </summary>
internal enum LevelPrecedenceMode
{
/// <summary>
/// The latest fired binding is used as the current value.
/// </summary>
Latest,
/// <summary>
/// The latest added binding is used as the current value.
/// </summary>
Newest,
}
/// <summary>
/// Stores bindings for a priority level in a <see cref="PriorityValue"/>.
/// </summary>
@ -47,38 +33,22 @@ namespace Perspex
/// </remarks>
internal class PriorityLevel
{
/// <summary>
/// Method called when current value changes.
/// </summary>
private readonly Action<PriorityLevel> _changed;
/// <summary>
/// The current direct value.
/// </summary>
private PriorityValue _owner;
private object _directValue;
/// <summary>
/// The index of the next <see cref="PriorityBindingEntry"/>.
/// </summary>
private int _nextIndex;
private readonly LevelPrecedenceMode _mode;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityLevel"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="priority">The priority.</param>
/// <param name="mode">The precedence mode.</param>
/// <param name="changed">A method to be called when the current value changes.</param>
public PriorityLevel(
int priority,
LevelPrecedenceMode mode,
Action<PriorityLevel> changed)
PriorityValue owner,
int priority)
{
Contract.Requires<ArgumentNullException>(changed != null);
Contract.Requires<ArgumentNullException>(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<ArgumentNullException>(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 <see cref="Bindings"/> changes value.
/// </summary>
/// <param name="entry">The entry that changed.</param>
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 <see cref="Bindings"/> completes.
/// </summary>
/// <param name="entry">The entry that completed.</param>
private void Completed(PriorityBindingEntry entry)
public void Completed(PriorityBindingEntry entry)
{
Bindings.Remove(entry);
@ -184,6 +154,16 @@ namespace Perspex
}
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> encounters a recoverable error.
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="error">The error.</param>
public void Error(PriorityBindingEntry entry, BindingError error)
{
_owner.LevelError(this, error);
}
/// <summary>
/// Activates the first binding that has a value.
/// </summary>
@ -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);
}
}
}

135
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 <see cref="Value"/> is selected from the highest
/// priority binding that doesn't return <see cref="PerspexProperty.UnsetValue"/>. 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 <see cref="Changed"/> observable is
/// fired with the old and new values.
/// has a higher priority. Each time the value changes, the
/// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the
/// owner object is fired with the old and new values.
/// </remarks>
internal class PriorityValue
{
/// <summary>
/// The owner of the object.
/// </summary>
private readonly PerspexObject _owner;
/// <summary>
/// The name of the property.
/// </summary>
private readonly string _name;
/// <summary>
/// The value type.
/// </summary>
private readonly IPriorityValueOwner _owner;
private readonly Type _valueType;
/// <summary>
/// The currently registered bindings organised by priority.
/// </summary>
private readonly Dictionary<int, PriorityLevel> _levels = new Dictionary<int, PriorityLevel>();
/// <summary>
/// The changed observable.
/// </summary>
private readonly Subject<Tuple<object, object>> _changed = new Subject<Tuple<object, object>>();
/// <summary>
/// The current value.
/// </summary>
private object _value;
/// <summary>
/// The function used to validate the value, if any.
/// </summary>
private readonly Func<object, object> _validate;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
/// </summary>
/// <param name="owner">The owner of the object.</param>
/// <param name="name">The name of the property.</param>
/// <param name="property">The property that the value represents.</param>
/// <param name="valueType">The value type.</param>
/// <param name="validate">An optional validation function.</param>
public PriorityValue(
PerspexObject owner,
string name,
IPriorityValueOwner owner,
PerspexProperty property,
Type valueType,
Func<object, object> validate = null)
{
_owner = owner;
_name = name;
Property = property;
_valueType = valueType;
_value = PerspexProperty.UnsetValue;
ValuePriority = int.MaxValue;
@ -82,12 +54,9 @@ namespace Perspex
}
/// <summary>
/// Fired whenever the current <see cref="Value"/> changes.
/// Gets the property that the value represents.
/// </summary>
/// <remarks>
/// The old and new values may be the same, this class does not check for distinct values.
/// </remarks>
public IObservable<Tuple<object, object>> Changed => _changed;
public PerspexProperty Property { get; }
/// <summary>
/// Gets the current value.
@ -181,6 +150,51 @@ namespace Perspex
return b.ToString();
}
/// <summary>
/// Called when the value for a priority level changes.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
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);
}
}
}
/// <summary>
/// Called when a priority level encounters an error.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
/// <param name="error">The binding error.</param>
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);
}
/// <summary>
/// Causes a revalidation of the value.
/// </summary>
@ -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());
}
}
/// <summary>
/// Called when the value for a priority level changes.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
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);
}
}
}
}
}

3
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")]
[assembly: InternalsVisibleTo("Perspex.Base.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

2
src/Perspex.Base/StyledPropertyBase.cs

@ -46,7 +46,7 @@ namespace Perspex
/// <param name="source">The property to add the owner to.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
protected StyledPropertyBase(StyledPropertyBase<TValue> source, Type ownerType)
: base(source, ownerType)
: base(source, ownerType, null)
{
_inherits = source.Inherits;
}

69
src/Perspex.Controls/Presenters/TextPresenter.cs

@ -12,24 +12,28 @@ namespace Perspex.Controls.Presenters
{
public class TextPresenter : TextBlock
{
public static readonly StyledProperty<int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>();
public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly StyledProperty<int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>();
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly StyledProperty<int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>();
public static readonly DirectProperty<TextPresenter, int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(
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<TextPresenter>((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;

19
src/Perspex.Controls/TextBlock.cs

@ -72,8 +72,11 @@ namespace Perspex.Controls
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly StyledProperty<string> TextProperty =
PerspexProperty.Register<TextBlock, string>(nameof(Text));
public static readonly DirectProperty<TextBlock, string> TextProperty =
PerspexProperty.RegisterDirect<TextBlock, string>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
@ -87,14 +90,8 @@ namespace Perspex.Controls
public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
PerspexProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping));
/// <summary>
/// The formatted text used for rendering.
/// </summary>
private string _text;
private FormattedText _formattedText;
/// <summary>
/// Stores the last constraint passed to MeasureOverride.
/// </summary>
private Size _constraint;
/// <summary>
@ -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); }
}
/// <summary>

81
src/Perspex.Controls/TextBox.cs

@ -29,18 +29,29 @@ namespace Perspex.Controls
public static readonly DirectProperty<TextBox, bool> CanScrollHorizontallyProperty =
PerspexProperty.RegisterDirect<TextBox, bool>("CanScrollHorizontally", o => o.CanScrollHorizontally);
// TODO: Should CaretIndex, SelectionStart/End and Text be direct properties?
public static readonly StyledProperty<int> CaretIndexProperty =
PerspexProperty.Register<TextBox, int>("CaretIndex", validate: ValidateCaretIndex);
public static readonly StyledProperty<int> SelectionStartProperty =
PerspexProperty.Register<TextBox, int>("SelectionStart", validate: ValidateCaretIndex);
public static readonly StyledProperty<int> SelectionEndProperty =
PerspexProperty.Register<TextBox, int>("SelectionEnd", validate: ValidateCaretIndex);
public static readonly StyledProperty<string> TextProperty =
TextBlock.TextProperty.AddOwner<TextBox>();
public static readonly DirectProperty<TextBox, int> CaretIndexProperty =
PerspexProperty.RegisterDirect<TextBox, int>(
nameof(CaretIndex),
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly DirectProperty<TextBox, int> SelectionStartProperty =
PerspexProperty.RegisterDirect<TextBox, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<TextBox, int> SelectionEndProperty =
PerspexProperty.RegisterDirect<TextBox, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<TextBox, string> TextProperty =
TextBlock.TextProperty.AddOwner<TextBox>(
o => o.Text,
(o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
TextBlock.TextAlignmentProperty.AddOwner<TextBox>();
@ -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<UndoRedoState> _undoRedoHelper;
@ -78,7 +93,6 @@ namespace Perspex.Controls
static TextBox()
{
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
TextProperty.OverrideMetadata<TextBox>(new StyledPropertyMetadata<string>(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));
}

2
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}"/>
</Panel>

12
tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj

@ -44,6 +44,10 @@
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
<Reference Include="Moq, Version=4.2.1510.2205, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<HintPath>..\..\packages\Moq.4.2.1510.2205\lib\net40\Moq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
@ -61,10 +65,6 @@
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c">
<HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
@ -124,6 +124,10 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Perspex.Base</Name>
</ProjectReference>
<ProjectReference Include="..\Perspex.UnitTests\Perspex.UnitTests.csproj">
<Project>{88060192-33d5-4932-b0f9-8bd2763e857d}</Project>
<Name>Perspex.UnitTests</Name>
</ProjectReference>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">

59
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<object>();
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<object>();
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<object>();
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);
}
}
/// <summary>
/// Returns an observable that returns a single value but does not complete.
/// </summary>

59
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<object>();
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<object>();
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<object>();
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<Class1, string> FooProperty =

77
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<string>(
"Test",
typeof(PriorityValueTests),
new StyledPropertyMetadata<string>());
[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<string>("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<object>("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<object>("na");
var source = new BehaviorSubject<object>("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<object>("na");
var source = new BehaviorSubject<object>("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<object>("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<string>("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<object>("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<IPriorityValueOwner>();
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<IPriorityValueOwner>();
var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
var subject = new BehaviorSubject<object>("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<object>("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<object>("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<object>("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<object>();
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<object>();
target.Add(source, 0);

1
tests/Perspex.Base.UnitTests/packages.config

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Moq" version="4.2.1510.2205" targetFramework="net45" />
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />

14
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<BindingError>(result);
var error = result as BindingError;
Assert.IsType<MissingMemberException>(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<BindingError>(result[1]);
Assert.Equal("baz", result[2]);
sub.Dispose();

24
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<BindingError>(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]

3
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<BindingError>(result);
}
private enum TestEnum

1
tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

@ -104,6 +104,7 @@
<Compile Include="StyleTests.cs" />
<Compile Include="Templates\DataTemplateTests.cs" />
<Compile Include="Xaml\BasicTests.cs" />
<Compile Include="Xaml\ControlBindingTests.cs" />
<Compile Include="Xaml\BindingTests.cs" />
<Compile Include="Xaml\DataTemplateTests.cs" />
<Compile Include="Xaml\InitializationOrderTracker.cs" />

71
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 = @"
<Window xmlns='https://github.com/perspex'>
<ProgressBar Maximum='10' Value='{Binding Value, FallbackValue=3}'/>
</Window>";
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 = @"
<Window xmlns='https://github.com/perspex'>
<ProgressBar Maximum='10' Value='{Binding Value, FallbackValue=bar}'/>
</Window>";
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);
}
}
}
}

1
tests/Perspex.UnitTests/Perspex.UnitTests.csproj

@ -63,6 +63,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="NotifyingBase.cs" />
<Compile Include="TestLogSink.cs" />
<Compile Include="TestTemplatedRoot.cs" />
<Compile Include="TestRoot.cs" />
<Compile Include="TestServices.cs" />

38
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);
}
}
}
Loading…
Cancel
Save