Browse Source

Reworked to provide common NumericUpDown control which values are doubles.

pull/1457/head
dzhelnin 9 years ago
parent
commit
d6b5e04f0c
  1. 6
      samples/ControlCatalog/ControlCatalog.csproj
  2. 2
      samples/ControlCatalog/MainView.xaml
  3. 96
      samples/ControlCatalog/Pages/NumbersPage.xaml
  4. 86
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  5. 21
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs
  6. 15
      src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs
  7. 24
      src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs
  8. 346
      src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs
  9. 24
      src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs
  10. 109
      src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs
  11. 24
      src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs
  12. 24
      src/Avalonia.Controls/NumericUpDown/LongUpDown.cs
  13. 1117
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  14. 16
      src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs
  15. 24
      src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs
  16. 106
      src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs
  17. 824
      src/Avalonia.Controls/NumericUpDown/UpDownBase.cs
  18. 2
      src/Avalonia.Themes.Default/NumericUpDown.xaml

6
samples/ControlCatalog/ControlCatalog.csproj

@ -78,7 +78,7 @@
<EmbeddedResource Include="Pages\MenuPage.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Pages\NumbersPage.xaml">
<EmbeddedResource Include="Pages\NumericUpDownPage.xaml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Pages\ProgressBarPage.xaml">
@ -173,8 +173,8 @@
<Compile Include="Pages\ButtonSpinnerPage.xaml.cs">
<DependentUpon>ButtonSpinnerPage.xaml</DependentUpon>
</Compile>
<Compile Include="Pages\NumbersPage.xaml.cs">
<DependentUpon>NumbersPage.xaml</DependentUpon>
<Compile Include="Pages\NumericUpDownPage.xaml.cs">
<DependentUpon>NumericUpDownPage.xaml</DependentUpon>
</Compile>
<Compile Include="Pages\ScreenPage.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

2
samples/ControlCatalog/MainView.xaml

@ -19,7 +19,7 @@
<TabItem Header="Image"><pages:ImagePage/></TabItem>
<TabItem Header="LayoutTransformControl"><pages:LayoutTransformControlPage/></TabItem>
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Numbers"><pages:NumbersPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>

96
samples/ControlCatalog/Pages/NumbersPage.xaml

@ -1,96 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<TextBlock Margin="2" Classes="h1">Numeric up-down controls</TextBlock>
<TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down controls provide a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
<TextBlock Margin="2" Classes="h2">The following controls are available to support various native numeric types:</TextBlock>
<TextBlock Margin="2" Classes="h2">ByteUpDown, ShortUpDown, IntegerUpDown, LongUpDown, SingleUpDown, DoubleUpDown, DecimalUpDown.</TextBlock>
<TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock>
<Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock>
<CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #doubleUpDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">IsReadOnly:</TextBlock>
<CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding #doubleUpDown.IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowSpin:</TextBlock>
<CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding #doubleUpDown.AllowSpin}" IsEnabled="{Binding #doubleUpDown.!IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">ClipValueToMinMax:</TextBlock>
<CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #doubleUpDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">DisplayDefaultValueOnEmptyText:</TextBlock>
<CheckBox Grid.Row="4" Grid.Column="1" IsChecked="{Binding #doubleUpDown.DisplayDefaultValueOnEmptyText}" VerticalAlignment="Center" Margin="2"/>
</Grid>
<Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
<DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
VerticalAlignment="Center" Margin="2">
<DropDown.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Gap="2">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="-"/>
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</DropDown.ItemTemplate>
</DropDown>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
<DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #doubleUpDown.ButtonSpinnerLocation}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
<DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #doubleUpDown.CultureInfo}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #doubleUpDown.Watermark}" VerticalAlignment="Center" Margin="2" />
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #doubleUpDown.Text}" VerticalAlignment="Center" Margin="2" />
</Grid>
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
<DoubleUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #doubleUpDown.Minimum}" AllowInputSpecialValues="NegativeInfinity"
CultureInfo="{Binding #doubleUpDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
<DoubleUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #doubleUpDown.Maximum}" AllowInputSpecialValues="PositiveInfinity"
CultureInfo="{Binding #doubleUpDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
<DoubleUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #doubleUpDown.Increment}" VerticalAlignment="Center"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
<DoubleUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #doubleUpDown.Value}" VerticalAlignment="Center"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">DefaultValue:</TextBlock>
<DoubleUpDown Grid.Row="4" Grid.Column="1" Value="{Binding #doubleUpDown.DefaultValue}" AllowInputSpecialValues="Any"
VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
</Grid>
</Grid>
<TextBlock Margin="2" Classes="h2">DoubleUpDown and SingleUpDown support the AllowInputSpecialValues property</TextBlock>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowInputSpecialValues:</TextBlock>
<DropDown Grid.Row="1" Grid.Column="1" Items="{Binding AllowedSpecialValues}" SelectedItem="{Binding #doubleUpDown.AllowInputSpecialValues}"
VerticalAlignment="Center" Margin="2"/>
</StackPanel>
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of DoubleUpDown:</TextBlock>
<DoubleUpDown Name="doubleUpDown" Minimum="0" Maximum="10" Increment="0.5"
AllowInputSpecialValues="Any" CultureInfo="en-US"
VerticalAlignment="Center" Height="25" Width="100"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
</StackPanel>
</UserControl>

86
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -0,0 +1,86 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Gap="4">
<TextBlock Margin="2" Classes="h1">Numeric up-down control</TextBlock>
<TextBlock Margin="2" Classes="h2" TextWrapping="Wrap">Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.</TextBlock>
<TextBlock Margin="2,5,2,2" FontSize="14" FontWeight="Bold">Features:</TextBlock>
<Grid Margin="2" ColumnDefinitions="Auto,Auto,Auto,Auto" RowDefinitions="Auto,Auto">
<Grid Grid.Row="0" Grid.Column="0" ColumnDefinitions="Auto, Auto" RowDefinitions="35,35,35,35,35">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">ShowButtonSpinner:</TextBlock>
<CheckBox Grid.Row="0" Grid.Column="1" IsChecked="{Binding #upDown.ShowButtonSpinner}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">IsReadOnly:</TextBlock>
<CheckBox Grid.Row="1" Grid.Column="1" IsChecked="{Binding #upDown.IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">AllowSpin:</TextBlock>
<CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding #upDown.AllowSpin}" IsEnabled="{Binding #upDown.!IsReadOnly}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">ClipValueToMinMax:</TextBlock>
<CheckBox Grid.Row="3" Grid.Column="1" IsChecked="{Binding #upDown.ClipValueToMinMax}" VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">DisplayDefaultValueOnEmptyText:</TextBlock>
<CheckBox Grid.Row="4" Grid.Column="1" IsChecked="{Binding #upDown.DisplayDefaultValueOnEmptyText}" VerticalAlignment="Center" Margin="2"/>
</Grid>
<Grid Grid.Row="0" Grid.Column="1" Margin="10,2,2,2" ColumnDefinitions="Auto, 120" RowDefinitions="35,35,35,35,35">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="2">FormatString:</TextBlock>
<DropDown Grid.Row="0" Grid.Column="1" Items="{Binding Formats}" SelectedItem="{Binding SelectedFormat}"
VerticalAlignment="Center" Margin="2">
<DropDown.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Gap="2">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="-"/>
<TextBlock Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</DropDown.ItemTemplate>
</DropDown>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="2">ButtonSpinnerLocation:</TextBlock>
<DropDown Grid.Row="1" Grid.Column="1" Items="{Binding SpinnerLocations}" SelectedItem="{Binding #upDown.ButtonSpinnerLocation}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="2">CultureInfo:</TextBlock>
<DropDown Grid.Row="2" Grid.Column="1" Items="{Binding Cultures}" SelectedItem="{Binding #upDown.CultureInfo}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.Watermark}" VerticalAlignment="Center" Margin="2" />
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
</Grid>
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="35,35,35,35,35" ColumnDefinitions="Auto, 120">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
<NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<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"
Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">DefaultValue:</TextBlock>
<NumericUpDown Grid.Row="4" Grid.Column="1" Value="{Binding #upDown.DefaultValue}"
VerticalAlignment="Center" Height="25" Margin="2" Width="70" HorizontalAlignment="Center"/>
</Grid>
</Grid>
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Gap="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Height="25" Width="100"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
</StackPanel>
</UserControl>

21
samples/ControlCatalog/Pages/NumbersPage.xaml.cs → samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs

@ -10,9 +10,9 @@ using ReactiveUI;
namespace ControlCatalog.Pages
{
public class NumbersPage : UserControl
public class NumericUpDownPage : UserControl
{
public NumbersPage()
public NumericUpDownPage()
{
this.InitializeComponent();
var viewModel = new NumbersPageViewModel();
@ -31,7 +31,6 @@ namespace ControlCatalog.Pages
private IList<FormatObject> _formats;
private FormatObject _selectedFormat;
private IList<Location> _spinnerLocations;
private IList<AllowedSpecialValues> _allowedSpecialValues;
public NumbersPageViewModel()
{
@ -80,22 +79,6 @@ namespace ControlCatalog.Pages
new CultureInfo("cs-CZ")
};
public IList<AllowedSpecialValues> AllowedSpecialValues
{
get
{
if (_allowedSpecialValues == null)
{
_allowedSpecialValues = new List<AllowedSpecialValues>();
foreach (AllowedSpecialValues value in Enum.GetValues(typeof(AllowedSpecialValues)))
{
_allowedSpecialValues.Add(value);
}
}
return _allowedSpecialValues;
}
}
public FormatObject SelectedFormat
{
get { return _selectedFormat; }

15
src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs

@ -1,15 +0,0 @@
using System;
namespace Avalonia.Controls
{
[Flags]
public enum AllowedSpecialValues
{
None = 0,
NaN = 1,
PositiveInfinity = 2,
NegativeInfinity = 4,
AnyInfinity = PositiveInfinity | NegativeInfinity,
Any = NaN | AnyInfinity
}
}

24
src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs

@ -1,24 +0,0 @@
namespace Avalonia.Controls
{
/// <inheritdoc />
public class ByteUpDown : CommonNumericUpDown<byte>
{
/// <summary>
/// Initializes static members of the <see cref="ByteUpDown"/> class.
/// </summary>
static ByteUpDown() => UpdateMetadata(typeof(ByteUpDown), 1, byte.MinValue, byte.MaxValue);
/// <summary>
/// Initializes new instance of the <see cref="ByteUpDown"/> class.
/// </summary>
public ByteUpDown() : base(byte.TryParse, decimal.ToByte, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2)
{
}
/// <inheritdoc />
protected override byte IncrementValue(byte value, byte increment) => (byte)(value + increment);
/// <inheritdoc />
protected override byte DecrementValue(byte value, byte increment) => (byte)(value - increment);
}
}

346
src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs

@ -1,346 +0,0 @@
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 nullable 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));
}
}
}
}

24
src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs

@ -1,24 +0,0 @@
namespace Avalonia.Controls
{
/// <inheritdoc />
public class DecimalUpDown : CommonNumericUpDown<decimal>
{
/// <summary>
/// Initializes static members of the <see cref="DecimalUpDown"/> class.
/// </summary>
static DecimalUpDown() => UpdateMetadata(typeof(DecimalUpDown), 1m, decimal.MinValue, decimal.MaxValue);
/// <summary>
/// Initializes new instance of the <see cref="DecimalUpDown"/> class.
/// </summary>
public DecimalUpDown() : base(decimal.TryParse, d => d, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2)
{
}
/// <inheritdoc />
protected override decimal IncrementValue(decimal value, decimal increment) => value + increment;
/// <inheritdoc />
protected override decimal DecrementValue(decimal value, decimal increment) => value - increment;
}
}

109
src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs

@ -1,109 +0,0 @@
using System;
namespace Avalonia.Controls
{
/// <inheritdoc />
public class DoubleUpDown : CommonNumericUpDown<double>
{
/// <summary>
/// Defines the <see cref="AllowInputSpecialValues"/> property.
/// </summary>
public static readonly DirectProperty<DoubleUpDown, AllowedSpecialValues> AllowInputSpecialValuesProperty =
AvaloniaProperty.RegisterDirect<DoubleUpDown, AllowedSpecialValues>(nameof(AllowInputSpecialValues),
updown => updown.AllowInputSpecialValues, (updown, v) => updown.AllowInputSpecialValues = v);
private AllowedSpecialValues _allowInputSpecialValues;
/// <summary>
/// Initializes static members of the <see cref="DoubleUpDown"/> class.
/// </summary>
static DoubleUpDown() => UpdateMetadata(typeof(DoubleUpDown), 1d, double.NegativeInfinity, double.PositiveInfinity);
/// <summary>
/// Initializes new instance of the <see cref="DoubleUpDown"/> class.
/// </summary>
public DoubleUpDown() : base(double.TryParse, decimal.ToDouble, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2)
{
}
/// <summary>
/// Gets or sets a value representing the special values the user is allowed to input, such as "Infinity", "-Infinity" and "NaN" values.
/// </summary>
public AllowedSpecialValues AllowInputSpecialValues
{
get { return _allowInputSpecialValues; }
set { SetAndRaise(AllowInputSpecialValuesProperty, ref _allowInputSpecialValues, value); }
}
/// <inheritdoc />
protected override double IncrementValue(double value, double increment) => value + increment;
/// <inheritdoc />
protected override double DecrementValue(double value, double increment) => value - increment;
/// <inheritdoc />
protected override double? OnCoerceIncrement(double? baseValue)
{
if (baseValue.HasValue && double.IsNaN(baseValue.Value))
{
throw new ArgumentException("NaN is invalid for Increment.");
}
return base.OnCoerceIncrement(baseValue);
}
/// <inheritdoc />
protected override double? OnCoerceMaximum(double? baseValue)
{
if (baseValue.HasValue && double.IsNaN(baseValue.Value))
{
throw new ArgumentException("NaN is invalid for Maximum.");
}
return base.OnCoerceMaximum(baseValue);
}
/// <inheritdoc />
protected override double? OnCoerceMinimum(double? baseValue)
{
if (baseValue.HasValue && double.IsNaN(baseValue.Value))
{
throw new ArgumentException("NaN is invalid for Minimum.");
}
return base.OnCoerceMinimum(baseValue);
}
/// <inheritdoc />
protected override void SetValidSpinDirection()
{
if (Value.HasValue && double.IsInfinity(Value.Value) && (Spinner != null))
{
Spinner.ValidSpinDirection = ValidSpinDirections.None;
}
else
{
base.SetValidSpinDirection();
}
}
/// <inheritdoc />
protected override double? ConvertTextToValue(string text)
{
var result = base.ConvertTextToValue(text);
if (result != null)
{
if (double.IsNaN(result.Value))
{
TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NaN);
}
else if (double.IsPositiveInfinity(result.Value))
{
TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.PositiveInfinity);
}
else if (double.IsNegativeInfinity(result.Value))
{
TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NegativeInfinity);
}
}
return result;
}
}
}

24
src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs

@ -1,24 +0,0 @@
namespace Avalonia.Controls
{
/// <inheritdoc />
public class IntegerUpDown : CommonNumericUpDown<int>
{
/// <summary>
/// Initializes static members of the <see cref="IntegerUpDown"/> class.
/// </summary>
static IntegerUpDown() => UpdateMetadata(typeof(IntegerUpDown), 1, int.MinValue, int.MaxValue);
/// <summary>
/// Initializes new instance of the <see cref="IntegerUpDown"/> class.
/// </summary>
public IntegerUpDown() : base(int.TryParse, decimal.ToInt32, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2)
{
}
/// <inheritdoc />
protected override int IncrementValue(int value, int increment) => value + increment;
/// <inheritdoc />
protected override int DecrementValue(int value, int increment) => value - increment;
}
}

24
src/Avalonia.Controls/NumericUpDown/LongUpDown.cs

@ -1,24 +0,0 @@
namespace Avalonia.Controls
{
/// <inheritdoc />
public class LongUpDown : CommonNumericUpDown<long>
{
/// <summary>
/// Initializes static members of the <see cref="LongUpDown"/> class.
/// </summary>
static LongUpDown() => UpdateMetadata(typeof(LongUpDown), 1L, long.MinValue, long.MaxValue);
/// <summary>
/// Initializes new instance of the <see cref="LongUpDown"/> class.
/// </summary>
public LongUpDown() : base(long.TryParse, decimal.ToInt64, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2)
{
}
/// <inheritdoc />
protected override long IncrementValue(long value, long increment) => value + increment;
/// <inheritdoc />
protected override long DecrementValue(long value, long increment) => value - increment;
}
}

1117
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

File diff suppressed because it is too large

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

@ -0,0 +1,16 @@
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
public class NumericUpDownValueChangedEventArgs : RoutedEventArgs
{
public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double? oldValue, double? newValue) : base(routedEvent)
{
OldValue = oldValue;
NewValue = newValue;
}
public double? OldValue { get; }
public double? NewValue { get; }
}
}

24
src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs

@ -1,24 +0,0 @@
namespace Avalonia.Controls
{
/// <inheritdoc />
public class ShortUpDown : CommonNumericUpDown<short>
{
/// <summary>
/// Initializes static members of the <see cref="ShortUpDown"/> class.
/// </summary>
static ShortUpDown() => UpdateMetadata(typeof(ShortUpDown), 1, short.MinValue, short.MaxValue);
/// <summary>
/// Initializes new instance of the <see cref="ShortUpDown"/> class.
/// </summary>
public ShortUpDown() : base(short.TryParse, decimal.ToInt16, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2)
{
}
/// <inheritdoc />
protected override short IncrementValue(short value, short increment) => (short)(value + increment);
/// <inheritdoc />
protected override short DecrementValue(short value, short increment) => (short)(value - increment);
}
}

106
src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs

@ -1,106 +0,0 @@
using System;
namespace Avalonia.Controls
{
/// <inheritdoc />
public class SingleUpDown : CommonNumericUpDown<float>
{
/// <summary>
/// Defines the <see cref="AllowInputSpecialValues"/> property.
/// </summary>
public static readonly DirectProperty<SingleUpDown, AllowedSpecialValues> AllowInputSpecialValuesProperty =
AvaloniaProperty.RegisterDirect<SingleUpDown, AllowedSpecialValues>(nameof(AllowInputSpecialValues),
updown => updown.AllowInputSpecialValues, (updown, v) => updown.AllowInputSpecialValues = v);
private AllowedSpecialValues _allowInputSpecialValues;
/// <summary>
/// Initializes static members of the <see cref="SingleUpDown"/> class.
/// </summary>
static SingleUpDown() => UpdateMetadata(typeof(SingleUpDown), 1f, float.NegativeInfinity, float.PositiveInfinity);
/// <summary>
/// Initializes new instance of the <see cref="SingleUpDown"/> class.
/// </summary>
public SingleUpDown() : base(float.TryParse, decimal.ToSingle, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2)
{
}
/// <summary>
/// Gets or sets a value representing the special values the user is allowed to input, such as "Infinity", "-Infinity" and "NaN" values.
/// </summary>
public AllowedSpecialValues AllowInputSpecialValues
{
get { return _allowInputSpecialValues; }
set { SetAndRaise(AllowInputSpecialValuesProperty, ref _allowInputSpecialValues, value); }
}
/// <inheritdoc />
protected override float? OnCoerceIncrement(float? baseValue)
{
if (baseValue.HasValue && float.IsNaN(baseValue.Value))
throw new ArgumentException("NaN is invalid for Increment.");
return base.OnCoerceIncrement(baseValue);
}
/// <inheritdoc />
protected override float? OnCoerceMaximum(float? baseValue)
{
if (baseValue.HasValue && float.IsNaN(baseValue.Value))
throw new ArgumentException("NaN is invalid for Maximum.");
return base.OnCoerceMaximum(baseValue);
}
/// <inheritdoc />
protected override float? OnCoerceMinimum(float? baseValue)
{
if (baseValue.HasValue && float.IsNaN(baseValue.Value))
throw new ArgumentException("NaN is invalid for Minimum.");
return base.OnCoerceMinimum(baseValue);
}
/// <inheritdoc />
protected override float IncrementValue(float value, float increment) => value + increment;
/// <inheritdoc />
protected override float DecrementValue(float value, float increment) => value - increment;
/// <inheritdoc />
protected override void SetValidSpinDirection()
{
if (Value.HasValue && float.IsInfinity(Value.Value) && (Spinner != null))
{
Spinner.ValidSpinDirection = ValidSpinDirections.None;
}
else
{
base.SetValidSpinDirection();
}
}
/// <inheritdoc />
protected override float? ConvertTextToValue(string text)
{
var result = base.ConvertTextToValue(text);
if (result != null)
{
if (float.IsNaN(result.Value))
{
TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NaN);
}
else if (float.IsPositiveInfinity(result.Value))
{
TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.PositiveInfinity);
}
else if (float.IsNegativeInfinity(result.Value))
{
TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NegativeInfinity);
}
}
return result;
}
}
}

824
src/Avalonia.Controls/NumericUpDown/UpDownBase.cs

@ -1,824 +0,0 @@
using System;
using System.Globalization;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace Avalonia.Controls
{
public abstract class UpDownBase : TemplatedControl
{
}
/// <summary>
/// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing values.
/// </summary>
public abstract class UpDownBase<T> : UpDownBase
{
/// <summary>
/// Defines the <see cref="AllowSpin"/> property.
/// </summary>
public static readonly StyledProperty<bool> AllowSpinProperty =
ButtonSpinner.AllowSpinProperty.AddOwner<UpDownBase<T>>();
/// <summary>
/// Defines the <see cref="ButtonSpinnerLocation"/> property.
/// </summary>
public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<UpDownBase<T>>();
/// <summary>
/// Defines the <see cref="ShowButtonSpinner"/> property.
/// </summary>
public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<UpDownBase<T>>();
/// <summary>
/// Defines the <see cref="ClipValueToMinMax"/> property.
/// </summary>
public static readonly DirectProperty<UpDownBase<T>, bool> ClipValueToMinMaxProperty =
AvaloniaProperty.RegisterDirect<UpDownBase<T>, bool>(nameof(ClipValueToMinMax),
updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
/// <summary>
/// Defines the <see cref="CultureInfo"/> property.
/// </summary>
public static readonly DirectProperty<UpDownBase<T>, CultureInfo> CultureInfoProperty =
AvaloniaProperty.RegisterDirect<UpDownBase<T>, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo,
(o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture);
/// <summary>
/// Defines the <see cref="DefaultValue"/> property.
/// </summary>
public static readonly StyledProperty<T> DefaultValueProperty =
AvaloniaProperty.Register<UpDownBase<T>, T>(nameof(DefaultValue));
/// <summary>
/// Defines the <see cref="DisplayDefaultValueOnEmptyText"/> property.
/// </summary>
public static readonly StyledProperty<bool> DisplayDefaultValueOnEmptyTextProperty =
AvaloniaProperty.Register<UpDownBase<T>, bool>(nameof(DisplayDefaultValueOnEmptyText));
/// <summary>
/// Defines the <see cref="IsReadOnly"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsReadOnlyProperty =
AvaloniaProperty.Register<UpDownBase<T>, bool>(nameof(IsReadOnly));
/// <summary>
/// Defines the <see cref="Maximum"/> property.
/// </summary>
public static readonly StyledProperty<T> MaximumProperty =
AvaloniaProperty.Register<UpDownBase<T>, T>(nameof(Maximum), validate: OnCoerceMaximum);
/// <summary>
/// Defines the <see cref="Minimum"/> property.
/// </summary>
public static readonly StyledProperty<T> MinimumProperty =
AvaloniaProperty.Register<UpDownBase<T>, T>(nameof(Minimum), validate: OnCoerceMinimum);
/// <summary>
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly DirectProperty<UpDownBase<T>, string> TextProperty =
AvaloniaProperty.RegisterDirect<UpDownBase<T>, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="Value"/> property.
/// </summary>
public static readonly DirectProperty<UpDownBase<T>, T> ValueProperty =
AvaloniaProperty.RegisterDirect<UpDownBase<T>, T>(nameof(Value), updown => updown.Value,
(updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="Watermark"/> property.
/// </summary>
public static readonly StyledProperty<string> WatermarkProperty =
AvaloniaProperty.Register<UpDownBase<T>, string>(nameof(Watermark));
private IDisposable _textBoxTextChangedSubscription;
private T _value;
private string _text;
private bool _internalValueSet;
private bool _clipValueToMinMax;
private bool _isSyncingTextAndValueProperties;
private bool _isTextChangedFromUI;
private CultureInfo _cultureInfo;
/// <summary>
/// Gets the Spinner template part.
/// </summary>
protected Spinner Spinner { get; private set; }
/// <summary>
/// Gets the TextBox template part.
/// </summary>
protected TextBox TextBox { get; private set; }
/// <summary>
/// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
/// </summary>
public bool AllowSpin
{
get { return GetValue(AllowSpinProperty); }
set { SetValue(AllowSpinProperty, value); }
}
/// <summary>
/// Gets or sets current location of the <see cref="ButtonSpinner"/>.
/// </summary>
public Location ButtonSpinnerLocation
{
get { return GetValue(ButtonSpinnerLocationProperty); }
set { SetValue(ButtonSpinnerLocationProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the spin buttons should be shown.
/// </summary>
public bool ShowButtonSpinner
{
get { return GetValue(ShowButtonSpinnerProperty); }
set { SetValue(ShowButtonSpinnerProperty, value); }
}
/// <summary>
/// Gets or sets if the value should be clipped when minimum/maximum is reached.
/// </summary>
public bool ClipValueToMinMax
{
get { return _clipValueToMinMax; }
set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); }
}
/// <summary>
/// Gets or sets the current CultureInfo.
/// </summary>
public CultureInfo CultureInfo
{
get { return _cultureInfo; }
set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); }
}
/// <summary>
/// Gets or sets the value to use when the <see cref="Value"/> is null and an increment/decrement operation is performed.
/// </summary>
public T DefaultValue
{
get { return GetValue(DefaultValueProperty); }
set { SetValue(DefaultValueProperty, value); }
}
/// <summary>
/// Gets or sets if the defaultValue should be displayed when the Text is empty.
/// </summary>
public bool DisplayDefaultValueOnEmptyText
{
get { return GetValue(DisplayDefaultValueOnEmptyTextProperty); }
set { SetValue(DisplayDefaultValueOnEmptyTextProperty, value); }
}
/// <summary>
/// Gets or sets if the control is read only.
/// </summary>
public bool IsReadOnly
{
get { return GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
/// <summary>
/// Gets or sets the maximum allowed value.
/// </summary>
public T Maximum
{
get { return GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
/// <summary>
/// Gets or sets the minimum allowed value.
/// </summary>
public T Minimum
{
get { return GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
/// <summary>
/// Gets or sets the formatted string representation of the value.
/// </summary>
public string Text
{
get { return _text; }
set { SetAndRaise(TextProperty, ref _text, value); }
}
/// <summary>
/// Gets or sets the value.
/// </summary>
public T Value
{
get { return _value; }
set
{
value = OnCoerceValue(value);
SetAndRaise(ValueProperty, ref _value, value);
}
}
/// <summary>
/// Gets or sets the object to use as a watermark if the <see cref="Value"/> is null.
/// </summary>
public string Watermark
{
get { return GetValue(WatermarkProperty); }
set { SetValue(WatermarkProperty, value); }
}
/// <summary>
/// Initializes new instance of <see cref="UpDownBase{T}"/> class.
/// </summary>
protected UpDownBase()
{
Initialized += (sender, e) =>
{
if (!_internalValueSet && IsInitialized)
{
SyncTextAndValueProperties(false, null, true);
}
SetValidSpinDirection();
};
}
/// <summary>
/// Initializes static members of the <see cref="UpDownBase{T}"/> class.
/// </summary>
static UpDownBase()
{
CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
DefaultValueProperty.Changed.Subscribe(OnDefaultValueChanged);
DisplayDefaultValueOnEmptyTextProperty.Changed.Subscribe(OnDisplayDefaultValueOnEmptyTextChanged);
IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged);
MaximumProperty.Changed.Subscribe(OnMaximumChanged);
MinimumProperty.Changed.Subscribe(OnMinimumChanged);
TextProperty.Changed.Subscribe(OnTextChanged);
ValueProperty.Changed.Subscribe(OnValueChanged);
}
/// <inheritdoc />
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
if (TextBox != null)
{
TextBox.PointerPressed -= TextBoxOnPointerPressed;
_textBoxTextChangedSubscription?.Dispose();
}
TextBox = e.NameScope.Find<TextBox>("PART_TextBox");
if (TextBox != null)
{
TextBox.Text = Text;
TextBox.PointerPressed += TextBoxOnPointerPressed;
_textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged());
}
if (Spinner != null)
{
Spinner.Spin -= OnSpinnerSpin;
}
Spinner = e.NameScope.Find<Spinner>("PART_Spinner");
if (Spinner != null)
{
Spinner.Spin += OnSpinnerSpin;
}
SetValidSpinDirection();
}
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Enter:
var commitSuccess = CommitInput();
e.Handled = !commitSuccess;
break;
}
}
/// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue)
{
if (IsInitialized)
{
SyncTextAndValueProperties(false, null);
}
}
/// <summary>
/// Called when the <see cref="DefaultValue"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnDefaultValueChanged(T oldValue, T newValue)
{
if (IsInitialized && string.IsNullOrEmpty(Text))
{
SyncTextAndValueProperties(true, Text);
}
}
/// <summary>
/// Called when the <see cref="DisplayDefaultValueOnEmptyText"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnDisplayDefaultValueOnEmptyTextChanged(bool oldValue, bool newValue)
{
if (IsInitialized && string.IsNullOrEmpty(Text))
{
SyncTextAndValueProperties(false, Text);
}
}
/// <summary>
/// Called when the <see cref="IsReadOnly"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
{
SetValidSpinDirection();
}
/// <summary>
/// Called when the <see cref="Maximum"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnMaximumChanged(T oldValue, T newValue)
{
if (IsInitialized)
{
SetValidSpinDirection();
}
}
/// <summary>
/// Called when the <see cref="Minimum"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnMinimumChanged(T oldValue, T newValue)
{
if (IsInitialized)
{
SetValidSpinDirection();
}
}
/// <summary>
/// Called when the <see cref="Text"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnTextChanged(string oldValue, string newValue)
{
if (IsInitialized)
{
SyncTextAndValueProperties(true, Text);
}
}
/// <summary>
/// Called when the <see cref="Value"/> property value changed.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void OnValueChanged(T oldValue, T newValue)
{
if (!_internalValueSet && IsInitialized)
{
SyncTextAndValueProperties(false, null, true);
}
SetValidSpinDirection();
RaiseValueChangedEvent(oldValue, newValue);
}
/// <summary>
/// Called when the <see cref="Maximum"/> property has to be coerced.
/// </summary>
/// <param name="baseValue">The value.</param>
protected virtual T OnCoerceMaximum(T baseValue)
{
return baseValue;
}
/// <summary>
/// Called when the <see cref="Minimum"/> property has to be coerced.
/// </summary>
/// <param name="baseValue">The value.</param>
protected virtual T OnCoerceMinimum(T baseValue)
{
return baseValue;
}
/// <summary>
/// Called when the <see cref="Value"/> property has to be coerced.
/// </summary>
/// <param name="baseValue">The value.</param>
protected virtual T OnCoerceValue(T baseValue)
{
return baseValue;
}
/// <summary>
/// Raises the OnSpin event when spinning is initiated by the end-user.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSpin(SpinEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
var handler = Spinned;
handler?.Invoke(this, e);
if (e.Direction == SpinDirection.Increase)
{
DoIncrement();
}
else
{
DoDecrement();
}
}
/// <summary>
/// Raises the <see cref="ValueChanged"/> event.
/// </summary>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
protected virtual void RaiseValueChangedEvent(T oldValue, T newValue)
{
var e = new UpDownValueChangedEventArgs<T>(ValueChangedEvent, oldValue, newValue);
RaiseEvent(e);
}
/// <summary>
/// Converts the formatted text to a value.
/// </summary>
protected abstract T ConvertTextToValue(string text);
/// <summary>
/// Converts the value to formatted text.
/// </summary>
/// <returns></returns>
protected abstract string ConvertValueToText();
/// <summary>
/// Called by OnSpin when the spin direction is SpinDirection.Increase.
/// </summary>
protected abstract void OnIncrement();
/// <summary>
/// Called by OnSpin when the spin direction is SpinDirection.Descrease.
/// </summary>
protected abstract void OnDecrement();
/// <summary>
/// Sets the valid spin directions.
/// </summary>
protected abstract void SetValidSpinDirection();
/// <summary>
/// Called when the <see cref="CultureInfo"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (CultureInfo)e.OldValue;
var newValue = (CultureInfo)e.NewValue;
upDown.OnCultureInfoChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="DefaultValue"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnDefaultValueChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (T)e.OldValue;
var newValue = (T)e.NewValue;
upDown.OnDefaultValueChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="DisplayDefaultValueOnEmptyText"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnDisplayDefaultValueOnEmptyTextChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (bool) e.OldValue;
var newValue = (bool) e.NewValue;
upDown.OnDisplayDefaultValueOnEmptyTextChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="IsReadOnly"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (bool)e.OldValue;
var newValue = (bool)e.NewValue;
upDown.OnIsReadOnlyChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Maximum"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (T)e.OldValue;
var newValue = (T)e.NewValue;
upDown.OnMaximumChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Minimum"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (T)e.OldValue;
var newValue = (T)e.NewValue;
upDown.OnMinimumChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Text"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (string)e.OldValue;
var newValue = (string)e.NewValue;
upDown.OnTextChanged(oldValue, newValue);
}
}
/// <summary>
/// Called when the <see cref="Value"/> property value changed.
/// </summary>
/// <param name="e">The event args.</param>
private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is UpDownBase<T> upDown)
{
var oldValue = (T)e.OldValue;
var newValue = (T)e.NewValue;
upDown.OnValueChanged(oldValue, newValue);
}
}
private void SetValueInternal(T value)
{
_internalValueSet = true;
try
{
Value = value;
}
finally
{
_internalValueSet = false;
}
}
private static T OnCoerceMaximum(UpDownBase<T> upDown, T value)
{
return upDown.OnCoerceMaximum(value);
}
private static T OnCoerceMinimum(UpDownBase<T> upDown, T value)
{
return upDown.OnCoerceMinimum(value);
}
private void TextBoxOnTextChanged()
{
try
{
_isTextChangedFromUI = true;
if (TextBox != null)
{
Text = TextBox.Text;
}
}
finally
{
_isTextChangedFromUI = false;
}
}
private void OnSpinnerSpin(object sender, SpinEventArgs e)
{
if (AllowSpin && !IsReadOnly)
{
var spin = !e.UsingMouseWheel;
spin |= ((TextBox != null) && TextBox.IsFocused);
if (spin)
{
e.Handled = true;
OnSpin(e);
}
}
}
internal void DoDecrement()
{
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease)
{
OnDecrement();
}
}
internal void DoIncrement()
{
if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase)
{
OnIncrement();
}
}
public event EventHandler<SpinEventArgs> Spinned;
private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e)
{
if (e.Device.Captured != Spinner)
{
Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input);
}
}
/// <summary>
/// Defines the <see cref="ValueChanged"/> event.
/// </summary>
public static readonly RoutedEvent<UpDownValueChangedEventArgs<T>> ValueChangedEvent =
RoutedEvent.Register<UpDownBase<T>, UpDownValueChangedEventArgs<T>>(nameof(ValueChanged), RoutingStrategies.Bubble);
/// <summary>
/// Raised when the <see cref="Value"/> changes.
/// </summary>
public event EventHandler<SpinEventArgs> ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
private bool CommitInput()
{
return SyncTextAndValueProperties(true, Text);
}
/// <summary>
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
/// </summary>
/// <param name="updateValueFromText">If value should be updated from text.</param>
/// <param name="text">The text.</param>
protected bool SyncTextAndValueProperties(bool updateValueFromText, string text)
{
return SyncTextAndValueProperties(updateValueFromText, text, false);
}
/// <summary>
/// Synchronize <see cref="Text"/> and <see cref="Value"/> properties.
/// </summary>
/// <param name="updateValueFromText">If value should be updated from text.</param>
/// <param name="text">The text.</param>
/// <param name="forceTextUpdate">Force text update.</param>
private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate)
{
if (_isSyncingTextAndValueProperties)
return true;
_isSyncingTextAndValueProperties = true;
var parsedTextIsValid = true;
try
{
if (updateValueFromText)
{
if (string.IsNullOrEmpty(text))
{
// An empty input sets the value to the default value.
SetValueInternal(DefaultValue);
}
else
{
try
{
var newValue = ConvertTextToValue(text);
if (!Equals(newValue, Value))
{
SetValueInternal(newValue);
}
}
catch
{
parsedTextIsValid = false;
}
}
}
// Do not touch the ongoing text input from user.
if (!_isTextChangedFromUI)
{
// Don't replace the empty Text with the non-empty representation of DefaultValue.
var shouldKeepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text) && Equals(Value, DefaultValue) && !DisplayDefaultValueOnEmptyText;
if (!shouldKeepEmpty)
{
var newText = ConvertValueToText();
if (!Equals(Text, newText))
{
Text = newText;
}
}
// Sync Text and textBox
if (TextBox != null)
{
TextBox.Text = Text;
}
}
if (_isTextChangedFromUI && !parsedTextIsValid)
{
// Text input was made from the user and the text
// repesents an invalid value. Disable the spinner in this case.
if (Spinner != null)
{
Spinner.ValidSpinDirection = ValidSpinDirections.None;
}
}
else
{
SetValidSpinDirection();
}
}
finally
{
_isSyncingTextAndValueProperties = false;
}
return parsedTextIsValid;
}
}
public class UpDownValueChangedEventArgs<T> : RoutedEventArgs
{
public UpDownValueChangedEventArgs(RoutedEvent routedEvent, T oldValue, T newValue) : base(routedEvent)
{
OldValue = oldValue;
NewValue = newValue;
}
public T OldValue { get; }
public T NewValue { get; }
}
}

2
src/Avalonia.Themes.Default/NumericUpDown.xaml

@ -1,5 +1,5 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector=":is(UpDownBase)">
<Style Selector="NumericUpDown">
<Setter Property="TemplatedControl.BorderBrush" Value="{DynamicResource ThemeBorderLightBrush}"/>
<Setter Property="TemplatedControl.BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="TemplatedControl.Background" Value="{DynamicResource ThemeBackgroundBrush}" />

Loading…
Cancel
Save