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