1 changed files with 346 additions and 0 deletions
@ -0,0 +1,346 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
|
||||
|
/// </summary>
|
||||
|
public abstract class CommonNumericUpDown<T> : NumericUpDown<T?> where T : struct, IFormattable, IComparable<T> |
||||
|
{ |
||||
|
protected delegate bool FromText(string s, NumberStyles style, IFormatProvider provider, out T result); |
||||
|
protected delegate T FromDecimal(decimal d); |
||||
|
|
||||
|
private readonly FromText _fromText; |
||||
|
private readonly FromDecimal _fromDecimal; |
||||
|
private readonly Func<T, T, bool> _fromLowerThan; |
||||
|
private readonly Func<T, T, bool> _fromGreaterThan; |
||||
|
|
||||
|
private NumberStyles _parsingNumberStyle = NumberStyles.Any; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="ParsingNumberStyle"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly DirectProperty<CommonNumericUpDown<T>, NumberStyles> ParsingNumberStyleProperty = |
||||
|
AvaloniaProperty.RegisterDirect<CommonNumericUpDown<T>, NumberStyles>(nameof(ParsingNumberStyle), |
||||
|
updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes new instance of the <see cref="CommonNumericUpDown{T}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="fromText">Delegate to parse value from text.</param>
|
||||
|
/// <param name="fromDecimal">Delegate to parse value from decimal.</param>
|
||||
|
/// <param name="fromLowerThan">Delegate to compare if one value is lower than another.</param>
|
||||
|
/// <param name="fromGreaterThan">Delegate to compare if one value is greater than another.</param>
|
||||
|
protected CommonNumericUpDown(FromText fromText, FromDecimal fromDecimal, Func<T, T, bool> fromLowerThan, Func<T, T, bool> fromGreaterThan) |
||||
|
{ |
||||
|
_fromText = fromText ?? throw new ArgumentNullException(nameof(fromText)); |
||||
|
_fromDecimal = fromDecimal ?? throw new ArgumentNullException(nameof(fromDecimal)); |
||||
|
_fromLowerThan = fromLowerThan ?? throw new ArgumentNullException(nameof(fromLowerThan)); |
||||
|
_fromGreaterThan = fromGreaterThan ?? throw new ArgumentNullException(nameof(fromGreaterThan)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any.
|
||||
|
/// </summary>
|
||||
|
public NumberStyles ParsingNumberStyle |
||||
|
{ |
||||
|
get { return _parsingNumberStyle; } |
||||
|
set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void OnIncrement() |
||||
|
{ |
||||
|
if (!HandleNullSpin()) |
||||
|
{ |
||||
|
var result = IncrementValue(Value.Value, Increment.Value); |
||||
|
Value = CoerceValueMinMax(result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void OnDecrement() |
||||
|
{ |
||||
|
if (!HandleNullSpin()) |
||||
|
{ |
||||
|
var result = DecrementValue(Value.Value, Increment.Value); |
||||
|
Value = CoerceValueMinMax(result); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void OnMinimumChanged(T? oldValue, T? newValue) |
||||
|
{ |
||||
|
base.OnMinimumChanged(oldValue, newValue); |
||||
|
|
||||
|
if (Value.HasValue && ClipValueToMinMax) |
||||
|
{ |
||||
|
Value = CoerceValueMinMax(Value.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void OnMaximumChanged(T? oldValue, T? newValue) |
||||
|
{ |
||||
|
base.OnMaximumChanged(oldValue, newValue); |
||||
|
|
||||
|
if (Value.HasValue && ClipValueToMinMax) |
||||
|
{ |
||||
|
Value = CoerceValueMinMax(Value.Value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override T? ConvertTextToValue(string text) |
||||
|
{ |
||||
|
T? result = null; |
||||
|
|
||||
|
if (string.IsNullOrEmpty(text)) |
||||
|
{ |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
// Since the conversion from Value to text using a FormartString may not be parsable,
|
||||
|
// we verify that the already existing text is not the exact same value.
|
||||
|
var currentValueText = ConvertValueToText(); |
||||
|
if (Equals(currentValueText, text)) |
||||
|
{ |
||||
|
return Value; |
||||
|
} |
||||
|
|
||||
|
result = ConvertTextToValueCore(currentValueText, text); |
||||
|
|
||||
|
if (ClipValueToMinMax) |
||||
|
{ |
||||
|
return GetClippedMinMaxValue(result); |
||||
|
} |
||||
|
|
||||
|
ValidateDefaultMinMax(result); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override string ConvertValueToText() |
||||
|
{ |
||||
|
if (Value == null) |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
|
||||
|
//Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
|
||||
|
if (FormatString.Contains("{0")) |
||||
|
{ |
||||
|
return string.Format(CultureInfo, FormatString, Value.Value); |
||||
|
} |
||||
|
|
||||
|
return Value.Value.ToString(FormatString, CultureInfo); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void SetValidSpinDirection() |
||||
|
{ |
||||
|
var validDirections = ValidSpinDirections.None; |
||||
|
|
||||
|
// Null increment always prevents spin.
|
||||
|
if (Increment != null && !IsReadOnly) |
||||
|
{ |
||||
|
if (IsLowerThan(Value, Maximum) || !Value.HasValue || !Maximum.HasValue) |
||||
|
{ |
||||
|
validDirections = validDirections | ValidSpinDirections.Increase; |
||||
|
} |
||||
|
|
||||
|
if (IsGreaterThan(Value, Minimum) || !Value.HasValue || !Minimum.HasValue) |
||||
|
{ |
||||
|
validDirections = validDirections | ValidSpinDirections.Decrease; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Spinner != null) |
||||
|
{ |
||||
|
Spinner.ValidSpinDirection = validDirections; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks if provided value is within allowed values.
|
||||
|
/// </summary>
|
||||
|
/// <param name="allowedValues">The alowed values.</param>
|
||||
|
/// <param name="valueToCompare">The value to check.</param>
|
||||
|
protected void TestInputSpecialValue(AllowedSpecialValues allowedValues, AllowedSpecialValues valueToCompare) |
||||
|
{ |
||||
|
if ((allowedValues & valueToCompare) != valueToCompare) |
||||
|
{ |
||||
|
switch (valueToCompare) |
||||
|
{ |
||||
|
case AllowedSpecialValues.NaN: |
||||
|
throw new InvalidDataException("Value to parse shouldn't be NaN."); |
||||
|
case AllowedSpecialValues.PositiveInfinity: |
||||
|
throw new InvalidDataException("Value to parse shouldn't be Positive Infinity."); |
||||
|
case AllowedSpecialValues.NegativeInfinity: |
||||
|
throw new InvalidDataException("Value to parse shouldn't be Negative Infinity."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected static void UpdateMetadata(Type type, T? increment, T? minimun, T? maximum) |
||||
|
{ |
||||
|
IncrementProperty.OverrideDefaultValue(type, increment); |
||||
|
MinimumProperty.OverrideDefaultValue(type, minimun); |
||||
|
MaximumProperty.OverrideDefaultValue(type, maximum); |
||||
|
} |
||||
|
|
||||
|
protected abstract T IncrementValue(T value, T increment); |
||||
|
|
||||
|
protected abstract T DecrementValue(T value, T increment); |
||||
|
|
||||
|
private bool IsLowerThan(T? value1, T? value2) |
||||
|
{ |
||||
|
if (value1 == null || value2 == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
return _fromLowerThan(value1.Value, value2.Value); |
||||
|
} |
||||
|
|
||||
|
private bool IsGreaterThan(T? value1, T? value2) |
||||
|
{ |
||||
|
if (value1 == null || value2 == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
return _fromGreaterThan(value1.Value, value2.Value); |
||||
|
} |
||||
|
|
||||
|
private bool HandleNullSpin() |
||||
|
{ |
||||
|
if (!Value.HasValue) |
||||
|
{ |
||||
|
var forcedValue = DefaultValue ?? default(T); |
||||
|
Value = CoerceValueMinMax(forcedValue); |
||||
|
return true; |
||||
|
} |
||||
|
else if (!Increment.HasValue) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
internal bool IsValid(T? value) |
||||
|
{ |
||||
|
return !IsLowerThan(value, Minimum) && !IsGreaterThan(value, Maximum); |
||||
|
} |
||||
|
|
||||
|
private T? CoerceValueMinMax(T value) |
||||
|
{ |
||||
|
if (IsLowerThan(value, Minimum)) |
||||
|
{ |
||||
|
return Minimum; |
||||
|
} |
||||
|
else if (IsGreaterThan(value, Maximum)) |
||||
|
{ |
||||
|
return Maximum; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool IsPercent(string stringToTest) |
||||
|
{ |
||||
|
var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); |
||||
|
if (PIndex >= 0) |
||||
|
{ |
||||
|
//stringToTest contains a "P" between 2 "'", it's considered as text, not percent
|
||||
|
var isText = stringToTest.Substring(0, PIndex).Contains("'") |
||||
|
&& stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); |
||||
|
|
||||
|
return !isText; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private T? ConvertTextToValueCore(string currentValueText, string text) |
||||
|
{ |
||||
|
T? result; |
||||
|
|
||||
|
if (IsPercent(FormatString)) |
||||
|
{ |
||||
|
result = _fromDecimal(ParsePercent(text, CultureInfo)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Problem while converting new text
|
||||
|
if (!_fromText(text, ParsingNumberStyle, CultureInfo, out T outputValue)) |
||||
|
{ |
||||
|
var shouldThrow = true; |
||||
|
|
||||
|
// Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
|
||||
|
if (!_fromText(currentValueText, ParsingNumberStyle, CultureInfo, out T _)) |
||||
|
{ |
||||
|
// extract non-digit characters
|
||||
|
var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); |
||||
|
var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); |
||||
|
// same non-digit characters on currentValueText and new text => remove them on new Text to parse it again.
|
||||
|
if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) |
||||
|
{ |
||||
|
foreach (var character in textSpecialCharacters) |
||||
|
{ |
||||
|
text = text.Replace(character.ToString(), string.Empty); |
||||
|
} |
||||
|
// if without the special characters, parsing is good, do not throw
|
||||
|
if (_fromText(text, ParsingNumberStyle, CultureInfo, out outputValue)) |
||||
|
{ |
||||
|
shouldThrow = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (shouldThrow) |
||||
|
{ |
||||
|
throw new InvalidDataException("Input string was not in a correct format."); |
||||
|
} |
||||
|
} |
||||
|
result = outputValue; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private T? GetClippedMinMaxValue(T? result) |
||||
|
{ |
||||
|
if (IsGreaterThan(result, Maximum)) |
||||
|
{ |
||||
|
return Maximum; |
||||
|
} |
||||
|
else if (IsLowerThan(result, Minimum)) |
||||
|
{ |
||||
|
return Minimum; |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private void ValidateDefaultMinMax(T? value) |
||||
|
{ |
||||
|
// DefaultValue is always accepted.
|
||||
|
if (Equals(value, DefaultValue)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (IsLowerThan(value, Minimum)) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); |
||||
|
} |
||||
|
else if (IsGreaterThan(value, Maximum)) |
||||
|
{ |
||||
|
throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue