1 using System.ComponentModel.DataAnnotations.Resources; 2 using System.Diagnostics.CodeAnalysis; 3 using System.Globalization; 4 5 namespace System.ComponentModel.DataAnnotations { 6 /// <summary> 7 /// Used for specifying a range constraint 8 /// </summary> 9 [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] 10 [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "We want it to be accessible via method on parent.")] 11 [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "We want users to be able to extend this class")] 12 public class RangeAttribute : ValidationAttribute { 13 /// <summary> 14 /// Gets the minimum value for the range 15 /// </summary> 16 public object Minimum { get; private set; } 17 18 /// <summary> 19 /// Gets the maximum value for the range 20 /// </summary> 21 public object Maximum { get; private set; } 22 23 /// <summary> 24 /// Gets the type of the <see cref="Minimum"/> and <see cref="Maximum"/> values (e.g. Int32, Double, or some custom type) 25 /// </summary> 26 public Type OperandType { get; private set; } 27 28 private Func<object, object> Conversion { get; set; } 29 30 /// <summary> 31 /// Constructor that takes integer minimum and maximum values 32 /// </summary> 33 /// <param name="minimum">The minimum value, inclusive</param> 34 /// <param name="maximum">The maximum value, inclusive</param> RangeAttribute(int minimum, int maximum)35 public RangeAttribute(int minimum, int maximum) 36 : this() { 37 this.Minimum = minimum; 38 this.Maximum = maximum; 39 this.OperandType = typeof(int); 40 } 41 42 /// <summary> 43 /// Constructor that takes double minimum and maximum values 44 /// </summary> 45 /// <param name="minimum">The minimum value, inclusive</param> 46 /// <param name="maximum">The maximum value, inclusive</param> RangeAttribute(double minimum, double maximum)47 public RangeAttribute(double minimum, double maximum) 48 : this() { 49 this.Minimum = minimum; 50 this.Maximum = maximum; 51 this.OperandType = typeof(double); 52 } 53 54 /// <summary> 55 /// Allows for specifying range for arbitrary types. The minimum and maximum strings will be converted to the target type. 56 /// </summary> 57 /// <param name="type">The type of the range parameters. Must implement IComparable.</param> 58 /// <param name="minimum">The minimum allowable value.</param> 59 /// <param name="maximum">The maximum allowable value.</param> RangeAttribute(Type type, string minimum, string maximum)60 public RangeAttribute(Type type, string minimum, string maximum) 61 : this() { 62 this.OperandType = type; 63 this.Minimum = minimum; 64 this.Maximum = maximum; 65 } 66 RangeAttribute()67 private RangeAttribute() 68 : base(() => DataAnnotationsResources.RangeAttribute_ValidationError) { 69 } 70 Initialize(IComparable minimum, IComparable maximum, Func<object, object> conversion)71 private void Initialize(IComparable minimum, IComparable maximum, Func<object, object> conversion) { 72 if (minimum.CompareTo(maximum) > 0) { 73 throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, DataAnnotationsResources.RangeAttribute_MinGreaterThanMax, maximum, minimum)); 74 } 75 76 this.Minimum = minimum; 77 this.Maximum = maximum; 78 this.Conversion = conversion; 79 } 80 81 /// <summary> 82 /// Returns true if the value falls between min and max, inclusive. 83 /// </summary> 84 /// <param name="value">The value to test for validity.</param> 85 /// <returns><c>true</c> means the <paramref name="value"/> is valid</returns> 86 /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception> 87 #if !SILVERLIGHT 88 public 89 #else 90 internal 91 #endif IsValid(object value)92 override bool IsValid(object value) { 93 // Validate our properties and create the conversion function 94 this.SetupConversion(); 95 96 // Automatically pass if value is null or empty. RequiredAttribute should be used to assert a value is not empty. 97 if (value == null) { 98 return true; 99 } 100 string s = value as string; 101 if (s != null && String.IsNullOrEmpty(s)) { 102 return true; 103 } 104 105 object convertedValue = null; 106 107 try { 108 convertedValue = this.Conversion(value); 109 } catch (FormatException) { 110 return false; 111 } catch (InvalidCastException) { 112 return false; 113 } catch (NotSupportedException) { 114 return false; 115 } 116 117 IComparable min = (IComparable)this.Minimum; 118 IComparable max = (IComparable)this.Maximum; 119 return min.CompareTo(convertedValue) <= 0 && max.CompareTo(convertedValue) >= 0; 120 } 121 122 /// <summary> 123 /// Override of <see cref="ValidationAttribute.FormatErrorMessage"/> 124 /// </summary> 125 /// <remarks>This override exists to provide a formatted message describing the minimum and maximum values</remarks> 126 /// <param name="name">The user-visible name to include in the formatted message.</param> 127 /// <returns>A localized string describing the minimum and maximum values</returns> 128 /// <exception cref="InvalidOperationException"> is thrown if the current attribute is ill-formed.</exception> FormatErrorMessage(string name)129 public override string FormatErrorMessage(string name) { 130 this.SetupConversion(); 131 132 return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, this.Minimum, this.Maximum); 133 } 134 135 /// <summary> 136 /// Validates the properties of this attribute and sets up the conversion function. 137 /// This method throws exceptions if the attribute is not configured properly. 138 /// If it has once determined it is properly configured, it is a NOP. 139 /// </summary> SetupConversion()140 private void SetupConversion() { 141 if (this.Conversion == null) { 142 object minimum = this.Minimum; 143 object maximum = this.Maximum; 144 145 if (minimum == null || maximum == null) { 146 throw new InvalidOperationException(DataAnnotationsResources.RangeAttribute_Must_Set_Min_And_Max); 147 } 148 149 // Careful here -- OperandType could be int or double if they used the long form of the ctor. 150 // But the min and max would still be strings. Do use the type of the min/max operands to condition 151 // the following code. 152 Type operandType = minimum.GetType(); 153 154 if (operandType == typeof(int)) { 155 this.Initialize((int)minimum, (int)maximum, v => Convert.ToInt32(v, CultureInfo.InvariantCulture)); 156 } else if (operandType == typeof(double)) { 157 this.Initialize((double)minimum, (double)maximum, v => Convert.ToDouble(v, CultureInfo.InvariantCulture)); 158 } else { 159 Type type = this.OperandType; 160 if (type == null) { 161 throw new InvalidOperationException(DataAnnotationsResources.RangeAttribute_Must_Set_Operand_Type); 162 } 163 Type comparableType = typeof(IComparable); 164 if (!comparableType.IsAssignableFrom(type)) { 165 throw new InvalidOperationException( 166 String.Format( 167 CultureInfo.CurrentCulture, 168 DataAnnotationsResources.RangeAttribute_ArbitraryTypeNotIComparable, 169 type.FullName, 170 comparableType.FullName)); 171 } 172 173 #if SILVERLIGHT 174 Func<object, object> conversion = value => (value != null && value.GetType() == type) ? value : Convert.ChangeType(value, type, CultureInfo.CurrentCulture); 175 IComparable min = (IComparable)conversion(minimum); 176 IComparable max = (IComparable)conversion(maximum); 177 #else 178 TypeConverter converter = TypeDescriptor.GetConverter(type); 179 IComparable min = (IComparable)converter.ConvertFromString((string)minimum); 180 IComparable max = (IComparable)converter.ConvertFromString((string)maximum); 181 182 Func<object, object> conversion = value => (value != null && value.GetType() == type) ? value : converter.ConvertFrom(value); 183 #endif // !SILVERLIGHT 184 185 this.Initialize(min, max, conversion); 186 } 187 } 188 } 189 } 190 } 191