Browse Source

Merge pull request #8259 from timunie/feature/MakeNumericUpDownNullable-2

Make NumericUpDown's Value nullable
pull/8365/head
Max Katz 4 years ago
committed by GitHub
parent
commit
717483653a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  2. 111
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  3. 6
      src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs

2
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -65,7 +65,7 @@
Margin="2" HorizontalAlignment="Center"/> Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock> <TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center" <NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding DecimalValue}" VerticalAlignment="Center"
Margin="2" HorizontalAlignment="Center"/> Margin="2" HorizontalAlignment="Center"/>

111
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -107,8 +107,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Value"/> property. /// Defines the <see cref="Value"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<NumericUpDown, decimal> ValueProperty = public static readonly DirectProperty<NumericUpDown, decimal?> ValueProperty =
AvaloniaProperty.RegisterDirect<NumericUpDown, decimal>(nameof(Value), updown => updown.Value, AvaloniaProperty.RegisterDirect<NumericUpDown, decimal?>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true); (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay, enableDataValidation: true);
/// <summary> /// <summary>
@ -131,7 +131,7 @@ namespace Avalonia.Controls
private IDisposable? _textBoxTextChangedSubscription; private IDisposable? _textBoxTextChangedSubscription;
private decimal _value; private decimal? _value;
private string? _text; private string? _text;
private bool _internalValueSet; private bool _internalValueSet;
private bool _clipValueToMinMax; private bool _clipValueToMinMax;
@ -277,7 +277,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets the value. /// Gets or sets the value.
/// </summary> /// </summary>
public decimal Value public decimal? Value
{ {
get { return _value; } get { return _value; }
set set
@ -351,7 +351,7 @@ namespace Avalonia.Controls
/// <inheritdoc /> /// <inheritdoc />
protected override void OnLostFocus(RoutedEventArgs e) protected override void OnLostFocus(RoutedEventArgs e)
{ {
CommitInput(); CommitInput(true);
base.OnLostFocus(e); base.OnLostFocus(e);
} }
@ -489,9 +489,9 @@ namespace Avalonia.Controls
{ {
SetValidSpinDirection(); SetValidSpinDirection();
} }
if (ClipValueToMinMax) if (ClipValueToMinMax && Value.HasValue)
{ {
Value = MathUtilities.Clamp(Value, Minimum, Maximum); Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum);
} }
} }
@ -506,9 +506,9 @@ namespace Avalonia.Controls
{ {
SetValidSpinDirection(); SetValidSpinDirection();
} }
if (ClipValueToMinMax) if (ClipValueToMinMax && Value.HasValue)
{ {
Value = MathUtilities.Clamp(Value, Minimum, Maximum); Value = MathUtilities.Clamp(Value.Value, Minimum, Maximum);
} }
} }
@ -530,7 +530,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="oldValue">The old value.</param> /// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param> /// <param name="newValue">The new value.</param>
protected virtual void OnValueChanged(decimal oldValue, decimal newValue) protected virtual void OnValueChanged(decimal? oldValue, decimal? newValue)
{ {
if (!_internalValueSet && IsInitialized) if (!_internalValueSet && IsInitialized)
{ {
@ -573,7 +573,7 @@ namespace Avalonia.Controls
/// Called when the <see cref="Value"/> property has to be coerced. /// Called when the <see cref="Value"/> property has to be coerced.
/// </summary> /// </summary>
/// <param name="baseValue">The value.</param> /// <param name="baseValue">The value.</param>
protected virtual decimal OnCoerceValue(decimal baseValue) protected virtual decimal? OnCoerceValue(decimal? baseValue)
{ {
return baseValue; return baseValue;
} }
@ -607,7 +607,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
/// <param name="oldValue">The old value.</param> /// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param> /// <param name="newValue">The new value.</param>
protected virtual void RaiseValueChangedEvent(decimal oldValue, decimal newValue) protected virtual void RaiseValueChangedEvent(decimal? oldValue, decimal? newValue)
{ {
var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue);
RaiseEvent(e); RaiseEvent(e);
@ -616,9 +616,9 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Converts the formatted text to a value. /// Converts the formatted text to a value.
/// </summary> /// </summary>
private decimal ConvertTextToValue(string text) private decimal? ConvertTextToValue(string? text)
{ {
decimal result = 0; decimal? result = null;
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
{ {
@ -635,9 +635,9 @@ namespace Avalonia.Controls
result = ConvertTextToValueCore(currentValueText, text); result = ConvertTextToValueCore(currentValueText, text);
if (ClipValueToMinMax) if (ClipValueToMinMax && result.HasValue)
{ {
return MathUtilities.Clamp(result, Minimum, Maximum); return MathUtilities.Clamp(result.Value, Minimum, Maximum);
} }
ValidateMinMax(result); ValidateMinMax(result);
@ -649,7 +649,7 @@ namespace Avalonia.Controls
/// Converts the value to formatted text. /// Converts the value to formatted text.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private string ConvertValueToText() private string? ConvertValueToText()
{ {
//Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind.
if (FormatString.Contains("{0")) if (FormatString.Contains("{0"))
@ -657,7 +657,7 @@ namespace Avalonia.Controls
return string.Format(NumberFormat, FormatString, Value); return string.Format(NumberFormat, FormatString, Value);
} }
return Value.ToString(FormatString, NumberFormat); return Value?.ToString(FormatString, NumberFormat);
} }
/// <summary> /// <summary>
@ -665,7 +665,16 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private void OnIncrement() private void OnIncrement()
{ {
var result = Value + Increment; decimal result;
if (Value.HasValue)
{
result = Value.Value + Increment;
}
else
{
result = Minimum;
}
Value = MathUtilities.Clamp(result, Minimum, Maximum); Value = MathUtilities.Clamp(result, Minimum, Maximum);
} }
@ -674,7 +683,17 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
private void OnDecrement() private void OnDecrement()
{ {
var result = Value - Increment; decimal result;
if (Value.HasValue)
{
result = Value.Value - Increment;
}
else
{
result = Maximum;
}
Value = MathUtilities.Clamp(result, Minimum, Maximum); Value = MathUtilities.Clamp(result, Minimum, Maximum);
} }
@ -688,6 +707,11 @@ namespace Avalonia.Controls
// Zero increment always prevents spin. // Zero increment always prevents spin.
if (Increment != 0 && !IsReadOnly) if (Increment != 0 && !IsReadOnly)
{ {
if (!Value.HasValue)
{
validDirections = ValidSpinDirections.Increase | ValidSpinDirections.Decrease;
}
if (Value < Maximum) if (Value < Maximum)
{ {
validDirections = validDirections | ValidSpinDirections.Increase; validDirections = validDirections | ValidSpinDirections.Increase;
@ -825,13 +849,13 @@ namespace Avalonia.Controls
{ {
if (e.Sender is NumericUpDown upDown) if (e.Sender is NumericUpDown upDown)
{ {
var oldValue = (decimal)e.OldValue!; var oldValue = (decimal?)e.OldValue;
var newValue = (decimal)e.NewValue!; var newValue = (decimal?)e.NewValue;
upDown.OnValueChanged(oldValue, newValue); upDown.OnValueChanged(oldValue, newValue);
} }
} }
private void SetValueInternal(decimal value) private void SetValueInternal(decimal? value)
{ {
_internalValueSet = true; _internalValueSet = true;
try try
@ -946,9 +970,9 @@ namespace Avalonia.Controls
remove { RemoveHandler(ValueChangedEvent, value); } remove { RemoveHandler(ValueChangedEvent, value); }
} }
private bool CommitInput() private bool CommitInput(bool forceTextUpdate = false)
{ {
return SyncTextAndValueProperties(true, Text); return SyncTextAndValueProperties(true, Text, forceTextUpdate);
} }
/// <summary> /// <summary>
@ -978,28 +1002,24 @@ namespace Avalonia.Controls
{ {
if (updateValueFromText) if (updateValueFromText)
{ {
if (!string.IsNullOrEmpty(text)) try
{ {
try var newValue = ConvertTextToValue(text);
if (!Equals(newValue, Value))
{ {
var newValue = ConvertTextToValue(text); SetValueInternal(newValue);
if (!Equals(newValue, Value))
{
SetValueInternal(newValue);
}
}
catch
{
parsedTextIsValid = false;
} }
} }
catch
{
parsedTextIsValid = false;
}
} }
// Do not touch the ongoing text input from user. // Do not touch the ongoing text input from user.
if (!_isTextChangedFromUI) if (!_isTextChangedFromUI)
{ {
var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text); if (forceTextUpdate)
if (!keepEmpty)
{ {
var newText = ConvertValueToText(); var newText = ConvertValueToText();
if (!Equals(Text, newText)) if (!Equals(Text, newText))
@ -1036,10 +1056,15 @@ namespace Avalonia.Controls
return parsedTextIsValid; return parsedTextIsValid;
} }
private decimal ConvertTextToValueCore(string currentValueText, string text) private decimal? ConvertTextToValueCore(string? currentValueText, string? text)
{ {
decimal result; decimal result;
if (string.IsNullOrEmpty(text))
{
return null;
}
if (IsPercent(FormatString)) if (IsPercent(FormatString))
{ {
result = ParsePercent(text, NumberFormat); result = ParsePercent(text, NumberFormat);
@ -1052,7 +1077,7 @@ namespace Avalonia.Controls
var shouldThrow = true; var shouldThrow = true;
// Check if CurrentValueText is also failing => it also contains special characters. ex : 90° // Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
if (!decimal.TryParse(currentValueText, ParsingNumberStyle, NumberFormat, out var _)) if (!string.IsNullOrEmpty(currentValueText) && !decimal.TryParse(currentValueText, ParsingNumberStyle, NumberFormat, out var _))
{ {
// extract non-digit characters // extract non-digit characters
var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c));
@ -1082,8 +1107,12 @@ namespace Avalonia.Controls
return result; return result;
} }
private void ValidateMinMax(decimal value) private void ValidateMinMax(decimal? value)
{ {
if (!value.HasValue)
{
return;
}
if (value < Minimum) if (value < Minimum)
{ {
throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum)); throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum));

6
src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs

@ -4,13 +4,13 @@ namespace Avalonia.Controls
{ {
public class NumericUpDownValueChangedEventArgs : RoutedEventArgs public class NumericUpDownValueChangedEventArgs : RoutedEventArgs
{ {
public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, decimal oldValue, decimal newValue) : base(routedEvent) public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, decimal? oldValue, decimal? newValue) : base(routedEvent)
{ {
OldValue = oldValue; OldValue = oldValue;
NewValue = newValue; NewValue = newValue;
} }
public decimal OldValue { get; } public decimal? OldValue { get; }
public decimal NewValue { get; } public decimal? NewValue { get; }
} }
} }

Loading…
Cancel
Save