Browse Source

Merge pull request #3287 from AvaloniaUI/refactor/wpf-validation-coercion

Switch to WPF-styled property validation/coercion
pull/3255/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
ab26c550e9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      src/Avalonia.Base/AttachedProperty.cs
  2. 10
      src/Avalonia.Base/AvaloniaObject.cs
  3. 30
      src/Avalonia.Base/AvaloniaProperty.cs
  4. 28
      src/Avalonia.Base/BoxedValue.cs
  5. 7
      src/Avalonia.Base/IAvaloniaObject.cs
  6. 5
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  7. 14
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  8. 4
      src/Avalonia.Base/StyledProperty.cs
  9. 49
      src/Avalonia.Base/StyledPropertyBase.cs
  10. 29
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  11. 32
      src/Avalonia.Base/ValueStore.cs
  12. 121
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  13. 22
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  14. 43
      src/Avalonia.Controls/AutoCompleteBox.cs
  15. 16
      src/Avalonia.Controls/Calendar/Calendar.cs
  16. 30
      src/Avalonia.Controls/Calendar/DatePicker.cs
  17. 16
      src/Avalonia.Controls/DefinitionBase.cs
  18. 32
      src/Avalonia.Controls/Grid.cs
  19. 33
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  20. 155
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs
  21. 97
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
  22. 50
      tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs

8
src/Avalonia.Base/AttachedProperty.cs

@ -18,12 +18,14 @@ namespace Avalonia
/// <param name="ownerType">The class that is registering the property.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="validate">A value validation callback.</param>
public AttachedProperty(
string name,
Type ownerType,
Type ownerType,
StyledPropertyMetadata<TValue> metadata,
bool inherits = false)
: base(name, ownerType, metadata, inherits)
bool inherits = false,
Func<TValue, bool> validate = null)
: base(name, ownerType, metadata, inherits, validate)
{
}

10
src/Avalonia.Base/AvaloniaObject.cs

@ -413,6 +413,16 @@ namespace Avalonia
return new DirectBindingSubscription<T>(this, property, source);
}
/// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
public void CoerceValue<T>(StyledPropertyBase<T> property)
{
_values?.CoerceValue(property);
}
/// <inheritdoc/>
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{

30
src/Avalonia.Base/AvaloniaProperty.cs

@ -257,6 +257,8 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <param name="notifying">
/// A method that gets called before and after the property starts being notified on an
/// object; the bool argument will be true before and false afterwards. This callback is
@ -268,6 +270,8 @@ namespace Avalonia
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<TValue, bool> validate = null,
Func<IAvaloniaObject, TValue, TValue> coerce = null,
Action<IAvaloniaObject, bool> notifying = null)
where TOwner : IAvaloniaObject
{
@ -275,13 +279,15 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
defaultBindingMode: defaultBindingMode);
defaultBindingMode: defaultBindingMode,
coerce: coerce);
var result = new StyledProperty<TValue>(
name,
typeof(TOwner),
metadata,
inherits,
validate,
notifying);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
@ -297,21 +303,26 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static AttachedProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
string name,
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay)
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<TValue, bool> validate = null,
Func<IAvaloniaObject, TValue, TValue> coerce = null)
where THost : IAvaloniaObject
{
Contract.Requires<ArgumentNullException>(name != null);
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
defaultBindingMode: defaultBindingMode);
defaultBindingMode: defaultBindingMode,
coerce: coerce);
var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits);
var result = new AttachedProperty<TValue>(name, typeof(TOwner), metadata, inherits, validate);
var registry = AvaloniaPropertyRegistry.Instance;
registry.Register(typeof(TOwner), result);
registry.RegisterAttached(typeof(THost), result);
@ -328,22 +339,27 @@ namespace Avalonia
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="coerce">A value coercion callback.</param>
/// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static AttachedProperty<TValue> RegisterAttached<THost, TValue>(
string name,
Type ownerType,
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay)
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<TValue, bool> validate = null,
Func<IAvaloniaObject, TValue, TValue> coerce = null)
where THost : IAvaloniaObject
{
Contract.Requires<ArgumentNullException>(name != null);
var metadata = new StyledPropertyMetadata<TValue>(
defaultValue,
defaultBindingMode: defaultBindingMode);
defaultBindingMode: defaultBindingMode,
coerce: coerce);
var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits);
var result = new AttachedProperty<TValue>(name, ownerType, metadata, inherits, validate);
var registry = AvaloniaPropertyRegistry.Instance;
registry.Register(ownerType, result);
registry.RegisterAttached(typeof(THost), result);

28
src/Avalonia.Base/BoxedValue.cs

@ -1,28 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia
{
/// <summary>
/// Represents boxed value of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">Type of stored value.</typeparam>
internal readonly struct BoxedValue<T>
{
public BoxedValue(T value)
{
Boxed = value;
Typed = value;
}
/// <summary>
/// Boxed value.
/// </summary>
public object Boxed { get; }
/// <summary>
/// Typed value.
/// </summary>
public T Typed { get; }
}
}

7
src/Avalonia.Base/IAvaloniaObject.cs

@ -106,6 +106,13 @@ namespace Avalonia
DirectPropertyBase<T> property,
IObservable<BindingValue<T>> source);
/// <summary>
/// Coerces the specified <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
void CoerceValue<T>(StyledPropertyBase<T> property);
/// <summary>
/// Registers an object as an inheritance child.
/// </summary>

5
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@ -81,6 +81,11 @@ namespace Avalonia.PropertyStore
private void UpdateValue(BindingValue<T> value)
{
if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
{
value = Property.GetDefaultValue(_owner.GetType());
}
if (value.Type == BindingValueType.DoNothing)
{
return;

14
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@ -21,6 +21,7 @@ namespace Avalonia.PropertyStore
{
private readonly IValueSink _sink;
private readonly List<IPriorityValueEntry<T>> _entries = new List<IPriorityValueEntry<T>>();
private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
private Optional<T> _localValue;
public PriorityValue(
@ -29,6 +30,12 @@ namespace Avalonia.PropertyStore
{
Property = property;
_sink = sink;
if (property.HasCoercion)
{
var metadata = property.GetMetadata(owner.GetType());
_coerceValue = metadata.CoerceValue;
}
}
public PriorityValue(
@ -89,6 +96,8 @@ namespace Avalonia.PropertyStore
return binding;
}
public void CoerceValue() => UpdateEffectiveValue();
void IValueSink.ValueChanged<TValue>(
StyledPropertyBase<TValue> property,
BindingPriority priority,
@ -162,6 +171,11 @@ namespace Avalonia.PropertyStore
ValuePriority = BindingPriority.LocalValue;
}
if (value.HasValue && _coerceValue != null)
{
value = _coerceValue(_owner, value.Value);
}
if (value != Value)
{
var old = Value;

4
src/Avalonia.Base/StyledProperty.cs

@ -17,14 +17,16 @@ namespace Avalonia
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="notifying">A <see cref="AvaloniaProperty.Notifying"/> callback.</param>
public StyledProperty(
string name,
Type ownerType,
StyledPropertyMetadata<TValue> metadata,
bool inherits = false,
Func<TValue, bool> validate = null,
Action<IAvaloniaObject, bool> notifying = null)
: base(name, ownerType, metadata, inherits, notifying)
: base(name, ownerType, metadata, inherits, validate, notifying)
{
}

49
src/Avalonia.Base/StyledPropertyBase.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Data;
using Avalonia.Reactive;
@ -23,12 +22,14 @@ namespace Avalonia
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="metadata">The property metadata.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="validate">A value validation callback.</param>
/// <param name="notifying">A <see cref="AvaloniaProperty.Notifying"/> callback.</param>
protected StyledPropertyBase(
string name,
Type ownerType,
StyledPropertyMetadata<TValue> metadata,
bool inherits = false,
Func<TValue, bool> validate = null,
Action<IAvaloniaObject, bool> notifying = null)
: base(name, ownerType, metadata, notifying)
{
@ -41,6 +42,14 @@ namespace Avalonia
}
_inherits = inherits;
ValidateValue = validate;
HasCoercion |= metadata.CoerceValue != null;
if (validate?.Invoke(metadata.DefaultValue) == false)
{
throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{name}'.");
}
}
/// <summary>
@ -62,6 +71,29 @@ namespace Avalonia
/// </value>
public override bool Inherits => _inherits;
/// <summary>
/// Gets the value validation callback for the property.
/// </summary>
public Func<TValue, bool> ValidateValue { get; }
/// <summary>
/// Gets a value indicating whether this property has any value coercion callbacks defined
/// in its metadata.
/// </summary>
internal bool HasCoercion { get; private set; }
public TValue CoerceValue(IAvaloniaObject instance, TValue baseValue)
{
var metadata = GetMetadata(instance.GetType());
if (metadata.CoerceValue != null)
{
return metadata.CoerceValue.Invoke(instance, baseValue);
}
return baseValue;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
@ -71,7 +103,7 @@ namespace Avalonia
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue.Typed;
return GetMetadata(type).DefaultValue;
}
/// <summary>
@ -123,6 +155,17 @@ namespace Avalonia
/// <param name="metadata">The metadata.</param>
public void OverrideMetadata(Type type, StyledPropertyMetadata<TValue> metadata)
{
if (ValidateValue != null)
{
if (!ValidateValue(metadata.DefaultValue))
{
throw new ArgumentException(
$"'{metadata.DefaultValue}' is not a valid default value for '{Name}'.");
}
}
HasCoercion |= metadata.CoerceValue != null;
base.OverrideMetadata(type, metadata);
}
@ -209,7 +252,7 @@ namespace Avalonia
{
Contract.Requires<ArgumentNullException>(type != null);
return GetMetadata(type).DefaultValue.Boxed;
return GetMetadata(type).DefaultValue;
}
[DebuggerHidden]

29
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@ -12,25 +12,35 @@ namespace Avalonia
/// </summary>
public class StyledPropertyMetadata<TValue> : PropertyMetadata, IStyledPropertyMetadata
{
private Optional<TValue> _defaultValue;
/// <summary>
/// Initializes a new instance of the <see cref="StyledPropertyMetadata{TValue}"/> class.
/// </summary>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="defaultBindingMode">The default binding mode.</param>
/// <param name="coerce">A value coercion callback.</param>
public StyledPropertyMetadata(
TValue defaultValue = default,
BindingMode defaultBindingMode = BindingMode.Default)
Optional<TValue> defaultValue = default,
BindingMode defaultBindingMode = BindingMode.Default,
Func<IAvaloniaObject, TValue, TValue> coerce = null)
: base(defaultBindingMode)
{
DefaultValue = new BoxedValue<TValue>(defaultValue);
_defaultValue = defaultValue;
CoerceValue = coerce;
}
/// <summary>
/// Gets the default value for the property.
/// </summary>
internal BoxedValue<TValue> DefaultValue { get; private set; }
public TValue DefaultValue => _defaultValue.GetValueOrDefault();
/// <summary>
/// Gets the value coercion callback, if any.
/// </summary>
public Func<IAvaloniaObject, TValue, TValue>? CoerceValue { get; private set; }
object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
object IStyledPropertyMetadata.DefaultValue => DefaultValue;
/// <inheritdoc/>
public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
@ -39,9 +49,14 @@ namespace Avalonia
if (baseMetadata is StyledPropertyMetadata<TValue> src)
{
if (DefaultValue.Boxed == null)
if (!_defaultValue.HasValue)
{
_defaultValue = src.DefaultValue;
}
if (CoerceValue == null)
{
DefaultValue = src.DefaultValue;
CoerceValue = src.CoerceValue;
}
}
}

32
src/Avalonia.Base/ValueStore.cs

@ -72,10 +72,22 @@ namespace Avalonia
public void SetValue<T>(StyledPropertyBase<T> property, T value, BindingPriority priority)
{
if (property.ValidateValue?.Invoke(value) == false)
{
throw new ArgumentException($"{value} is not a valid value for '{property.Name}.");
}
if (_values.TryGetValue(property, out var slot))
{
SetExisting(slot, property, value, priority);
}
else if (property.HasCoercion)
{
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this);
_values.AddValue(property, entry);
entry.SetValue(value, priority);
}
else if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry<T>(value));
@ -98,6 +110,15 @@ namespace Avalonia
{
return BindExisting(slot, property, source, priority);
}
else if (property.HasCoercion)
{
// If the property has any coercion callbacks then always create a PriorityValue.
var entry = new PriorityValue<T>(_owner, property, this);
var binding = entry.AddBinding(source, priority);
_values.AddValue(property, entry);
binding.Start();
return binding;
}
else
{
var entry = new BindingEntry<T>(property, source, priority, this);
@ -135,6 +156,17 @@ namespace Avalonia
}
}
public void CoerceValue<T>(StyledPropertyBase<T> property)
{
if (_values.TryGetValue(property, out var slot))
{
if (slot is PriorityValue<T> p)
{
p.CoerceValue();
}
}
}
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))

121
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -201,21 +201,13 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> ColumnHeaderHeightProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(ColumnHeaderHeight),
defaultValue: double.NaN/*,
validate: ValidateColumnHeaderHeight*/);
defaultValue: double.NaN,
validate: IsValidColumnHeaderHeight);
private static double ValidateColumnHeaderHeight(DataGrid grid, double value)
private static bool IsValidColumnHeaderHeight(double value)
{
if (value < DATAGRID_minimumColumnHeaderHeight)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_minimumColumnHeaderHeight);
}
if (value > DATAGRID_maxHeadersThickness)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(ColumnHeaderHeight), DATAGRID_maxHeadersThickness);
}
return value;
return double.IsNaN(value) ||
(value >= DATAGRID_minimumColumnHeaderHeight && value <= DATAGRID_maxHeadersThickness);
}
/// <summary>
@ -261,8 +253,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<int> FrozenColumnCountProperty =
AvaloniaProperty.Register<DataGrid, int>(
nameof(FrozenColumnCount)/*,
validate: ValidateFrozenColumnCount*/);
nameof(FrozenColumnCount),
validate: ValidateFrozenColumnCount);
/// <summary>
/// Gets or sets the number of columns that the user cannot scroll horizontally.
@ -273,15 +265,7 @@ namespace Avalonia.Controls
set { SetValue(FrozenColumnCountProperty, value); }
}
private static int ValidateFrozenColumnCount(DataGrid grid, int value)
{
if (value < 0)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0);
}
return value;
}
private static bool ValidateFrozenColumnCount(int value) => value >= 0;
public static readonly StyledProperty<DataGridGridLinesVisibility> GridLinesVisibilityProperty =
AvaloniaProperty.Register<DataGrid, DataGridGridLinesVisibility>(nameof(GridLinesVisibility));
@ -395,30 +379,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> MaxColumnWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(MaxColumnWidth),
defaultValue: DATAGRID_defaultMaxColumnWidth/*,
validate: ValidateMaxColumnWidth*/);
defaultValue: DATAGRID_defaultMaxColumnWidth,
validate: IsValidColumnWidth);
private static double ValidateMaxColumnWidth(DataGrid grid, double value)
private static bool IsValidColumnWidth(double value)
{
if (double.IsNaN(value))
{
throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MaxColumnWidth));
}
if (value < 0)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), 0);
}
if (grid.MinColumnWidth > value)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MaxColumnWidth), nameof(MinColumnWidth));
}
if (value < 0)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(FrozenColumnCount), 0);
}
return value;
return !double.IsNaN(value) && value > 0;
}
/// <summary>
@ -433,29 +399,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> MinColumnWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(MinColumnWidth),
defaultValue: DATAGRID_defaultMinColumnWidth/*,
validate: ValidateMinColumnWidth*/);
defaultValue: DATAGRID_defaultMinColumnWidth,
validate: IsValidMinColumnWidth);
private static double ValidateMinColumnWidth(DataGrid grid, double value)
private static bool IsValidMinColumnWidth(double value)
{
if (double.IsNaN(value))
{
throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(MinColumnWidth));
}
if (value < 0)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(MinColumnWidth), 0);
}
if (double.IsPositiveInfinity(value))
{
throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(MinColumnWidth));
}
if (grid.MaxColumnWidth < value)
{
throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(MinColumnWidth), nameof(MaxColumnWidth));
}
return value;
return !double.IsNaN(value) && !double.IsPositiveInfinity(value) && value >= 0;
}
/// <summary>
@ -482,20 +431,13 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> RowHeightProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(RowHeight),
defaultValue: double.NaN/*,
validate: ValidateRowHeight*/);
private static double ValidateRowHeight(DataGrid grid, double value)
defaultValue: double.NaN,
validate: IsValidRowHeight);
private static bool IsValidRowHeight(double value)
{
if (value < DataGridRow.DATAGRIDROW_minimumHeight)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeight), 0);
}
if (value > DataGridRow.DATAGRIDROW_maximumHeight)
{
throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeight), DataGridRow.DATAGRIDROW_maximumHeight);
}
return value;
return double.IsNaN(value) ||
(value >= DataGridRow.DATAGRIDROW_minimumHeight &&
value <= DataGridRow.DATAGRIDROW_maximumHeight);
}
/// <summary>
@ -510,20 +452,13 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> RowHeaderWidthProperty =
AvaloniaProperty.Register<DataGrid, double>(
nameof(RowHeaderWidth),
defaultValue: double.NaN/*,
validate: ValidateRowHeaderWidth*/);
private static double ValidateRowHeaderWidth(DataGrid grid, double value)
defaultValue: double.NaN,
validate: IsValidRowHeaderWidth);
private static bool IsValidRowHeaderWidth(double value)
{
if (value < DATAGRID_minimumRowHeaderWidth)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_minimumRowHeaderWidth);
}
if (value > DATAGRID_maxHeadersThickness)
{
throw DataGridError.DataGrid.ValueMustBeLessThanOrEqualTo(nameof(value), nameof(RowHeaderWidth), DATAGRID_maxHeadersThickness);
}
return value;
return double.IsNaN(value) ||
(value >= DATAGRID_minimumRowHeaderWidth &&
value <= DATAGRID_maxHeadersThickness);
}
/// <summary>

22
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -67,26 +67,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> SublevelIndentProperty =
AvaloniaProperty.Register<DataGridRowGroupHeader, double>(
nameof(SublevelIndent),
defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent/*,
validate: ValidateSublevelIndent*/);
defaultValue: DataGrid.DATAGRID_defaultRowGroupSublevelIndent,
validate: IsValidSublevelIndent);
private static double ValidateSublevelIndent(DataGridRowGroupHeader header, double value)
private static bool IsValidSublevelIndent(double value)
{
// We don't need to revert to the old value if our input is bad because we never read this property value
if (double.IsNaN(value))
{
throw DataGridError.DataGrid.ValueCannotBeSetToNAN(nameof(SublevelIndent));
}
else if (double.IsInfinity(value))
{
throw DataGridError.DataGrid.ValueCannotBeSetToInfinity(nameof(SublevelIndent));
}
else if (value < 0)
{
throw DataGridError.DataGrid.ValueMustBeGreaterThanOrEqualTo(nameof(value), nameof(SublevelIndent), 0);
}
return value;
return !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0;
}
/// <summary>

43
src/Avalonia.Controls/AutoCompleteBox.cs

@ -377,8 +377,8 @@ namespace Avalonia.Controls
/// dependency property.</value>
public static readonly StyledProperty<int> MinimumPrefixLengthProperty =
AvaloniaProperty.Register<AutoCompleteBox, int>(
nameof(MinimumPrefixLength), 1/*,
validate: ValidateMinimumPrefixLength*/);
nameof(MinimumPrefixLength), 1,
validate: IsValidMinimumPrefixLength);
/// <summary>
/// Identifies the
@ -391,8 +391,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TimeSpan> MinimumPopulateDelayProperty =
AvaloniaProperty.Register<AutoCompleteBox, TimeSpan>(
nameof(MinimumPopulateDelay),
TimeSpan.Zero/*,
validate: ValidateMinimumPopulateDelay*/);
TimeSpan.Zero,
validate: IsValidMinimumPopulateDelay);
/// <summary>
/// Identifies the
@ -405,8 +405,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<double> MaxDropDownHeightProperty =
AvaloniaProperty.Register<AutoCompleteBox, double>(
nameof(MaxDropDownHeight),
double.PositiveInfinity/*,
validate: ValidateMaxDropDownHeight*/);
double.PositiveInfinity,
validate: IsValidMaxDropDownHeight);
/// <summary>
/// Identifies the
@ -494,8 +494,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty<AutoCompleteFilterMode> FilterModeProperty =
AvaloniaProperty.Register<AutoCompleteBox, AutoCompleteFilterMode>(
nameof(FilterMode),
defaultValue: AutoCompleteFilterMode.StartsWith/*,
validate: ValidateFilterMode*/);
defaultValue: AutoCompleteFilterMode.StartsWith,
validate: IsValidFilterMode);
/// <summary>
/// Identifies the
@ -546,26 +546,11 @@ namespace Avalonia.Controls
o => o.AsyncPopulator,
(o, v) => o.AsyncPopulator = v);
private static int ValidateMinimumPrefixLength(AutoCompleteBox control, int value)
{
Contract.Requires<ArgumentOutOfRangeException>(value >= -1);
return value;
}
private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value)
{
Contract.Requires<ArgumentOutOfRangeException>(value.TotalMilliseconds >= 0.0);
return value;
}
private static bool IsValidMinimumPrefixLength(int value) => value >= -1;
private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value)
{
Contract.Requires<ArgumentOutOfRangeException>(value >= 0.0);
private static bool IsValidMinimumPopulateDelay(TimeSpan value) => value.TotalMilliseconds >= 0.0;
return value;
}
private static bool IsValidMaxDropDownHeight(double value) => value >= 0.0;
private static bool IsValidFilterMode(AutoCompleteFilterMode mode)
{
@ -590,12 +575,6 @@ namespace Avalonia.Controls
return false;
}
}
private static AutoCompleteFilterMode ValidateFilterMode(AutoCompleteBox control, AutoCompleteFilterMode value)
{
Contract.Requires<ArgumentException>(IsValidFilterMode(value));
return value;
}
/// <summary>
/// Handle the change of the IsEnabled property.

16
src/Avalonia.Controls/Calendar/Calendar.cs

@ -351,8 +351,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<CalendarMode> DisplayModeProperty =
AvaloniaProperty.Register<Calendar, CalendarMode>(
nameof(DisplayMode)/*,
validate: ValidateDisplayMode*/);
nameof(DisplayMode),
validate: IsValidDisplayMode);
/// <summary>
/// Gets or sets a value indicating whether the calendar is displayed in
/// months, years, or decades.
@ -417,17 +418,6 @@ namespace Avalonia.Controls
}
OnDisplayModeChanged(new CalendarModeChangedEventArgs((CalendarMode)e.OldValue, mode));
}
private static CalendarMode ValidateDisplayMode(Calendar o, CalendarMode mode)
{
if(IsValidDisplayMode(mode))
{
return mode;
}
else
{
throw new ArgumentOutOfRangeException(nameof(mode), "Invalid DisplayMode");
}
}
private static bool IsValidDisplayMode(CalendarMode mode)
{
return mode == CalendarMode.Month

30
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -189,14 +189,14 @@ namespace Avalonia.Controls
public static readonly StyledProperty<DatePickerFormat> SelectedDateFormatProperty =
AvaloniaProperty.Register<DatePicker, DatePickerFormat>(
nameof(SelectedDateFormat),
defaultValue: DatePickerFormat.Short/*,
validate: ValidateSelectedDateFormat*/);
defaultValue: DatePickerFormat.Short,
validate: IsValidSelectedDateFormat);
public static readonly StyledProperty<string> CustomDateFormatStringProperty =
AvaloniaProperty.Register<DatePicker, string>(
nameof(CustomDateFormatString),
defaultValue: "d"/*,
validate: ValidateDateFormatString*/);
defaultValue: "d",
validate: IsValidDateFormatString);
public static readonly DirectProperty<DatePicker, string> TextProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>(
@ -1146,27 +1146,9 @@ namespace Avalonia.Controls
|| value == DatePickerFormat.Short
|| value == DatePickerFormat.Custom;
}
private static DatePickerFormat ValidateSelectedDateFormat(DatePicker dp, DatePickerFormat format)
private static bool IsValidDateFormatString(string formatString)
{
if(IsValidSelectedDateFormat(format))
{
return format;
}
else
{
throw new ArgumentOutOfRangeException(nameof(format), "DatePickerFormat value is not valid.");
}
}
private static string ValidateDateFormatString(DatePicker dp, string formatString)
{
if(string.IsNullOrWhiteSpace(formatString))
{
throw new ArgumentException("DateFormatString value is not valid.", nameof(formatString));
}
else
{
return formatString;
}
return !string.IsNullOrWhiteSpace(formatString);
}
private static DateTime DiscardDayTime(DateTime d)
{

16
src/Avalonia.Controls/DefinitionBase.cs

@ -356,9 +356,13 @@ namespace Avalonia.Controls
/// b) contains only letters, digits and underscore ('_').
/// c) does not start with a digit.
/// </remarks>
private static string SharedSizeGroupPropertyValueValid(Control _, string value)
private static bool SharedSizeGroupPropertyValueValid(string value)
{
Contract.Requires<ArgumentNullException>(value != null);
// null is default value
if (value == null)
{
return true;
}
string id = (string)value;
@ -380,11 +384,11 @@ namespace Avalonia.Controls
if (i == id.Length)
{
return value;
return true;
}
}
throw new ArgumentException("Invalid SharedSizeGroup string.");
return false;
}
/// <remark>
@ -750,8 +754,8 @@ namespace Avalonia.Controls
/// </remarks>
public static readonly AttachedProperty<string> SharedSizeGroupProperty =
AvaloniaProperty.RegisterAttached<DefinitionBase, Control, string>(
"SharedSizeGroup"/*,
validate: SharedSizeGroupPropertyValueValid*/);
"SharedSizeGroup",
validate: SharedSizeGroupPropertyValueValid);
/// <summary>
/// Static ctor. Used for static registration of properties.

32
src/Avalonia.Controls/Grid.cs

@ -2740,12 +2740,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<int> ColumnProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"Column",
defaultValue: 0/*,
validate: (_, v) =>
{
if (v >= 0) return v;
else throw new ArgumentException("Invalid Grid.Column value.");
}*/);
defaultValue: 0,
validate: v => v >= 0);
/// <summary>
/// Row property. This is an attached property.
@ -2761,12 +2757,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<int> RowProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"Row",
defaultValue: 0/*,
validate: (_, v) =>
{
if (v >= 0) return v;
else throw new ArgumentException("Invalid Grid.Row value.");
}*/);
defaultValue: 0,
validate: v => v >= 0);
/// <summary>
/// ColumnSpan property. This is an attached property.
@ -2781,12 +2773,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<int> ColumnSpanProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"ColumnSpan",
defaultValue: 1/*,
validate: (_, v) =>
{
if (v >= 1) return v;
else throw new ArgumentException("Invalid Grid.ColumnSpan value.");
}*/);
defaultValue: 1,
validate: v => v >= 0);
/// <summary>
/// RowSpan property. This is an attached property.
@ -2801,12 +2789,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<int> RowSpanProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"RowSpan",
defaultValue: 1/*,
validate: (_, v) =>
{
if (v >= 1) return v;
else throw new ArgumentException("Invalid Grid.RowSpan value.");
}*/);
defaultValue: 1,
validate: v => v >= 0);
/// <summary>
/// IsSharedSizeScope property marks scoping element for shared size.

33
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -58,7 +58,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="Increment"/> property.
/// </summary>
public static readonly StyledProperty<double> IncrementProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d/*, validate: OnCoerceIncrement*/);
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, coerce: OnCoerceIncrement);
/// <summary>
/// Defines the <see cref="IsReadOnly"/> property.
@ -70,13 +70,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="Maximum"/> property.
/// </summary>
public static readonly StyledProperty<double> MaximumProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue/*, validate: OnCoerceMaximum*/);
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum);
/// <summary>
/// Defines the <see cref="Minimum"/> property.
/// </summary>
public static readonly StyledProperty<double> MinimumProperty =
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue/*, validate: OnCoerceMinimum*/);
AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum);
/// <summary>
/// Defines the <see cref="ParsingNumberStyle"/> property.
@ -738,19 +738,34 @@ namespace Avalonia.Controls
}
}
private static double OnCoerceMaximum(NumericUpDown upDown, double value)
private static double OnCoerceMaximum(IAvaloniaObject instance, double value)
{
return upDown.OnCoerceMaximum(value);
if (instance is NumericUpDown upDown)
{
return upDown.OnCoerceMaximum(value);
}
return value;
}
private static double OnCoerceMinimum(NumericUpDown upDown, double value)
private static double OnCoerceMinimum(IAvaloniaObject instance, double value)
{
return upDown.OnCoerceMinimum(value);
if (instance is NumericUpDown upDown)
{
return upDown.OnCoerceMinimum(value);
}
return value;
}
private static double OnCoerceIncrement(NumericUpDown upDown, double value)
private static double OnCoerceIncrement(IAvaloniaObject instance, double value)
{
return upDown.OnCoerceIncrement(value);
if (instance is NumericUpDown upDown)
{
return upDown.OnCoerceIncrement(value);
}
return value;
}
private void TextBoxOnTextChanged()

155
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs

@ -0,0 +1,155 @@
// Copyright (c) The Avalonia 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.Subjects;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Coercion
{
[Fact]
public void Coerces_Set_Value()
{
var target = new Class1();
target.Foo = 150;
Assert.Equal(100, target.Foo);
}
[Fact]
public void Coerces_Set_Value_Attached()
{
var target = new Class1();
target.SetValue(Class1.AttachedProperty, 150);
Assert.Equal(100, target.GetValue(Class1.AttachedProperty));
}
[Fact]
public void Coerces_Bound_Value()
{
var target = new Class1();
var source = new Subject<BindingValue<int>>();
target.Bind(Class1.FooProperty, source);
source.OnNext(150);
Assert.Equal(100, target.Foo);
}
[Fact]
public void CoerceValue_Updates_Value()
{
var target = new Class1 { Foo = 99 };
Assert.Equal(99, target.Foo);
target.MaxFoo = 50;
target.CoerceValue(Class1.FooProperty);
Assert.Equal(50, target.Foo);
}
[Fact]
public void Coerced_Value_Can_Be_Restored_If_Limit_Changed()
{
var target = new Class1();
target.Foo = 150;
Assert.Equal(100, target.Foo);
target.MaxFoo = 200;
target.CoerceValue(Class1.FooProperty);
Assert.Equal(150, target.Foo);
}
[Fact]
public void Coerced_Value_Can_Be_Restored_From_Previously_Active_Binding()
{
var target = new Class1();
var source1 = new Subject<BindingValue<int>>();
var source2 = new Subject<BindingValue<int>>();
target.Bind(Class1.FooProperty, source1);
source1.OnNext(150);
target.Bind(Class1.FooProperty, source2);
source2.OnNext(160);
Assert.Equal(100, target.Foo);
target.MaxFoo = 200;
source2.OnCompleted();
Assert.Equal(150, target.Foo);
}
[Fact]
public void Coercion_Can_Be_Overridden()
{
var target = new Class2();
target.Foo = 150;
Assert.Equal(-150, target.Foo);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<int> FooProperty =
AvaloniaProperty.Register<Class1, int>(
"Qux",
defaultValue: 11,
coerce: CoerceFoo);
public static readonly AttachedProperty<int> AttachedProperty =
AvaloniaProperty.RegisterAttached<Class1, Class1, int>(
"Attached",
defaultValue: 11,
coerce: CoerceFoo);
public int Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public int MaxFoo { get; set; } = 100;
public static int CoerceFoo(IAvaloniaObject instance, int value)
{
return Math.Min(((Class1)instance).MaxFoo, value);
}
}
private class Class2 : AvaloniaObject
{
public static readonly StyledProperty<int> FooProperty =
Class1.FooProperty.AddOwner<Class2>();
static Class2()
{
FooProperty.OverrideMetadata<Class2>(
new StyledPropertyMetadata<int>(
coerce: CoerceFoo));
}
public int Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public static int CoerceFoo(IAvaloniaObject instance, int value)
{
return -value;
}
}
}
}

97
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@ -0,0 +1,97 @@
// Copyright (c) The Avalonia 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.Subjects;
using Avalonia.Controls;
using Xunit;
namespace Avalonia.Base.UnitTests
{
public class AvaloniaObjectTests_Validation
{
[Fact]
public void Registration_Throws_If_DefaultValue_Fails_Validation()
{
Assert.Throws<ArgumentException>(() =>
new StyledProperty<int>(
"BadDefault",
typeof(Class1),
new StyledPropertyMetadata<int>(101),
validate: Class1.ValidateFoo));
}
[Fact]
public void Metadata_Override_Throws_If_DefaultValue_Fails_Validation()
{
Assert.Throws<ArgumentException>(() => Class1.FooProperty.OverrideDefaultValue<Class2>(101));
}
[Fact]
public void SetValue_Throws_If_Fails_Validation()
{
var target = new Class1();
Assert.Throws<ArgumentException>(() => target.SetValue(Class1.FooProperty, 101));
}
[Fact]
public void SetValue_Throws_If_Fails_Validation_Attached()
{
var target = new Class1();
Assert.Throws<ArgumentException>(() => target.SetValue(Class1.AttachedProperty, 101));
}
[Fact]
public void Reverts_To_DefaultValue_If_Binding_Fails_Validation()
{
var target = new Class1();
var source = new Subject<int>();
target.Bind(Class1.FooProperty, source);
source.OnNext(150);
Assert.Equal(11, target.GetValue(Class1.FooProperty));
}
[Fact]
public void Reverts_To_DefaultValue_Even_In_Presence_Of_Other_Bindings()
{
var target = new Class1();
var source1 = new Subject<int>();
var source2 = new Subject<int>();
target.Bind(Class1.FooProperty, source1);
target.Bind(Class1.FooProperty, source2);
source1.OnNext(42);
source2.OnNext(150);
Assert.Equal(11, target.GetValue(Class1.FooProperty));
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<int> FooProperty =
AvaloniaProperty.Register<Class1, int>(
"Qux",
defaultValue: 11,
validate: ValidateFoo);
public static readonly AttachedProperty<int> AttachedProperty =
AvaloniaProperty.RegisterAttached<Class1, Class1, int>(
"Attached",
defaultValue: 11,
validate: ValidateFoo);
public static bool ValidateFoo(int value)
{
return value < 100;
}
}
private class Class2 : AvaloniaObject
{
}
}
}

50
tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs

@ -84,18 +84,64 @@ namespace Avalonia.Benchmarks.Base
}
}
class StyledClass : AvaloniaObject
[Benchmark]
public void Set_Validated_Int_Property_LocalValue()
{
var obj = new StyledClass();
for (var i = 0; i < 100; ++i)
{
obj.ValidatedIntValue += 1;
}
}
[Benchmark]
public void Set_Coerced_Int_Property_LocalValue()
{
private int _intValue;
var obj = new StyledClass();
for (var i = 0; i < 100; ++i)
{
obj.CoercedIntValue += 1;
}
}
class StyledClass : AvaloniaObject
{
public static readonly StyledProperty<int> IntValueProperty =
AvaloniaProperty.Register<StyledClass, int>(nameof(IntValue));
public static readonly StyledProperty<int> ValidatedIntValueProperty =
AvaloniaProperty.Register<StyledClass, int>(nameof(ValidatedIntValue), validate: ValidateIntValue);
public static readonly StyledProperty<int> CoercedIntValueProperty =
AvaloniaProperty.Register<StyledClass, int>(nameof(CoercedIntValue), coerce: CoerceIntValue);
public int IntValue
{
get => GetValue(IntValueProperty);
set => SetValue(IntValueProperty, value);
}
public int ValidatedIntValue
{
get => GetValue(ValidatedIntValueProperty);
set => SetValue(ValidatedIntValueProperty, value);
}
public int CoercedIntValue
{
get => GetValue(CoercedIntValueProperty);
set => SetValue(CoercedIntValueProperty, value);
}
private static bool ValidateIntValue(int arg)
{
return arg < 1000;
}
private static int CoerceIntValue(IAvaloniaObject arg1, int arg2)
{
return Math.Min(1000, arg2);
}
}
}
}

Loading…
Cancel
Save