Browse Source

Merge branch 'master' into skiasharp

pull/506/head
Jason Jarvis 11 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 on which the property is being set and the value and returns the coerced value
or throws an exception for an invalid 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 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 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. - Overriding default values.
- Inherited 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 ## When to use a Direct vs a Styled Property
Direct properties have advantages and disadvantages: 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, Converter = Converter,
ConverterParameter = ConverterParameter, ConverterParameter = ConverterParameter,
ElementName = ElementName, ElementName = ElementName,
FallbackValue = FallbackValue,
Mode = Mode, Mode = Mode,
Path = Path, Path = Path,
Priority = Priority, Priority = Priority,

68
src/Markup/Perspex.Markup/Data/ExpressionSubject.cs

@ -6,6 +6,7 @@ using System.Globalization;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Perspex.Data; using Perspex.Data;
using Perspex.Logging;
using Perspex.Utilities; using Perspex.Utilities;
namespace Perspex.Markup.Data namespace Perspex.Markup.Data
@ -103,9 +104,44 @@ namespace Perspex.Markup.Data
if (converted == PerspexProperty.UnsetValue) if (converted == PerspexProperty.UnsetValue)
{ {
converted = TypeUtilities.Default(type); 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) private object ConvertValue(object value)
{ {
var converted = Converter.Convert( var converted =
value as BindingError ??
Converter.Convert(
value, value,
_targetType, _targetType,
ConverterParameter, ConverterParameter,
CultureInfo.CurrentUICulture); 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; 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> /// <param name="changed">A function to call when the property changes.</param>
/// <returns> /// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// 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> /// </returns>
IPropertyAccessor Start( IPropertyAccessor Start(
WeakReference reference, 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> /// <param name="changed">A function to call when the property changes.</param>
/// <returns> /// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// 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> /// </returns>
public IPropertyAccessor Start( public IPropertyAccessor Start(
WeakReference reference, WeakReference reference,
@ -58,14 +58,9 @@ namespace Perspex.Markup.Data.Plugins
} }
else else
{ {
Logger.Error( var message = $"Could not find CLR property '{propertyName}' on '{instance}'";
LogArea.Binding, var exception = new MissingMemberException(message);
this, return new PropertyError(new BindingError(exception));
"Could not find CLR property {Property} on {Source}",
propertyName,
instance);
return null;
} }
} }

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> /// <param name="changed">A function to call when the property changes.</param>
/// <returns> /// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the /// 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> /// </returns>
public IPropertyAccessor Start( public IPropertyAccessor Start(
WeakReference reference, WeakReference reference,
@ -52,14 +52,14 @@ namespace Perspex.Markup.Data.Plugins
{ {
return new Accessor(new WeakReference<PerspexObject>(o), p, changed); 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 else
{ {
Logger.Error(
LogArea.Binding,
this,
"Could not find PerspexProperty {Property} on {Source}",
propertyName,
instance);
return null; 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; var instance = reference.Target;
if (instance != null) if (instance != null && instance != PerspexProperty.UnsetValue)
{ {
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference)); 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.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Perspex.Data;
using Perspex.Logging; using Perspex.Logging;
using Perspex.Utilities; using Perspex.Utilities;
@ -42,12 +43,8 @@ namespace Perspex.Markup
if (value != null) if (value != null)
{ {
Logger.Error( var message = $"Could not convert '{value}' to '{targetType}'";
LogArea.Binding, return new BindingError(new InvalidCastException(message));
this,
"Could not convert {Value} to {Type}",
value,
targetType);
} }
return PerspexProperty.UnsetValue; 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\IdentifierParser.cs" />
<Compile Include="Data\Parsers\ExpressionParser.cs" /> <Compile Include="Data\Parsers\ExpressionParser.cs" />
<Compile Include="Data\Parsers\Reader.cs" /> <Compile Include="Data\Parsers\Reader.cs" />
<Compile Include="Data\Plugins\PropertyError.cs" />
<Compile Include="Data\PropertyAccessorNode.cs" /> <Compile Include="Data\PropertyAccessorNode.cs" />
<Compile Include="Data\ExpressionNode.cs" /> <Compile Include="Data\ExpressionNode.cs" />
<Compile Include="Data\ExpressionObserver.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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Perspex.Data;
namespace Perspex namespace Perspex
{ {
@ -44,11 +45,13 @@ namespace Perspex
/// <param name="source">The property to copy.</param> /// <param name="source">The property to copy.</param>
/// <param name="getter">Gets the current value of the property.</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="setter">Sets the value of the property. May be null.</param>
/// <param name="metadata">Optional overridden metadata.</param>
private DirectProperty( private DirectProperty(
PerspexProperty<TValue> source, PerspexProperty<TValue> source,
Func<TOwner, TValue> getter, Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter) Action<TOwner, TValue> setter,
: base(source, typeof(TOwner)) PropertyMetadata metadata)
: base(source, typeof(TOwner), metadata)
{ {
Contract.Requires<ArgumentNullException>(getter != null); Contract.Requires<ArgumentNullException>(getter != null);
@ -76,16 +79,25 @@ namespace Perspex
/// Registers the direct property on another type. /// Registers the direct property on another type.
/// </summary> /// </summary>
/// <typeparam name="TNewOwner">The type of the additional owner.</typeparam> /// <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> /// <returns>The property.</returns>
public DirectProperty<TNewOwner, TValue> AddOwner<TNewOwner>( public DirectProperty<TNewOwner, TValue> AddOwner<TNewOwner>(
Func<TNewOwner, TValue> getter, 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 where TNewOwner : PerspexObject
{ {
var result = new DirectProperty<TNewOwner, TValue>( var result = new DirectProperty<TNewOwner, TValue>(
this, this,
getter, getter,
setter); setter,
new DirectPropertyMetadata<TValue>(unsetValue, defaultBindingMode));
PerspexPropertyRegistry.Instance.Register(typeof(TNewOwner), result); PerspexPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return 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"> <Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link> <Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile> </Compile>
<Compile Include="Data\BindingError.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" /> <Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" /> <Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" /> <Compile Include="Data\BindingOperations.cs" />
@ -54,6 +55,7 @@
<Compile Include="Diagnostics\IPerspexObjectDebug.cs" /> <Compile Include="Diagnostics\IPerspexObjectDebug.cs" />
<Compile Include="Diagnostics\PerspexObjectExtensions.cs" /> <Compile Include="Diagnostics\PerspexObjectExtensions.cs" />
<Compile Include="AttachedProperty.cs" /> <Compile Include="AttachedProperty.cs" />
<Compile Include="IPriorityValueOwner.cs" />
<Compile Include="IStyledPropertyAccessor.cs" /> <Compile Include="IStyledPropertyAccessor.cs" />
<Compile Include="IDirectPropertyAccessor.cs" /> <Compile Include="IDirectPropertyAccessor.cs" />
<Compile Include="DirectProperty.cs" /> <Compile Include="DirectProperty.cs" />

106
src/Perspex.Base/PerspexObject.cs

@ -21,7 +21,7 @@ namespace Perspex
/// <remarks> /// <remarks>
/// This class is analogous to DependencyObject in WPF. /// This class is analogous to DependencyObject in WPF.
/// </remarks> /// </remarks>
public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
{ {
/// <summary> /// <summary>
/// Maintains a list of direct property binding subscriptions so that the binding source /// Maintains a list of direct property binding subscriptions so that the binding source
@ -403,9 +403,9 @@ namespace Perspex
IDisposable subscription = null; IDisposable subscription = null;
subscription = source subscription = source
.Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType)) .Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => s_directBindings.Remove(subscription)) .Do(_ => { }, () => s_directBindings.Remove(subscription))
.Subscribe(x => SetValue(property, x)); .Subscribe(x => DirectBindingSet(property, x));
s_directBindings.Add(subscription); 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/> /// <inheritdoc/>
Delegate[] IPerspexObjectDebug.GetPropertyChangedSubscribers() 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> /// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>. /// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>.
/// </summary> /// </summary>
@ -604,35 +653,42 @@ namespace Perspex
PriorityValue result = new PriorityValue( PriorityValue result = new PriorityValue(
this, this,
property.Name, property,
property.PropertyType, property.PropertyType,
validate2); 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) ? if (error.UseFallbackValue)
GetDefaultValue(property) :
x.Item1;
object newValue = (x.Item2 == PerspexProperty.UnsetValue) ?
GetDefaultValue(property) :
x.Item2;
if (!Equals(oldValue, newValue))
{ {
RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority); SetValue(property, error.FallbackValue);
Logger.Verbose(
LogArea.Property,
this,
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
oldValue,
newValue,
(BindingPriority)result.ValuePriority);
} }
});
return result; Logger.Error(
LogArea.Binding,
this,
"Error binding to {Target}.{Property}: {Message}",
this,
property,
error.Exception.Message);
}
} }
/// <summary> /// <summary>

11
src/Perspex.Base/PerspexProperty.cs

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

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

@ -32,8 +32,12 @@ namespace Perspex
/// </summary> /// </summary>
/// <param name="source">The property to copy.</param> /// <param name="source">The property to copy.</param>
/// <param name="ownerType">The new owner type.</param> /// <param name="ownerType">The new owner type.</param>
protected PerspexProperty(PerspexProperty source, Type ownerType) /// <param name="metadata">Optional overridden metadata.</param>
: base(source, ownerType) 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Perspex.Data;
namespace Perspex namespace Perspex
{ {
@ -10,19 +11,19 @@ namespace Perspex
/// </summary> /// </summary>
internal class PriorityBindingEntry : IDisposable internal class PriorityBindingEntry : IDisposable
{ {
/// <summary> private PriorityLevel _owner;
/// The binding subscription.
/// </summary>
private IDisposable _subscription; private IDisposable _subscription;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class. /// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class.
/// </summary> /// </summary>
/// <param name="owner">The owner.</param>
/// <param name="index"> /// <param name="index">
/// The binding index. Later bindings should have higher indexes. /// The binding index. Later bindings should have higher indexes.
/// </param> /// </param>
public PriorityBindingEntry(int index) public PriorityBindingEntry(PriorityLevel owner, int index)
{ {
_owner = owner;
Index = index; Index = index;
} }
@ -61,16 +62,9 @@ namespace Perspex
/// Starts listening to the binding. /// Starts listening to the binding.
/// </summary> /// </summary>
/// <param name="binding">The binding.</param> /// <param name="binding">The binding.</param>
/// <param name="changed">Called when the binding changes.</param> public void Start(IObservable<object> binding)
/// <param name="completed">Called when the binding completes.</param>
public void Start(
IObservable<object> binding,
Action<PriorityBindingEntry> changed,
Action<PriorityBindingEntry> completed)
{ {
Contract.Requires<ArgumentNullException>(binding != null); Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null);
Contract.Requires<ArgumentNullException>(completed != null);
if (_subscription != null) if (_subscription != null)
{ {
@ -85,13 +79,7 @@ namespace Perspex
Description = ((IDescription)binding).Description; Description = ((IDescription)binding).Description;
} }
_subscription = binding.Subscribe( _subscription = binding.Subscribe(ValueChanged, Completed);
value =>
{
Value = value;
changed(this);
},
() => completed(this));
} }
/// <summary> /// <summary>
@ -101,5 +89,26 @@ namespace Perspex
{ {
_subscription?.Dispose(); _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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Perspex.Data;
using Perspex.Logging;
namespace Perspex 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> /// <summary>
/// Stores bindings for a priority level in a <see cref="PriorityValue"/>. /// Stores bindings for a priority level in a <see cref="PriorityValue"/>.
/// </summary> /// </summary>
@ -47,38 +33,22 @@ namespace Perspex
/// </remarks> /// </remarks>
internal class PriorityLevel internal class PriorityLevel
{ {
/// <summary> private PriorityValue _owner;
/// Method called when current value changes.
/// </summary>
private readonly Action<PriorityLevel> _changed;
/// <summary>
/// The current direct value.
/// </summary>
private object _directValue; private object _directValue;
/// <summary>
/// The index of the next <see cref="PriorityBindingEntry"/>.
/// </summary>
private int _nextIndex; private int _nextIndex;
private readonly LevelPrecedenceMode _mode;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PriorityLevel"/> class. /// Initializes a new instance of the <see cref="PriorityLevel"/> class.
/// </summary> /// </summary>
/// <param name="owner">The owner.</param>
/// <param name="priority">The priority.</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( public PriorityLevel(
int priority, PriorityValue owner,
LevelPrecedenceMode mode, int priority)
Action<PriorityLevel> changed)
{ {
Contract.Requires<ArgumentNullException>(changed != null); Contract.Requires<ArgumentNullException>(owner != null);
_mode = mode; _owner = owner;
_changed = changed;
Priority = priority; Priority = priority;
Value = _directValue = PerspexProperty.UnsetValue; Value = _directValue = PerspexProperty.UnsetValue;
ActiveBindingIndex = -1; ActiveBindingIndex = -1;
@ -103,7 +73,7 @@ namespace Perspex
set set
{ {
Value = _directValue = value; Value = _directValue = value;
_changed(this); _owner.LevelValueChanged(this);
} }
} }
@ -132,10 +102,10 @@ namespace Perspex
{ {
Contract.Requires<ArgumentNullException>(binding != null); Contract.Requires<ArgumentNullException>(binding != null);
var entry = new PriorityBindingEntry(_nextIndex++); var entry = new PriorityBindingEntry(this, _nextIndex++);
var node = Bindings.AddFirst(entry); var node = Bindings.AddFirst(entry);
entry.Start(binding, Changed, Completed); entry.Start(binding);
return Disposable.Create(() => return Disposable.Create(() =>
{ {
@ -153,15 +123,15 @@ namespace Perspex
/// Invoked when an entry in <see cref="Bindings"/> changes value. /// Invoked when an entry in <see cref="Bindings"/> changes value.
/// </summary> /// </summary>
/// <param name="entry">The entry that changed.</param> /// <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) if (entry.Value != PerspexProperty.UnsetValue)
{ {
Value = entry.Value; Value = entry.Value;
ActiveBindingIndex = entry.Index; ActiveBindingIndex = entry.Index;
_changed(this); _owner.LevelValueChanged(this);
} }
else else
{ {
@ -174,7 +144,7 @@ namespace Perspex
/// Invoked when an entry in <see cref="Bindings"/> completes. /// Invoked when an entry in <see cref="Bindings"/> completes.
/// </summary> /// </summary>
/// <param name="entry">The entry that completed.</param> /// <param name="entry">The entry that completed.</param>
private void Completed(PriorityBindingEntry entry) public void Completed(PriorityBindingEntry entry)
{ {
Bindings.Remove(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> /// <summary>
/// Activates the first binding that has a value. /// Activates the first binding that has a value.
/// </summary> /// </summary>
@ -195,14 +175,14 @@ namespace Perspex
{ {
Value = binding.Value; Value = binding.Value;
ActiveBindingIndex = binding.Index; ActiveBindingIndex = binding.Index;
_changed(this); _owner.LevelValueChanged(this);
return; return;
} }
} }
Value = DirectValue; Value = DirectValue;
ActiveBindingIndex = -1; ActiveBindingIndex = -1;
_changed(this); _owner.LevelValueChanged(this);
} }
} }
} }

135
src/Perspex.Base/PriorityValue.cs

@ -4,8 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Subjects;
using System.Text; using System.Text;
using Perspex.Data;
using Perspex.Logging; using Perspex.Logging;
using Perspex.Utilities; using Perspex.Utilities;
@ -20,61 +20,33 @@ namespace Perspex
/// represent higher priorites. The current <see cref="Value"/> is selected from the highest /// 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 /// 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 /// 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 /// has a higher priority. Each time the value changes, the
/// fired with the old and new values. /// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the
/// owner object is fired with the old and new values.
/// </remarks> /// </remarks>
internal class PriorityValue internal class PriorityValue
{ {
/// <summary> private readonly IPriorityValueOwner _owner;
/// 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 Type _valueType; private readonly Type _valueType;
/// <summary>
/// The currently registered bindings organised by priority.
/// </summary>
private readonly Dictionary<int, PriorityLevel> _levels = new Dictionary<int, PriorityLevel>(); 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; private object _value;
/// <summary>
/// The function used to validate the value, if any.
/// </summary>
private readonly Func<object, object> _validate; private readonly Func<object, object> _validate;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class. /// Initializes a new instance of the <see cref="PriorityValue"/> class.
/// </summary> /// </summary>
/// <param name="owner">The owner of the object.</param> /// <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="valueType">The value type.</param>
/// <param name="validate">An optional validation function.</param> /// <param name="validate">An optional validation function.</param>
public PriorityValue( public PriorityValue(
PerspexObject owner, IPriorityValueOwner owner,
string name, PerspexProperty property,
Type valueType, Type valueType,
Func<object, object> validate = null) Func<object, object> validate = null)
{ {
_owner = owner; _owner = owner;
_name = name; Property = property;
_valueType = valueType; _valueType = valueType;
_value = PerspexProperty.UnsetValue; _value = PerspexProperty.UnsetValue;
ValuePriority = int.MaxValue; ValuePriority = int.MaxValue;
@ -82,12 +54,9 @@ namespace Perspex
} }
/// <summary> /// <summary>
/// Fired whenever the current <see cref="Value"/> changes. /// Gets the property that the value represents.
/// </summary> /// </summary>
/// <remarks> public PerspexProperty Property { get; }
/// 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;
/// <summary> /// <summary>
/// Gets the current value. /// Gets the current value.
@ -181,6 +150,51 @@ namespace Perspex
return b.ToString(); 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> /// <summary>
/// Causes a revalidation of the value. /// Causes a revalidation of the value.
/// </summary> /// </summary>
@ -209,8 +223,7 @@ namespace Perspex
if (!_levels.TryGetValue(priority, out result)) if (!_levels.TryGetValue(priority, out result))
{ {
var mode = (LevelPrecedenceMode)(priority % 2); result = new PriorityLevel(this, priority);
result = new PriorityLevel(priority, mode, ValueChanged);
_levels.Add(priority, result); _levels.Add(priority, result);
} }
@ -237,47 +250,19 @@ namespace Perspex
ValuePriority = priority; ValuePriority = priority;
_value = castValue; _value = castValue;
_changed.OnNext(Tuple.Create(old, _value)); _owner?.Changed(this, old, _value);
} }
else else
{ {
Logger.Error( Logger.Error(
LogArea.Property, LogArea.Binding,
_owner, _owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_name, Property.Name,
_valueType, _valueType,
value, value,
value.GetType()); 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; using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("Perspex.Base")] [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="source">The property to add the owner to.</param>
/// <param name="ownerType">The type of the class that registers the property.</param> /// <param name="ownerType">The type of the class that registers the property.</param>
protected StyledPropertyBase(StyledPropertyBase<TValue> source, Type ownerType) protected StyledPropertyBase(StyledPropertyBase<TValue> source, Type ownerType)
: base(source, ownerType) : base(source, ownerType, null)
{ {
_inherits = source.Inherits; _inherits = source.Inherits;
} }

2
src/Perspex.Controls/Canvas.cs

@ -226,7 +226,7 @@ namespace Perspex.Controls
private static void AffectsCanvasArrangeInvalidate(PerspexPropertyChangedEventArgs e) private static void AffectsCanvasArrangeInvalidate(PerspexPropertyChangedEventArgs e)
{ {
var control = e.Sender as IControl; var control = e.Sender as IControl;
var canvas = control?.Parent as Canvas; var canvas = control?.VisualParent as Canvas;
canvas?.InvalidateArrange(); canvas?.InvalidateArrange();
} }
} }

18
src/Perspex.Controls/Menu.cs

@ -7,6 +7,7 @@ using System.Reactive.Disposables;
using Perspex.Controls.Primitives; using Perspex.Controls.Primitives;
using Perspex.Controls.Templates; using Perspex.Controls.Templates;
using Perspex.Input; using Perspex.Input;
using Perspex.Input.Raw;
using Perspex.Interactivity; using Perspex.Interactivity;
using Perspex.LogicalTree; using Perspex.LogicalTree;
using Perspex.Rendering; using Perspex.Rendering;
@ -112,7 +113,8 @@ namespace Perspex.Controls
_subscription = new CompositeDisposable( _subscription = new CompositeDisposable(
pointerPress, pointerPress,
Disposable.Create(() => topLevel.Deactivated -= Deactivated)); Disposable.Create(() => topLevel.Deactivated -= Deactivated),
InputManager.Instance.Process.Subscribe(ListenForNonClientClick));
var inputRoot = e.Root as IInputRoot; var inputRoot = e.Root as IInputRoot;
@ -195,6 +197,20 @@ namespace Perspex.Controls
Close(); 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> /// <summary>
/// Called when a submenu is clicked somewhere in the menu. /// Called when a submenu is clicked somewhere in the menu.
/// </summary> /// </summary>

4
src/Perspex.Controls/Mixins/ContentControlMixin.cs

@ -74,7 +74,7 @@ namespace Perspex.Controls.Mixins
UpdateLogicalChild( UpdateLogicalChild(
sender, sender,
logicalChildren, logicalChildren,
logicalChildren.FirstOrDefault(), null,
presenter.GetValue(ContentPresenter.ChildProperty)); presenter.GetValue(ContentPresenter.ChildProperty));
subscriptions.Value.Add(sender, subscription); subscriptions.Value.Add(sender, subscription);
@ -143,7 +143,7 @@ namespace Perspex.Controls.Mixins
child = newValue as IControl; child = newValue as IControl;
if (child != null) if (child != null && !logicalChildren.Contains(child))
{ {
child.SetValue(Control.TemplatedParentProperty, control.TemplatedParent); child.SetValue(Control.TemplatedParentProperty, control.TemplatedParent);
logicalChildren.Add(child); 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.Primitives;
using Perspex.Controls.Templates; using Perspex.Controls.Templates;
using Perspex.Controls.Utils; using Perspex.Controls.Utils;
using Perspex.Data;
namespace Perspex.Controls.Presenters namespace Perspex.Controls.Presenters
{ {
@ -127,8 +128,22 @@ namespace Perspex.Controls.Presenters
/// </summary> /// </summary>
public int SelectedIndex public int SelectedIndex
{ {
get { return _selectedIndex; } get
set { SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); } {
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> /// <summary>

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

@ -12,24 +12,28 @@ namespace Perspex.Controls.Presenters
{ {
public class TextPresenter : TextBlock public class TextPresenter : TextBlock
{ {
public static readonly StyledProperty<int> CaretIndexProperty = public static readonly DirectProperty<TextPresenter, int> CaretIndexProperty =
TextBox.CaretIndexProperty.AddOwner<TextPresenter>(); TextBox.CaretIndexProperty.AddOwner<TextPresenter>(
o => o.CaretIndex,
(o, v) => o.CaretIndex = v);
public static readonly StyledProperty<int> SelectionStartProperty = public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(); TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly StyledProperty<int> SelectionEndProperty = public static readonly DirectProperty<TextPresenter, int> SelectionEndProperty =
TextBox.SelectionEndProperty.AddOwner<TextPresenter>(); TextBox.SelectionEndProperty.AddOwner<TextPresenter>(
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
private readonly DispatcherTimer _caretTimer; private readonly DispatcherTimer _caretTimer;
private int _caretIndex;
private int _selectionStart;
private int _selectionEnd;
private bool _caretBlink; private bool _caretBlink;
private IBrush _highlightBrush; private IBrush _highlightBrush;
static TextPresenter()
{
CaretIndexProperty.OverrideValidation<TextPresenter>((o, v) => v);
}
public TextPresenter() public TextPresenter()
{ {
_caretTimer = new DispatcherTimer(); _caretTimer = new DispatcherTimer();
@ -47,20 +51,44 @@ namespace Perspex.Controls.Presenters
public int CaretIndex public int CaretIndex
{ {
get { return GetValue(CaretIndexProperty); } get
set { SetValue(CaretIndexProperty, value); } {
return _caretIndex;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
}
} }
public int SelectionStart public int SelectionStart
{ {
get { return GetValue(SelectionStartProperty); } get
set { SetValue(SelectionStartProperty, value); } {
return _selectionStart;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
}
} }
public int SelectionEnd public int SelectionEnd
{ {
get { return GetValue(SelectionEndProperty); } get
set { SetValue(SelectionEndProperty, value); } {
return _selectionEnd;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
}
} }
public int GetCaretIndex(Point point) 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) private void CaretTimerTick(object sender, EventArgs e)
{ {
_caretBlink = !_caretBlink; _caretBlink = !_caretBlink;

15
src/Perspex.Controls/Primitives/Popup.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Perspex.Input; using Perspex.Input;
using Perspex.Input.Raw;
using Perspex.Interactivity; using Perspex.Interactivity;
using Perspex.LogicalTree; using Perspex.LogicalTree;
using Perspex.Metadata; using Perspex.Metadata;
@ -53,6 +54,7 @@ namespace Perspex.Controls.Primitives
private bool _isOpen; private bool _isOpen;
private PopupRoot _popupRoot; private PopupRoot _popupRoot;
private TopLevel _topLevel; private TopLevel _topLevel;
private IDisposable _nonClientListener;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="Popup"/> class. /// Initializes static members of the <see cref="Popup"/> class.
@ -181,6 +183,7 @@ namespace Perspex.Controls.Primitives
{ {
_topLevel.Deactivated += TopLevelDeactivated; _topLevel.Deactivated += TopLevelDeactivated;
_topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
_nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
} }
PopupRootCreated?.Invoke(this, EventArgs.Empty); PopupRootCreated?.Invoke(this, EventArgs.Empty);
@ -201,6 +204,8 @@ namespace Perspex.Controls.Primitives
{ {
_topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside); _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
_topLevel.Deactivated -= TopLevelDeactivated; _topLevel.Deactivated -= TopLevelDeactivated;
_nonClientListener?.Dispose();
_nonClientListener = null;
} }
_popupRoot.Hide(); _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) private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{ {
if (!StaysOpen) if (!StaysOpen)

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

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

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

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

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

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

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

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

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

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

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

@ -9,7 +9,7 @@ namespace Perspex.Controls.Shapes
{ {
static Rectangle() static Rectangle()
{ {
AffectsGeometry(BoundsProperty, StrokeThicknessProperty); AffectsGeometry<Rectangle>(BoundsProperty, StrokeThicknessProperty);
} }
protected override Geometry CreateDefiningGeometry() 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Reflection;
using Perspex.Collections; using Perspex.Collections;
using Perspex.Controls; using Perspex.Controls;
using Perspex.Media; 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 /// 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. /// property will cause <see cref="InvalidateGeometry"/> to be called on the element.
/// </remarks> /// </remarks>
protected static void AffectsGeometry(params PerspexProperty[] properties) protected static void AffectsGeometry<TShape>(params PerspexProperty[] properties)
where TShape : Shape
{ {
foreach (var property in properties) 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> /// <summary>
/// Defines the <see cref="Text"/> property. /// Defines the <see cref="Text"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<string> TextProperty = public static readonly DirectProperty<TextBlock, string> TextProperty =
PerspexProperty.Register<TextBlock, string>(nameof(Text)); PerspexProperty.RegisterDirect<TextBlock, string>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
/// <summary> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
@ -87,14 +90,8 @@ namespace Perspex.Controls
public static readonly StyledProperty<TextWrapping> TextWrappingProperty = public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
PerspexProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping)); PerspexProperty.Register<TextBlock, TextWrapping>(nameof(TextWrapping));
/// <summary> private string _text;
/// The formatted text used for rendering.
/// </summary>
private FormattedText _formattedText; private FormattedText _formattedText;
/// <summary>
/// Stores the last constraint passed to MeasureOverride.
/// </summary>
private Size _constraint; private Size _constraint;
/// <summary> /// <summary>
@ -140,8 +137,8 @@ namespace Perspex.Controls
[Content] [Content]
public string Text public string Text
{ {
get { return GetValue(TextProperty); } get { return _text; }
set { SetValue(TextProperty, value); } set { SetAndRaise(TextProperty, ref _text, value); }
} }
/// <summary> /// <summary>

81
src/Perspex.Controls/TextBox.cs

@ -29,18 +29,29 @@ namespace Perspex.Controls
public static readonly DirectProperty<TextBox, bool> CanScrollHorizontallyProperty = public static readonly DirectProperty<TextBox, bool> CanScrollHorizontallyProperty =
PerspexProperty.RegisterDirect<TextBox, bool>("CanScrollHorizontally", o => o.CanScrollHorizontally); PerspexProperty.RegisterDirect<TextBox, bool>("CanScrollHorizontally", o => o.CanScrollHorizontally);
// TODO: Should CaretIndex, SelectionStart/End and Text be direct properties? public static readonly DirectProperty<TextBox, int> CaretIndexProperty =
public static readonly StyledProperty<int> CaretIndexProperty = PerspexProperty.RegisterDirect<TextBox, int>(
PerspexProperty.Register<TextBox, int>("CaretIndex", validate: ValidateCaretIndex); nameof(CaretIndex),
o => o.CaretIndex,
public static readonly StyledProperty<int> SelectionStartProperty = (o, v) => o.CaretIndex = v);
PerspexProperty.Register<TextBox, int>("SelectionStart", validate: ValidateCaretIndex);
public static readonly DirectProperty<TextBox, int> SelectionStartProperty =
public static readonly StyledProperty<int> SelectionEndProperty = PerspexProperty.RegisterDirect<TextBox, int>(
PerspexProperty.Register<TextBox, int>("SelectionEnd", validate: ValidateCaretIndex); nameof(SelectionStart),
o => o.SelectionStart,
public static readonly StyledProperty<string> TextProperty = (o, v) => o.SelectionStart = v);
TextBlock.TextProperty.AddOwner<TextBox>();
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 = public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
TextBlock.TextAlignmentProperty.AddOwner<TextBox>(); TextBlock.TextAlignmentProperty.AddOwner<TextBox>();
@ -71,6 +82,10 @@ namespace Perspex.Controls
public bool Equals(UndoRedoState other) => ReferenceEquals(Text, other.Text) || Equals(Text, other.Text); 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 bool _canScrollHorizontally;
private TextPresenter _presenter; private TextPresenter _presenter;
private UndoRedoHelper<UndoRedoState> _undoRedoHelper; private UndoRedoHelper<UndoRedoState> _undoRedoHelper;
@ -78,7 +93,6 @@ namespace Perspex.Controls
static TextBox() static TextBox()
{ {
FocusableProperty.OverrideDefaultValue(typeof(TextBox), true); FocusableProperty.OverrideDefaultValue(typeof(TextBox), true);
TextProperty.OverrideMetadata<TextBox>(new StyledPropertyMetadata<string>(defaultBindingMode: BindingMode.TwoWay));
} }
public TextBox() public TextBox()
@ -117,10 +131,15 @@ namespace Perspex.Controls
public int CaretIndex public int CaretIndex
{ {
get { return GetValue(CaretIndexProperty); } get
{
return _caretIndex;
}
set set
{ {
SetValue(CaretIndexProperty, value); value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
if (_undoRedoHelper.IsLastState && _undoRedoHelper.LastState.Text == Text) if (_undoRedoHelper.IsLastState && _undoRedoHelper.LastState.Text == Text)
_undoRedoHelper.UpdateLastState(); _undoRedoHelper.UpdateLastState();
} }
@ -128,21 +147,37 @@ namespace Perspex.Controls
public int SelectionStart public int SelectionStart
{ {
get { return GetValue(SelectionStartProperty); } get
set { SetValue(SelectionStartProperty, value); } {
return _selectionStart;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionStartProperty, ref _selectionStart, value);
}
} }
public int SelectionEnd public int SelectionEnd
{ {
get { return GetValue(SelectionEndProperty); } get
set { SetValue(SelectionEndProperty, value); } {
return _selectionEnd;
}
set
{
value = CoerceCaretIndex(value);
SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
}
} }
[Content] [Content]
public string Text public string Text
{ {
get { return GetValue(TextProperty); } get { return _text; }
set { SetValue(TextProperty, value); } set { SetAndRaise(TextProperty, ref _text, value); }
} }
public TextAlignment TextAlignment 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; var length = text?.Length ?? 0;
return Math.Max(0, Math.Min(length, value)); return Math.Max(0, Math.Min(length, value));
} }

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

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

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

@ -14,6 +14,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
TextBlock.Foreground="{TemplateBinding Foreground}" TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"

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

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

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

@ -5,7 +5,8 @@
Background="{TemplateBinding Background}" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/> Padding="{TemplateBinding Padding}"/>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>

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

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

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

@ -10,6 +10,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="{TemplateBinding Padding}"/> Padding="{TemplateBinding Padding}"/>

4
src/Perspex.Themes.Default/Expander.xaml

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

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

@ -6,6 +6,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/> Padding="{TemplateBinding Padding}"/>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>

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

@ -7,6 +7,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/> Padding="{TemplateBinding Padding}"/>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>

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

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

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

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

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

@ -9,6 +9,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/> Padding="{TemplateBinding Padding}"/>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>

2
src/Perspex.Themes.Default/TextBox.xaml

@ -40,7 +40,7 @@
CaretIndex="{TemplateBinding CaretIndex}" CaretIndex="{TemplateBinding CaretIndex}"
SelectionStart="{TemplateBinding SelectionStart}" SelectionStart="{TemplateBinding SelectionStart}"
SelectionEnd="{TemplateBinding SelectionEnd}" SelectionEnd="{TemplateBinding SelectionEnd}"
Text="{TemplateBinding Text}" Text="{TemplateBinding Text, Mode=TwoWay}"
TextAlignment="{TemplateBinding TextAlignment}" TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"/> TextWrapping="{TemplateBinding TextWrapping}"/>
</Panel> </Panel>

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

@ -14,6 +14,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
TextBlock.Foreground="{TemplateBinding Foreground}" TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"

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

@ -10,6 +10,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}" Content="{TemplateBinding Content}"
DataContext="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/> Padding="{TemplateBinding Padding}"/>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>

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

@ -13,6 +13,7 @@
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Header}" Content="{TemplateBinding Header}"
DataContext="{TemplateBinding Header}"
Padding="{TemplateBinding Padding}" Padding="{TemplateBinding Padding}"
TemplatedControl.IsTemplateFocusTarget="True" TemplatedControl.IsTemplateFocusTarget="True"
Grid.Column="1"/> Grid.Column="1"/>

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

@ -6,7 +6,10 @@
<ControlTemplate> <ControlTemplate>
<Border Background="{TemplateBinding Background}"> <Border Background="{TemplateBinding Background}">
<AdornerDecorator> <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> </AdornerDecorator>
</Border> </Border>
</ControlTemplate> </ControlTemplate>

29
src/Windows/Perspex.Win32/WindowImpl.cs

@ -436,20 +436,6 @@ namespace Perspex.Win32
break; 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_LBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_RBUTTONDOWN:
case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN: case UnmanagedMethods.WindowsMessage.WM_MBUTTONDOWN:
@ -522,6 +508,21 @@ namespace Perspex.Win32
new Point(), WindowsKeyboardDevice.Instance.Modifiers); new Point(), WindowsKeyboardDevice.Instance.Modifiers);
break; 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: case UnmanagedMethods.WindowsMessage.WM_PAINT:
if (Paint != null) if (Paint != null)
{ {

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

@ -44,6 +44,10 @@
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" /> <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" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <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> <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> <HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </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"> <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> <HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
</Reference> </Reference>
@ -124,6 +124,10 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project> <Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Perspex.Base</Name> <Name>Perspex.Base</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\Perspex.UnitTests\Perspex.UnitTests.csproj">
<Project>{88060192-33d5-4932-b0f9-8bd2763e857d}</Project>
<Name>Perspex.UnitTests</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<Choose> <Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'"> <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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Microsoft.Reactive.Testing; using Microsoft.Reactive.Testing;
using Perspex.Data; using Perspex.Data;
using Perspex.Logging;
using Perspex.UnitTests;
using Xunit; using Xunit;
namespace Perspex.Base.UnitTests namespace Perspex.Base.UnitTests
@ -263,6 +267,61 @@ namespace Perspex.Base.UnitTests
Assert.Equal("first", target2.GetValue(Class1.FooProperty)); 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> /// <summary>
/// Returns an observable that returns a single value but does not complete. /// Returns an observable that returns a single value but does not complete.
/// </summary> /// </summary>

59
tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Perspex.Data; using Perspex.Data;
using Perspex.Logging;
using Perspex.UnitTests;
using Xunit; using Xunit;
namespace Perspex.Base.UnitTests namespace Perspex.Base.UnitTests
@ -394,6 +396,63 @@ namespace Perspex.Base.UnitTests
Assert.True(raised); 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 private class Class1 : PerspexObject
{ {
public static readonly DirectProperty<Class1, string> FooProperty = 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.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Moq;
using Xunit; using Xunit;
namespace Perspex.Base.UnitTests namespace Perspex.Base.UnitTests
{ {
public class PriorityValueTests public class PriorityValueTests
{ {
private static readonly PerspexProperty TestProperty =
new StyledProperty<string>(
"Test",
typeof(PriorityValueTests),
new StyledPropertyMetadata<string>());
[Fact] [Fact]
public void Initial_Value_Should_Be_UnsetValue() 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); Assert.Same(PerspexProperty.UnsetValue, target.Value);
} }
@ -22,7 +29,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void First_Binding_Sets_Value() 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); target.Add(Single("foo"), 0);
@ -32,7 +39,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Changing_Binding_Should_Set_Value() 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"); var subject = new BehaviorSubject<string>("foo");
target.Add(subject, 0); target.Add(subject, 0);
@ -44,7 +51,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Setting_Direct_Value_Should_Override_Binding() 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.Add(Single("foo"), 0);
target.SetValue("bar", 0); target.SetValue("bar", 0);
@ -55,7 +62,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Binding_Firing_Should_Override_Direct_Value() 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"); var source = new BehaviorSubject<object>("initial");
target.Add(source, 0); target.Add(source, 0);
@ -67,25 +74,9 @@ namespace Perspex.Base.UnitTests
} }
[Fact] [Fact]
public void Earlier_Binding_Firing_Should_Override_Later_Priority_0() public void Earlier_Binding_Firing_Should_Not_Override_Later()
{
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()
{ {
var target = new PriorityValue(null, "Test", typeof(string)); var target = new PriorityValue(null, TestProperty, typeof(string));
var nonActive = new BehaviorSubject<object>("na"); var nonActive = new BehaviorSubject<object>("na");
var source = new BehaviorSubject<object>("initial"); var source = new BehaviorSubject<object>("initial");
@ -101,7 +92,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Binding_Completing_Should_Revert_To_Direct_Value() 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"); var source = new BehaviorSubject<object>("initial");
target.Add(source, 0); target.Add(source, 0);
@ -117,7 +108,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Binding_With_Lower_Priority_Has_Precedence() 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("foo"), 1);
target.Add(Single("bar"), 0); target.Add(Single("bar"), 0);
@ -129,7 +120,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Later_Binding_With_Same_Priority_Should_Take_Precedence() 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("foo"), 1);
target.Add(Single("bar"), 0); target.Add(Single("bar"), 0);
@ -142,7 +133,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value() 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"); var subject = new BehaviorSubject<string>("bar");
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -155,7 +146,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void UnsetValue_Should_Fall_Back_To_Next_Binding() 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"); var subject = new BehaviorSubject<object>("bar");
target.Add(subject, 0); target.Add(subject, 0);
@ -171,33 +162,31 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Adding_Value_Should_Call_OnNext() public void Adding_Value_Should_Call_OnNext()
{ {
var target = new PriorityValue(null, "Test", typeof(string)); var owner = new Mock<IPriorityValueOwner>();
bool called = false; 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); target.Add(Single("foo"), 0);
Assert.True(called); owner.Verify(x => x.Changed(target, PerspexProperty.UnsetValue, "foo"));
} }
[Fact] [Fact]
public void Changing_Value_Should_Call_OnNext() 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"); var subject = new BehaviorSubject<object>("foo");
bool called = false;
target.Add(subject, 0); target.Add(subject, 0);
target.Changed.Subscribe(value => called = (string)value.Item1 == "foo" && (string)value.Item2 == "bar");
subject.OnNext("bar"); subject.OnNext("bar");
Assert.True(called); owner.Verify(x => x.Changed(target, "foo", "bar"));
} }
[Fact] [Fact]
public void Disposing_A_Binding_Should_Revert_To_Next_Value() 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); target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0); var disposable = target.Add(Single("bar"), 0);
@ -210,7 +199,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Disposing_A_Binding_Should_Remove_BindingEntry() 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); target.Add(Single("foo"), 0);
var disposable = target.Add(Single("bar"), 0); var disposable = target.Add(Single("bar"), 0);
@ -223,7 +212,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Completing_A_Binding_Should_Revert_To_Previous_Binding() 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"); var source = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -237,7 +226,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Completing_A_Binding_Should_Revert_To_Lower_Priority() 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"); var source = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 1); target.Add(Single("foo"), 1);
@ -251,7 +240,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Completing_A_Binding_Should_Remove_BindingEntry() 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"); var subject = new BehaviorSubject<object>("bar");
target.Add(Single("foo"), 0); target.Add(Single("foo"), 0);
@ -265,7 +254,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Direct_Value_Should_Be_Coerced() 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); target.SetValue(5, 0);
Assert.Equal(5, target.Value); Assert.Equal(5, target.Value);
@ -276,7 +265,7 @@ namespace Perspex.Base.UnitTests
[Fact] [Fact]
public void Bound_Value_Should_Be_Coerced() 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>(); var source = new Subject<object>();
target.Add(source, 0); target.Add(source, 0);
@ -290,7 +279,7 @@ namespace Perspex.Base.UnitTests
public void Revalidate_Should_ReCoerce_Value() public void Revalidate_Should_ReCoerce_Value()
{ {
var max = 10; 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>(); var source = new Subject<object>();
target.Add(source, 0); target.Add(source, 0);

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

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Moq" version="4.2.1510.2205" targetFramework="net45" />
<package id="Rx-Core" version="2.2.5" 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-Interfaces" version="2.2.5" targetFramework="net45" />
<package id="Rx-Linq" 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.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Microsoft.Reactive.Testing; using Microsoft.Reactive.Testing;
using Perspex.Data;
using Perspex.Markup.Data; using Perspex.Markup.Data;
using Perspex.UnitTests; using Perspex.UnitTests;
using Xunit; using Xunit;
@ -74,13 +75,17 @@ namespace Perspex.Markup.UnitTests.Data
} }
[Fact] [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 data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1); 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] [Fact]
@ -209,7 +214,10 @@ namespace Perspex.Markup.UnitTests.Data
data.Next = breaking; data.Next = breaking;
data.Next = new Class2 { Bar = "baz" }; 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(); sub.Dispose();

24
tests/Perspex.Markup.UnitTests/Data/ExpressionSubjectTests.cs

@ -6,6 +6,7 @@ using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Reactive.Linq; using System.Reactive.Linq;
using Moq; using Moq;
using Perspex.Data;
using Perspex.Markup.Data; using Perspex.Markup.Data;
using Xunit; using Xunit;
@ -47,13 +48,13 @@ namespace Perspex.Markup.UnitTests.Data
} }
[Fact] [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 data = new Class1 { StringValue = "foo" };
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double)); var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1); var result = await target.Take(1);
Assert.Equal(PerspexProperty.UnsetValue, result); Assert.IsType<BindingError>(result);
} }
[Fact] [Fact]
@ -105,14 +106,29 @@ namespace Perspex.Markup.UnitTests.Data
} }
[Fact] [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 data = new Class1 { DoubleValue = 5.6 };
var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string)); var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
target.OnNext("foo"); 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] [Fact]

3
tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs

@ -3,6 +3,7 @@
using System.Globalization; using System.Globalization;
using Perspex.Controls; using Perspex.Controls;
using Perspex.Data;
using Xunit; using Xunit;
namespace Perspex.Markup.UnitTests namespace Perspex.Markup.UnitTests
@ -114,7 +115,7 @@ namespace Perspex.Markup.UnitTests
null, null,
CultureInfo.InvariantCulture); CultureInfo.InvariantCulture);
Assert.Equal(PerspexProperty.UnsetValue, result); Assert.IsType<BindingError>(result);
} }
private enum TestEnum private enum TestEnum

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

@ -104,13 +104,14 @@
<Compile Include="StyleTests.cs" /> <Compile Include="StyleTests.cs" />
<Compile Include="Templates\DataTemplateTests.cs" /> <Compile Include="Templates\DataTemplateTests.cs" />
<Compile Include="Xaml\BasicTests.cs" /> <Compile Include="Xaml\BasicTests.cs" />
<Compile Include="Xaml\ControlBindingTests.cs" />
<Compile Include="Xaml\BindingTests.cs" /> <Compile Include="Xaml\BindingTests.cs" />
<Compile Include="Xaml\DataTemplateTests.cs" /> <Compile Include="Xaml\DataTemplateTests.cs" />
<Compile Include="Xaml\InitializationOrderTracker.cs" /> <Compile Include="Xaml\InitializationOrderTracker.cs" />
<Compile Include="Xaml\NonControl.cs" /> <Compile Include="Xaml\NonControl.cs" />
<Compile Include="Xaml\TreeDataTemplateTests.cs" /> <Compile Include="Xaml\TreeDataTemplateTests.cs" />
<Compile Include="TypeProviderMock.cs" /> <Compile Include="TypeProviderMock.cs" />
<Compile Include="ViewModelMock.cs" /> <Compile Include="TestViewModel.cs" />
<Compile Include="Xaml\StyleTests.cs" /> <Compile Include="Xaml\StyleTests.cs" />
</ItemGroup> </ItemGroup>
<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); 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.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
namespace Perspex.UnitTests namespace Perspex.UnitTests
{ {
@ -34,7 +35,7 @@ namespace Perspex.UnitTests
private set; private set;
} }
public void RaisePropertyChanged(string propertyName) public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{ {
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }

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

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