浏览代码

Added WPF-style validation for AvaloniaProperty.

Steven Kirk 6 年之前
父节点
当前提交
aa81db75a0

+ 6 - 1
src/Avalonia.Base/AvaloniaProperty.cs

@@ -257,6 +257,7 @@ 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="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 +269,7 @@ namespace Avalonia
             TValue defaultValue = default(TValue),
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
+            Func<TValue, bool> validate = null,
             Action<IAvaloniaObject, bool> notifying = null)
                 where TOwner : IAvaloniaObject
         {
@@ -282,6 +284,7 @@ namespace Avalonia
                 typeof(TOwner),
                 metadata,
                 inherits,
+                validate,
                 notifying);
             AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result);
             return result;
@@ -297,12 +300,14 @@ 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>
         /// <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)
                 where THost : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);

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

+ 5 - 0
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -77,6 +77,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;

+ 3 - 1
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)
         {
         }
 

+ 25 - 3
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,13 @@ namespace Avalonia
             }
 
             _inherits = inherits;
+            ValidateValue = validate;
+
+            if (validate?.Invoke(metadata.DefaultValue) == false)
+            {
+                throw new ArgumentException(
+                    $"'{metadata.DefaultValue}' is not a valid default value for '{name}'.");
+            }
         }
 
         /// <summary>
@@ -62,6 +70,11 @@ 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 the default value for the property on the specified type.
         /// </summary>
@@ -71,7 +84,7 @@ namespace Avalonia
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
-            return GetMetadata(type).DefaultValue.Typed;
+            return GetMetadata(type).DefaultValue;
         }
 
         /// <summary>
@@ -123,6 +136,15 @@ 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}'.");
+                }
+            }
+
             base.OverrideMetadata(type, metadata);
         }
 
@@ -209,7 +231,7 @@ namespace Avalonia
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
-            return GetMetadata(type).DefaultValue.Boxed;
+            return GetMetadata(type).DefaultValue;
         }
 
         [DebuggerHidden]

+ 8 - 6
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@@ -12,25 +12,27 @@ 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>
         public StyledPropertyMetadata(
-            TValue defaultValue = default,
+            Optional<TValue> defaultValue = default,
             BindingMode defaultBindingMode = BindingMode.Default)
                 : base(defaultBindingMode)
         {
-            DefaultValue = new BoxedValue<TValue>(defaultValue);
+            _defaultValue = defaultValue;
         }
 
         /// <summary>
         /// Gets the default value for the property.
         /// </summary>
-        internal BoxedValue<TValue> DefaultValue { get; private set; }
+        internal TValue DefaultValue => _defaultValue.ValueOrDefault();
 
-        object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
+        object IStyledPropertyMetadata.DefaultValue => DefaultValue;
 
         /// <inheritdoc/>
         public override void Merge(PropertyMetadata baseMetadata, AvaloniaProperty property)
@@ -39,9 +41,9 @@ namespace Avalonia
 
             if (baseMetadata is StyledPropertyMetadata<TValue> src)
             {
-                if (DefaultValue.Boxed == null)
+                if (!_defaultValue.HasValue)
                 {
-                    DefaultValue = src.DefaultValue;
+                    _defaultValue = src.DefaultValue;
                 }
             }
         }

+ 5 - 0
src/Avalonia.Base/ValueStore.cs

@@ -66,6 +66,11 @@ 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);

+ 28 - 93
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>

+ 4 - 18
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>

+ 11 - 32
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.

+ 3 - 13
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

+ 6 - 24
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)
         {

+ 5 - 5
src/Avalonia.Controls/DefinitionBase.cs

@@ -356,7 +356,7 @@ 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);
 
@@ -380,11 +380,11 @@ namespace Avalonia.Controls
 
                 if (i == id.Length)
                 {
-                    return value;
+                    return true;
                 }
             }
 
-            throw new ArgumentException("Invalid SharedSizeGroup string.");
+            return false;
         }
 
         /// <remark>
@@ -750,8 +750,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.

+ 8 - 24
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.

+ 82 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Validation.cs

@@ -0,0 +1,82 @@
+// 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 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 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 bool ValidateFoo(int value)
+            {
+                return value < 100;
+            }
+        }
+
+        private class Class2 : AvaloniaObject
+        {
+        }
+    }
+}