diff --git a/src/Avalonia.Base/AttachedProperty.cs b/src/Avalonia.Base/AttachedProperty.cs
index fdb04b6dfc..d1df5fa5e3 100644
--- a/src/Avalonia.Base/AttachedProperty.cs
+++ b/src/Avalonia.Base/AttachedProperty.cs
@@ -18,12 +18,14 @@ namespace Avalonia
/// The class that is registering the property.
/// The property metadata.
/// Whether the property inherits its value.
+ /// A value validation callback.
public AttachedProperty(
string name,
- Type ownerType,
+ Type ownerType,
StyledPropertyMetadata metadata,
- bool inherits = false)
- : base(name, ownerType, metadata, inherits)
+ bool inherits = false,
+ Func validate = null)
+ : base(name, ownerType, metadata, inherits, validate)
{
}
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 023594de9a..6a00feaf79 100644
--- a/src/Avalonia.Base/AvaloniaObject.cs
+++ b/src/Avalonia.Base/AvaloniaObject.cs
@@ -413,6 +413,16 @@ namespace Avalonia
return new DirectBindingSubscription(this, property, source);
}
+ ///
+ /// Coerces the specified .
+ ///
+ /// The type of the property.
+ /// The property.
+ public void CoerceValue(StyledPropertyBase property)
+ {
+ _values?.CoerceValue(property);
+ }
+
///
void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
{
diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs
index a39b8e6e1d..e1d4a23441 100644
--- a/src/Avalonia.Base/AvaloniaProperty.cs
+++ b/src/Avalonia.Base/AvaloniaProperty.cs
@@ -257,6 +257,8 @@ namespace Avalonia
/// The default value of the property.
/// Whether the property inherits its value.
/// The default binding mode for the property.
+ /// A value validation callback.
+ /// A value coercion callback.
///
/// 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 validate = null,
+ Func coerce = null,
Action notifying = null)
where TOwner : IAvaloniaObject
{
@@ -275,13 +279,15 @@ namespace Avalonia
var metadata = new StyledPropertyMetadata(
defaultValue,
- defaultBindingMode: defaultBindingMode);
+ defaultBindingMode: defaultBindingMode,
+ coerce: coerce);
var result = new StyledProperty(
name,
typeof(TOwner),
metadata,
inherits,
+ validate,
notifying);
AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
@@ -297,21 +303,26 @@ namespace Avalonia
/// The default value of the property.
/// Whether the property inherits its value.
/// The default binding mode for the property.
+ /// A value validation callback.
+ /// A value coercion callback.
/// A
public static AttachedProperty RegisterAttached(
string name,
TValue defaultValue = default(TValue),
bool inherits = false,
- BindingMode defaultBindingMode = BindingMode.OneWay)
+ BindingMode defaultBindingMode = BindingMode.OneWay,
+ Func validate = null,
+ Func coerce = null)
where THost : IAvaloniaObject
{
Contract.Requires(name != null);
var metadata = new StyledPropertyMetadata(
defaultValue,
- defaultBindingMode: defaultBindingMode);
+ defaultBindingMode: defaultBindingMode,
+ coerce: coerce);
- var result = new AttachedProperty(name, typeof(TOwner), metadata, inherits);
+ var result = new AttachedProperty(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
/// The default value of the property.
/// Whether the property inherits its value.
/// The default binding mode for the property.
+ /// A value validation callback.
+ /// A value coercion callback.
/// A
public static AttachedProperty RegisterAttached(
string name,
Type ownerType,
TValue defaultValue = default(TValue),
bool inherits = false,
- BindingMode defaultBindingMode = BindingMode.OneWay)
+ BindingMode defaultBindingMode = BindingMode.OneWay,
+ Func validate = null,
+ Func coerce = null)
where THost : IAvaloniaObject
{
Contract.Requires(name != null);
var metadata = new StyledPropertyMetadata(
defaultValue,
- defaultBindingMode: defaultBindingMode);
+ defaultBindingMode: defaultBindingMode,
+ coerce: coerce);
- var result = new AttachedProperty(name, ownerType, metadata, inherits);
+ var result = new AttachedProperty(name, ownerType, metadata, inherits, validate);
var registry = AvaloniaPropertyRegistry.Instance;
registry.Register(ownerType, result);
registry.RegisterAttached(typeof(THost), result);
diff --git a/src/Avalonia.Base/BoxedValue.cs b/src/Avalonia.Base/BoxedValue.cs
deleted file mode 100644
index 5fc515f299..0000000000
--- a/src/Avalonia.Base/BoxedValue.cs
+++ /dev/null
@@ -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
-{
- ///
- /// Represents boxed value of type .
- ///
- /// Type of stored value.
- internal readonly struct BoxedValue
- {
- public BoxedValue(T value)
- {
- Boxed = value;
- Typed = value;
- }
-
- ///
- /// Boxed value.
- ///
- public object Boxed { get; }
-
- ///
- /// Typed value.
- ///
- public T Typed { get; }
- }
-}
diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs
index c8150e8a64..fb85ae222c 100644
--- a/src/Avalonia.Base/IAvaloniaObject.cs
+++ b/src/Avalonia.Base/IAvaloniaObject.cs
@@ -106,6 +106,13 @@ namespace Avalonia
DirectPropertyBase property,
IObservable> source);
+ ///
+ /// Coerces the specified .
+ ///
+ /// The type of the property.
+ /// The property.
+ void CoerceValue(StyledPropertyBase property);
+
///
/// Registers an object as an inheritance child.
///
diff --git a/src/Avalonia.Base/PropertyStore/BindingEntry.cs b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
index 79e55e7e02..637272ad6a 100644
--- a/src/Avalonia.Base/PropertyStore/BindingEntry.cs
+++ b/src/Avalonia.Base/PropertyStore/BindingEntry.cs
@@ -81,6 +81,11 @@ namespace Avalonia.PropertyStore
private void UpdateValue(BindingValue value)
{
+ if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)
+ {
+ value = Property.GetDefaultValue(_owner.GetType());
+ }
+
if (value.Type == BindingValueType.DoNothing)
{
return;
diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
index a7b17d6f5a..7440ba8e03 100644
--- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs
+++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs
@@ -21,6 +21,7 @@ namespace Avalonia.PropertyStore
{
private readonly IValueSink _sink;
private readonly List> _entries = new List>();
+ private readonly Func? _coerceValue;
private Optional _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(
StyledPropertyBase 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;
diff --git a/src/Avalonia.Base/StyledProperty.cs b/src/Avalonia.Base/StyledProperty.cs
index 4eb85a046e..62443b424c 100644
--- a/src/Avalonia.Base/StyledProperty.cs
+++ b/src/Avalonia.Base/StyledProperty.cs
@@ -17,14 +17,16 @@ namespace Avalonia
/// The type of the class that registers the property.
/// The property metadata.
/// Whether the property inherits its value.
+ /// A value validation callback.
/// A callback.
public StyledProperty(
string name,
Type ownerType,
StyledPropertyMetadata metadata,
bool inherits = false,
+ Func validate = null,
Action notifying = null)
- : base(name, ownerType, metadata, inherits, notifying)
+ : base(name, ownerType, metadata, inherits, validate, notifying)
{
}
diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs
index 129b1f3c12..8c4d683ae0 100644
--- a/src/Avalonia.Base/StyledPropertyBase.cs
+++ b/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
/// The type of the class that registers the property.
/// The property metadata.
/// Whether the property inherits its value.
+ /// A value validation callback.
/// A callback.
protected StyledPropertyBase(
string name,
Type ownerType,
StyledPropertyMetadata metadata,
bool inherits = false,
+ Func validate = null,
Action 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}'.");
+ }
}
///
@@ -62,6 +71,29 @@ namespace Avalonia
///
public override bool Inherits => _inherits;
+ ///
+ /// Gets the value validation callback for the property.
+ ///
+ public Func ValidateValue { get; }
+
+ ///
+ /// Gets a value indicating whether this property has any value coercion callbacks defined
+ /// in its metadata.
+ ///
+ 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;
+ }
+
///
/// Gets the default value for the property on the specified type.
///
@@ -71,7 +103,7 @@ namespace Avalonia
{
Contract.Requires(type != null);
- return GetMetadata(type).DefaultValue.Typed;
+ return GetMetadata(type).DefaultValue;
}
///
@@ -123,6 +155,17 @@ namespace Avalonia
/// The metadata.
public void OverrideMetadata(Type type, StyledPropertyMetadata 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(type != null);
- return GetMetadata(type).DefaultValue.Boxed;
+ return GetMetadata(type).DefaultValue;
}
[DebuggerHidden]
diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
index 18a38655bd..f96298a298 100644
--- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs
+++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs
@@ -12,25 +12,35 @@ namespace Avalonia
///
public class StyledPropertyMetadata : PropertyMetadata, IStyledPropertyMetadata
{
+ private Optional _defaultValue;
+
///
/// Initializes a new instance of the class.
///
/// The default value of the property.
/// The default binding mode.
+ /// A value coercion callback.
public StyledPropertyMetadata(
- TValue defaultValue = default,
- BindingMode defaultBindingMode = BindingMode.Default)
+ Optional defaultValue = default,
+ BindingMode defaultBindingMode = BindingMode.Default,
+ Func coerce = null)
: base(defaultBindingMode)
{
- DefaultValue = new BoxedValue(defaultValue);
+ _defaultValue = defaultValue;
+ CoerceValue = coerce;
}
///
/// Gets the default value for the property.
///
- internal BoxedValue DefaultValue { get; private set; }
+ public TValue DefaultValue => _defaultValue.GetValueOrDefault();
+
+ ///
+ /// Gets the value coercion callback, if any.
+ ///
+ public Func? CoerceValue { get; private set; }
- object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
+ object IStyledPropertyMetadata.DefaultValue => DefaultValue;
///
public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
@@ -39,9 +49,14 @@ namespace Avalonia
if (baseMetadata is StyledPropertyMetadata src)
{
- if (DefaultValue.Boxed == null)
+ if (!_defaultValue.HasValue)
+ {
+ _defaultValue = src.DefaultValue;
+ }
+
+ if (CoerceValue == null)
{
- DefaultValue = src.DefaultValue;
+ CoerceValue = src.CoerceValue;
}
}
}
diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs
index 5b6285623e..a78f6d15d6 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/src/Avalonia.Base/ValueStore.cs
@@ -72,10 +72,22 @@ namespace Avalonia
public void SetValue(StyledPropertyBase 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(_owner, property, this);
+ _values.AddValue(property, entry);
+ entry.SetValue(value, priority);
+ }
else if (priority == BindingPriority.LocalValue)
{
_values.AddValue(property, new LocalValueEntry(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(_owner, property, this);
+ var binding = entry.AddBinding(source, priority);
+ _values.AddValue(property, entry);
+ binding.Start();
+ return binding;
+ }
else
{
var entry = new BindingEntry(property, source, priority, this);
@@ -135,6 +156,17 @@ namespace Avalonia
}
}
+ public void CoerceValue(StyledPropertyBase property)
+ {
+ if (_values.TryGetValue(property, out var slot))
+ {
+ if (slot is PriorityValue p)
+ {
+ p.CoerceValue();
+ }
+ }
+ }
+
public Diagnostics.AvaloniaPropertyValue? GetDiagnostic(AvaloniaProperty property)
{
if (_values.TryGetValue(property, out var slot))
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index a217d02ecd..86133d5fdb 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -201,21 +201,13 @@ namespace Avalonia.Controls
public static readonly StyledProperty ColumnHeaderHeightProperty =
AvaloniaProperty.Register(
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);
}
///
@@ -261,8 +253,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty FrozenColumnCountProperty =
AvaloniaProperty.Register(
- nameof(FrozenColumnCount)/*,
- validate: ValidateFrozenColumnCount*/);
+ nameof(FrozenColumnCount),
+ validate: ValidateFrozenColumnCount);
///
/// 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 GridLinesVisibilityProperty =
AvaloniaProperty.Register(nameof(GridLinesVisibility));
@@ -395,30 +379,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty MaxColumnWidthProperty =
AvaloniaProperty.Register(
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;
}
///
@@ -433,29 +399,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty MinColumnWidthProperty =
AvaloniaProperty.Register(
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;
}
///
@@ -482,20 +431,13 @@ namespace Avalonia.Controls
public static readonly StyledProperty RowHeightProperty =
AvaloniaProperty.Register(
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);
}
///
@@ -510,20 +452,13 @@ namespace Avalonia.Controls
public static readonly StyledProperty RowHeaderWidthProperty =
AvaloniaProperty.Register(
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);
}
///
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
index bede7f481e..69dfed761f 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
@@ -67,26 +67,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty SublevelIndentProperty =
AvaloniaProperty.Register(
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;
}
///
diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index 1e1a62f4a4..6deddef0d0 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -377,8 +377,8 @@ namespace Avalonia.Controls
/// dependency property.
public static readonly StyledProperty MinimumPrefixLengthProperty =
AvaloniaProperty.Register(
- nameof(MinimumPrefixLength), 1/*,
- validate: ValidateMinimumPrefixLength*/);
+ nameof(MinimumPrefixLength), 1,
+ validate: IsValidMinimumPrefixLength);
///
/// Identifies the
@@ -391,8 +391,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty MinimumPopulateDelayProperty =
AvaloniaProperty.Register(
nameof(MinimumPopulateDelay),
- TimeSpan.Zero/*,
- validate: ValidateMinimumPopulateDelay*/);
+ TimeSpan.Zero,
+ validate: IsValidMinimumPopulateDelay);
///
/// Identifies the
@@ -405,8 +405,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty MaxDropDownHeightProperty =
AvaloniaProperty.Register(
nameof(MaxDropDownHeight),
- double.PositiveInfinity/*,
- validate: ValidateMaxDropDownHeight*/);
+ double.PositiveInfinity,
+ validate: IsValidMaxDropDownHeight);
///
/// Identifies the
@@ -494,8 +494,8 @@ namespace Avalonia.Controls
public static readonly StyledProperty FilterModeProperty =
AvaloniaProperty.Register(
nameof(FilterMode),
- defaultValue: AutoCompleteFilterMode.StartsWith/*,
- validate: ValidateFilterMode*/);
+ defaultValue: AutoCompleteFilterMode.StartsWith,
+ validate: IsValidFilterMode);
///
/// 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(value >= -1);
-
- return value;
- }
-
- private static TimeSpan ValidateMinimumPopulateDelay(AutoCompleteBox control, TimeSpan value)
- {
- Contract.Requires(value.TotalMilliseconds >= 0.0);
-
- return value;
- }
+ private static bool IsValidMinimumPrefixLength(int value) => value >= -1;
- private static double ValidateMaxDropDownHeight(AutoCompleteBox control, double value)
- {
- Contract.Requires(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(IsValidFilterMode(value));
-
- return value;
- }
///
/// Handle the change of the IsEnabled property.
diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs
index 58b7d7cb47..94f8ad41a0 100644
--- a/src/Avalonia.Controls/Calendar/Calendar.cs
+++ b/src/Avalonia.Controls/Calendar/Calendar.cs
@@ -351,8 +351,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty DisplayModeProperty =
AvaloniaProperty.Register(
- nameof(DisplayMode)/*,
- validate: ValidateDisplayMode*/);
+ nameof(DisplayMode),
+ validate: IsValidDisplayMode);
+
///
/// 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
diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs
index aa3a8fae3d..b4d4fed9fc 100644
--- a/src/Avalonia.Controls/Calendar/DatePicker.cs
+++ b/src/Avalonia.Controls/Calendar/DatePicker.cs
@@ -189,14 +189,14 @@ namespace Avalonia.Controls
public static readonly StyledProperty SelectedDateFormatProperty =
AvaloniaProperty.Register(
nameof(SelectedDateFormat),
- defaultValue: DatePickerFormat.Short/*,
- validate: ValidateSelectedDateFormat*/);
+ defaultValue: DatePickerFormat.Short,
+ validate: IsValidSelectedDateFormat);
public static readonly StyledProperty CustomDateFormatStringProperty =
AvaloniaProperty.Register(
nameof(CustomDateFormatString),
- defaultValue: "d"/*,
- validate: ValidateDateFormatString*/);
+ defaultValue: "d",
+ validate: IsValidDateFormatString);
public static readonly DirectProperty TextProperty =
AvaloniaProperty.RegisterDirect(
@@ -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)
{
diff --git a/src/Avalonia.Controls/DefinitionBase.cs b/src/Avalonia.Controls/DefinitionBase.cs
index eae6cf5e30..38ebbe5bf9 100644
--- a/src/Avalonia.Controls/DefinitionBase.cs
+++ b/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.
///
- private static string SharedSizeGroupPropertyValueValid(Control _, string value)
+ private static bool SharedSizeGroupPropertyValueValid(string value)
{
- Contract.Requires(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;
}
///
@@ -750,8 +754,8 @@ namespace Avalonia.Controls
///
public static readonly AttachedProperty SharedSizeGroupProperty =
AvaloniaProperty.RegisterAttached(
- "SharedSizeGroup"/*,
- validate: SharedSizeGroupPropertyValueValid*/);
+ "SharedSizeGroup",
+ validate: SharedSizeGroupPropertyValueValid);
///
/// Static ctor. Used for static registration of properties.
diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs
index 7b57288e76..1781067abb 100644
--- a/src/Avalonia.Controls/Grid.cs
+++ b/src/Avalonia.Controls/Grid.cs
@@ -2740,12 +2740,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty ColumnProperty =
AvaloniaProperty.RegisterAttached(
"Column",
- defaultValue: 0/*,
- validate: (_, v) =>
- {
- if (v >= 0) return v;
- else throw new ArgumentException("Invalid Grid.Column value.");
- }*/);
+ defaultValue: 0,
+ validate: v => v >= 0);
///
/// Row property. This is an attached property.
@@ -2761,12 +2757,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty RowProperty =
AvaloniaProperty.RegisterAttached(
"Row",
- defaultValue: 0/*,
- validate: (_, v) =>
- {
- if (v >= 0) return v;
- else throw new ArgumentException("Invalid Grid.Row value.");
- }*/);
+ defaultValue: 0,
+ validate: v => v >= 0);
///
/// ColumnSpan property. This is an attached property.
@@ -2781,12 +2773,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty ColumnSpanProperty =
AvaloniaProperty.RegisterAttached(
"ColumnSpan",
- defaultValue: 1/*,
- validate: (_, v) =>
- {
- if (v >= 1) return v;
- else throw new ArgumentException("Invalid Grid.ColumnSpan value.");
- }*/);
+ defaultValue: 1,
+ validate: v => v >= 0);
///
/// RowSpan property. This is an attached property.
@@ -2801,12 +2789,8 @@ namespace Avalonia.Controls
public static readonly AttachedProperty RowSpanProperty =
AvaloniaProperty.RegisterAttached(
"RowSpan",
- defaultValue: 1/*,
- validate: (_, v) =>
- {
- if (v >= 1) return v;
- else throw new ArgumentException("Invalid Grid.RowSpan value.");
- }*/);
+ defaultValue: 1,
+ validate: v => v >= 0);
///
/// IsSharedSizeScope property marks scoping element for shared size.
diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
index 9bc97e3758..cbb5b667e7 100644
--- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
+++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
@@ -58,7 +58,7 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty IncrementProperty =
- AvaloniaProperty.Register(nameof(Increment), 1.0d/*, validate: OnCoerceIncrement*/);
+ AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement);
///
/// Defines the property.
@@ -70,13 +70,13 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty MaximumProperty =
- AvaloniaProperty.Register(nameof(Maximum), double.MaxValue/*, validate: OnCoerceMaximum*/);
+ AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum);
///
/// Defines the property.
///
public static readonly StyledProperty MinimumProperty =
- AvaloniaProperty.Register(nameof(Minimum), double.MinValue/*, validate: OnCoerceMinimum*/);
+ AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum);
///
/// Defines the 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()
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Coercion.cs
new file mode 100644
index 0000000000..3efb926ac3
--- /dev/null
+++ b/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>();
+
+ 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>();
+ var source2 = new Subject>();
+
+ 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 FooProperty =
+ AvaloniaProperty.Register(
+ "Qux",
+ defaultValue: 11,
+ coerce: CoerceFoo);
+
+ public static readonly AttachedProperty AttachedProperty =
+ AvaloniaProperty.RegisterAttached(
+ "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 FooProperty =
+ Class1.FooProperty.AddOwner();
+
+ static Class2()
+ {
+ FooProperty.OverrideMetadata(
+ new StyledPropertyMetadata(
+ coerce: CoerceFoo));
+ }
+
+ public int Foo
+ {
+ get => GetValue(FooProperty);
+ set => SetValue(FooProperty, value);
+ }
+
+ public static int CoerceFoo(IAvaloniaObject instance, int value)
+ {
+ return -value;
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs
new file mode 100644
index 0000000000..391b379c51
--- /dev/null
+++ b/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(() =>
+ new StyledProperty(
+ "BadDefault",
+ typeof(Class1),
+ new StyledPropertyMetadata(101),
+ validate: Class1.ValidateFoo));
+ }
+
+ [Fact]
+ public void Metadata_Override_Throws_If_DefaultValue_Fails_Validation()
+ {
+ Assert.Throws(() => Class1.FooProperty.OverrideDefaultValue(101));
+ }
+
+ [Fact]
+ public void SetValue_Throws_If_Fails_Validation()
+ {
+ var target = new Class1();
+
+ Assert.Throws(() => target.SetValue(Class1.FooProperty, 101));
+ }
+
+ [Fact]
+ public void SetValue_Throws_If_Fails_Validation_Attached()
+ {
+ var target = new Class1();
+
+ Assert.Throws(() => target.SetValue(Class1.AttachedProperty, 101));
+ }
+
+ [Fact]
+ public void Reverts_To_DefaultValue_If_Binding_Fails_Validation()
+ {
+ var target = new Class1();
+ var source = new Subject();
+
+ 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();
+ var source2 = new Subject();
+
+ 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 FooProperty =
+ AvaloniaProperty.Register(
+ "Qux",
+ defaultValue: 11,
+ validate: ValidateFoo);
+
+ public static readonly AttachedProperty AttachedProperty =
+ AvaloniaProperty.RegisterAttached(
+ "Attached",
+ defaultValue: 11,
+ validate: ValidateFoo);
+
+ public static bool ValidateFoo(int value)
+ {
+ return value < 100;
+ }
+ }
+
+ private class Class2 : AvaloniaObject
+ {
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs
index 4b57776759..b70ae19275 100644
--- a/tests/Avalonia.Benchmarks/Base/StyledPropertyBenchmark.cs
+++ b/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 IntValueProperty =
AvaloniaProperty.Register(nameof(IntValue));
+ public static readonly StyledProperty ValidatedIntValueProperty =
+ AvaloniaProperty.Register(nameof(ValidatedIntValue), validate: ValidateIntValue);
+ public static readonly StyledProperty CoercedIntValueProperty =
+ AvaloniaProperty.Register(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);
+ }
}
}
}