Browse Source

Merge branch 'master' into skiasharp

pull/506/head
Jason Jarvis 10 years ago
parent
commit
9582fe3c37
  1. 23
      docs/spec/defining-properties.md
  2. 1
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  3. 68
      src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
  4. 2
      src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  5. 13
      src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  6. 14
      src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs
  7. 39
      src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs
  8. 2
      src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
  9. 9
      src/Markup/Perspex.Markup/DefaultValueConverter.cs
  10. 1
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  11. 59
      src/Perspex.Base/Data/BindingError.cs
  12. 20
      src/Perspex.Base/DirectProperty.cs
  13. 19
      src/Perspex.Base/IPriorityValueOwner.cs
  14. 2
      src/Perspex.Base/Perspex.Base.csproj
  15. 106
      src/Perspex.Base/PerspexObject.cs
  16. 11
      src/Perspex.Base/PerspexProperty.cs
  17. 8
      src/Perspex.Base/PerspexProperty`1.cs
  18. 47
      src/Perspex.Base/PriorityBindingEntry.cs
  19. 74
      src/Perspex.Base/PriorityLevel.cs
  20. 135
      src/Perspex.Base/PriorityValue.cs
  21. 3
      src/Perspex.Base/Properties/AssemblyInfo.cs
  22. 2
      src/Perspex.Base/StyledPropertyBase.cs
  23. 2
      src/Perspex.Controls/Canvas.cs
  24. 18
      src/Perspex.Controls/Menu.cs
  25. 4
      src/Perspex.Controls/Mixins/ContentControlMixin.cs
  26. 19
      src/Perspex.Controls/Presenters/CarouselPresenter.cs
  27. 69
      src/Perspex.Controls/Presenters/TextPresenter.cs
  28. 15
      src/Perspex.Controls/Primitives/Popup.cs
  29. 2
      src/Perspex.Controls/Shapes/Ellipse.cs
  30. 2
      src/Perspex.Controls/Shapes/Line.cs
  31. 2
      src/Perspex.Controls/Shapes/Path.cs
  32. 2
      src/Perspex.Controls/Shapes/Polygon.cs
  33. 2
      src/Perspex.Controls/Shapes/Polyline.cs
  34. 2
      src/Perspex.Controls/Shapes/Rectangle.cs
  35. 15
      src/Perspex.Controls/Shapes/Shape.cs
  36. 19
      src/Perspex.Controls/TextBlock.cs
  37. 81
      src/Perspex.Controls/TextBox.cs
  38. 1
      src/Perspex.Input/Raw/RawMouseEventArgs.cs
  39. 1
      src/Perspex.Themes.Default/Button.xaml
  40. 1
      src/Perspex.Themes.Default/CheckBox.xaml
  41. 3
      src/Perspex.Themes.Default/ContentControl.xaml
  42. 1
      src/Perspex.Themes.Default/DropDown.xaml
  43. 1
      src/Perspex.Themes.Default/DropDownItem.xaml
  44. 4
      src/Perspex.Themes.Default/Expander.xaml
  45. 1
      src/Perspex.Themes.Default/LayoutTransformControl.xaml
  46. 1
      src/Perspex.Themes.Default/ListBoxItem.xaml
  47. 3
      src/Perspex.Themes.Default/MenuItem.xaml
  48. 1
      src/Perspex.Themes.Default/RadioButton.xaml
  49. 1
      src/Perspex.Themes.Default/TabStripItem.xaml
  50. 2
      src/Perspex.Themes.Default/TextBox.xaml
  51. 1
      src/Perspex.Themes.Default/ToggleButton.xaml
  52. 1
      src/Perspex.Themes.Default/ToolTip.xaml
  53. 1
      src/Perspex.Themes.Default/TreeViewItem.xaml
  54. 5
      src/Perspex.Themes.Default/Window.xaml
  55. 29
      src/Windows/Perspex.Win32/WindowImpl.cs
  56. 12
      tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  57. 59
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs
  58. 59
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs
  59. 77
      tests/Perspex.Base.UnitTests/PriorityValueTests.cs
  60. 1
      tests/Perspex.Base.UnitTests/packages.config
  61. 14
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  62. 24
      tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs
  63. 3
      tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs
  64. 3
      tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj
  65. 44
      tests/Perspex.Markup.Xaml.UnitTests/TestViewModel.cs
  66. 41
      tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs
  67. 71
      tests/Perspex.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
  68. 44
      tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
  69. 3
      tests/Perspex.UnitTests/NotifyingBase.cs
  70. 1
      tests/Perspex.UnitTests/Perspex.UnitTests.csproj
  71. 38
      tests/Perspex.UnitTests/TestLogSink.cs

23
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<MyControl, IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<MyControl>(
o => o.Items,
(o, v) => o.Items = v);
private IEnumerable _items = new PerspexList<object>();
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:

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

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

18
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();
}
/// <summary>
/// Listens for non-client clicks and closes the menu when one is detected.
/// </summary>
/// <param name="e">The raw event.</param>
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawMouseEventArgs;
if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
{
Close();
}
}
/// <summary>
/// Called when a submenu is clicked somewhere in the menu.
/// </summary>

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

19
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
/// </summary>
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<object>().Count()) ? value : -1;
if (old != effective)
{
_selectedIndex = effective;
RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
}
}
}
/// <summary>

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;

15
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;
/// <summary>
/// Initializes static members of the <see cref="Popup"/> 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)

2
src/Perspex.Controls/Shapes/Ellipse.cs

@ -9,7 +9,7 @@ namespace Perspex.Controls.Shapes
{
static Ellipse()
{
AffectsGeometry(BoundsProperty, StrokeThicknessProperty);
AffectsGeometry<Ellipse>(BoundsProperty, StrokeThicknessProperty);
}
protected override Geometry CreateDefiningGeometry()

2
src/Perspex.Controls/Shapes/Line.cs

@ -16,7 +16,7 @@ namespace Perspex.Controls.Shapes
static Line()
{
StrokeThicknessProperty.OverrideDefaultValue<Line>(1);
AffectsGeometry(StartPointProperty, EndPointProperty);
AffectsGeometry<Line>(StartPointProperty, EndPointProperty);
}
public Point StartPoint

2
src/Perspex.Controls/Shapes/Path.cs

@ -13,7 +13,7 @@ namespace Perspex.Controls.Shapes
static Path()
{
AffectsGeometry(DataProperty);
AffectsGeometry<Path>(DataProperty);
}
public Geometry Data

2
src/Perspex.Controls/Shapes/Polygon.cs

@ -13,7 +13,7 @@ namespace Perspex.Controls.Shapes
static Polygon()
{
AffectsGeometry(PointsProperty);
AffectsGeometry<Polygon>(PointsProperty);
}
public IList<Point> Points

2
src/Perspex.Controls/Shapes/Polyline.cs

@ -14,7 +14,7 @@ namespace Perspex.Controls.Shapes
static Polyline()
{
StrokeThicknessProperty.OverrideDefaultValue<Polyline>(1);
AffectsGeometry(PointsProperty);
AffectsGeometry<Polyline>(PointsProperty);
}
public IList<Point> Points

2
src/Perspex.Controls/Shapes/Rectangle.cs

@ -9,7 +9,7 @@ namespace Perspex.Controls.Shapes
{
static Rectangle()
{
AffectsGeometry(BoundsProperty, StrokeThicknessProperty);
AffectsGeometry<Rectangle>(BoundsProperty, StrokeThicknessProperty);
}
protected override Geometry CreateDefiningGeometry()

15
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 <see cref="InvalidateGeometry"/> to be called on the element.
/// </remarks>
protected static void AffectsGeometry(params PerspexProperty[] properties)
protected static void AffectsGeometry<TShape>(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);
}
});
}
}

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

1
src/Perspex.Input/Raw/RawMouseEventArgs.cs

@ -16,6 +16,7 @@ namespace Perspex.Input.Raw
MiddleButtonUp,
Move,
Wheel,
NonClientLeftButtonDown,
}
/// <summary>

1
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}"

1
src/Perspex.Themes.Default/CheckBox.xaml

@ -22,6 +22,7 @@
</Border>
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Margin="4,0,0,0"
VerticalAlignment="Center"
Grid.Column="1"/>

3
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}"/>
</ControlTemplate>
</Setter>

1
src/Perspex.Themes.Default/DropDown.xaml

@ -11,6 +11,7 @@
BorderThickness="{TemplateBinding BorderThickness}">
<Grid ColumnDefinitions="*,Auto">
<ContentPresenter Content="{TemplateBinding SelectionBoxItem}"
DataContext="{TemplateBinding SelectionBoxItem}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>

1
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}"/>

4
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" />
</Grid>
@ -34,6 +35,7 @@
Grid.Row="0"
IsVisible="{TemplateBinding IsExpanded}"
Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
@ -51,6 +53,7 @@
Grid.Column="1"
IsVisible="{TemplateBinding IsExpanded}"
Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
@ -68,6 +71,7 @@
Grid.Column="0"
IsVisible="{TemplateBinding IsExpanded}"
Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>

1
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}"/>
</ControlTemplate>
</Setter>

1
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}"/>
</ControlTemplate>
</Setter>

3
src/Perspex.Themes.Default/MenuItem.xaml

@ -13,6 +13,7 @@
<Grid ColumnDefinitions="22,13,*,20">
<ContentPresenter Name="icon"
Content="{TemplateBinding Icon}"
DataContext="{TemplateBinding Icon}"
Width="16"
Height="16"
Margin="3"
@ -26,6 +27,7 @@
VerticalAlignment="Center"/>
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
DataContext="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Grid.Column="2">
@ -81,6 +83,7 @@
<Panel>
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
DataContext="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">

1
src/Perspex.Themes.Default/RadioButton.xaml

@ -21,6 +21,7 @@
VerticalAlignment="Center"/>
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Margin="4,0,0,0"
VerticalAlignment="Center"
Grid.Column="1"/>

1
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}"/>
</ControlTemplate>
</Setter>

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>

1
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}"

1
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}"/>
</ControlTemplate>
</Setter>

1
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"/>

5
src/Perspex.Themes.Default/Window.xaml

@ -6,7 +6,10 @@
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<AdornerDecorator>
<ContentPresenter Name="PART_ContentPresenter" Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}"/>
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"/>
</AdornerDecorator>
</Border>
</ControlTemplate>

29
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)
{

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

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

@ -104,13 +104,14 @@
<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" />
<Compile Include="Xaml\NonControl.cs" />
<Compile Include="Xaml\TreeDataTemplateTests.cs" />
<Compile Include="TypeProviderMock.cs" />
<Compile Include="ViewModelMock.cs" />
<Compile Include="TestViewModel.cs" />
<Compile Include="Xaml\StyleTests.cs" />
</ItemGroup>
<ItemGroup>

44
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();
}
}
}
}

41
tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs

@ -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));
}
}
}

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

44
tests/Perspex.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

@ -36,5 +36,49 @@ namespace Perspex.Markup.Xaml.UnitTests.Xaml
Assert.IsType<Canvas>(target.Presenter.Child);
}
}
[Fact]
public void Can_Set_DataContext_In_DataTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/perspex'
xmlns:local='clr-namespace:Perspex.Markup.Xaml.UnitTests;assembly=Perspex.Markup.Xaml.UnitTests'>
<Window.DataTemplates>
<DataTemplate DataType='{Type local:TestViewModel}'>
<Canvas Name='foo' DataContext='{Binding Child}'/>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='{Binding Child}'/>
</Window>";
var loader = new PerspexXamlLoader();
var window = (Window)loader.Load(xaml);
var target = window.FindControl<ContentControl>("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);
}
}
}
}

3
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));
}

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