Browse Source

Reworked to provide common NumericUpDown control which values are doubles.

dzhelnin 7 years ago
parent
commit
d6b5e04f0c

+ 3 - 3
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" />

+ 1 - 1
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>

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

+ 2 - 19
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; }

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

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

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

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

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

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

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

+ 1075 - 42
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -1,36 +1,207 @@
 using System;
 using System.Globalization;
+using System.IO;
+using System.Linq;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
 
 namespace Avalonia.Controls
 {
     /// <summary>
-    /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
+    /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values.
     /// </summary>
-    public abstract class NumericUpDown<T> : UpDownBase<T>
+    public class NumericUpDown : TemplatedControl
     {
+        /// <summary>
+        /// Defines the <see cref="AllowSpin"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> AllowSpinProperty =
+            ButtonSpinner.AllowSpinProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ButtonSpinnerLocation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
+            ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ShowButtonSpinner"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ShowButtonSpinnerProperty =
+            ButtonSpinner.ShowButtonSpinnerProperty.AddOwner<NumericUpDown>();
+
+        /// <summary>
+        /// Defines the <see cref="ClipValueToMinMax"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, bool> ClipValueToMinMaxProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, bool>(nameof(ClipValueToMinMax),
+                updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b);
+
+        /// <summary>
+        /// Defines the <see cref="CultureInfo"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, CultureInfo> CultureInfoProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, 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<double?> DefaultValueProperty =
+            AvaloniaProperty.Register<NumericUpDown, double?>(nameof(DefaultValue));
+
+        /// <summary>
+        /// Defines the <see cref="DisplayDefaultValueOnEmptyText"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> DisplayDefaultValueOnEmptyTextProperty =
+            AvaloniaProperty.Register<NumericUpDown, bool>(nameof(DisplayDefaultValueOnEmptyText));
+
         /// <summary>
         /// Defines the <see cref="FormatString"/> property.
         /// </summary>
         public static readonly StyledProperty<string> FormatStringProperty =
-            AvaloniaProperty.Register<NumericUpDown<T>, string>(nameof(FormatString), string.Empty);
+            AvaloniaProperty.Register<NumericUpDown, string>(nameof(FormatString), string.Empty);
 
         /// <summary>
         /// Defines the <see cref="Increment"/> property.
         /// </summary>
-        public static readonly StyledProperty<T> IncrementProperty =
-            AvaloniaProperty.Register<NumericUpDown<T>, T>(nameof(Increment), default(T), validate: OnCoerceIncrement);
+        public static readonly StyledProperty<double> IncrementProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Increment), 1.0d, validate: OnCoerceIncrement);
 
         /// <summary>
-        /// Initializes static members of the <see cref="NumericUpDown{T}"/> class.
+        /// Defines the <see cref="IsReadOnly"/> property.
         /// </summary>
-        static NumericUpDown()
+        public static readonly StyledProperty<bool> IsReadOnlyProperty =
+            AvaloniaProperty.Register<NumericUpDown, bool>(nameof(IsReadOnly));
+
+        /// <summary>
+        /// Defines the <see cref="Maximum"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MaximumProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum);
+
+        /// <summary>
+        /// Defines the <see cref="Minimum"/> property.
+        /// </summary>
+        public static readonly StyledProperty<double> MinimumProperty =
+            AvaloniaProperty.Register<NumericUpDown, double>(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum);
+
+        /// <summary>
+        /// Defines the <see cref="ParsingNumberStyle"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, NumberStyles> ParsingNumberStyleProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, NumberStyles>(nameof(ParsingNumberStyle),
+                updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style);
+
+        /// <summary>
+        /// Defines the <see cref="Text"/> property.
+        /// </summary>
+        public static readonly DirectProperty<NumericUpDown, string> TextProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, 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<NumericUpDown, double?> ValueProperty =
+            AvaloniaProperty.RegisterDirect<NumericUpDown, double?>(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<NumericUpDown, string>(nameof(Watermark));
+
+        private IDisposable _textBoxTextChangedSubscription;
+
+        private double? _value;
+        private string _text;
+        private bool _internalValueSet;
+        private bool _clipValueToMinMax;
+        private bool _isSyncingTextAndValueProperties;
+        private bool _isTextChangedFromUI;
+        private CultureInfo _cultureInfo;
+        private NumberStyles _parsingNumberStyle = NumberStyles.Any;
+        
+        /// <summary>
+        /// Gets the Spinner template part.
+        /// </summary>
+        private Spinner Spinner { get; set; }
+
+        /// <summary>
+        /// Gets the TextBox template part.
+        /// </summary>
+        private TextBox TextBox { get; set; }
+
+        /// <summary>
+        /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel.
+        /// </summary>
+        public bool AllowSpin
         {
-            FormatStringProperty.Changed.Subscribe(FormatStringChanged);
-            IncrementProperty.Changed.Subscribe(IncrementChanged);
+            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 double? 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 the display format of the <see cref="UpDownBase{T}.Value"/>.
+        /// Gets or sets the display format of the <see cref="Value"/>.
         /// </summary>
         public string FormatString
         {
@@ -39,14 +210,196 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Gets or sets the amount in which to increment the <see cref="UpDownBase{T}.Value"/>.
+        /// Gets or sets the amount in which to increment the <see cref="Value"/>.
         /// </summary>
-        public T Increment
+        public double Increment
         {
             get { return GetValue(IncrementProperty); }
             set { SetValue(IncrementProperty, 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 double Maximum
+        {
+            get { return GetValue(MaximumProperty); }
+            set { SetValue(MaximumProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the minimum allowed value.
+        /// </summary>
+        public double Minimum
+        {
+            get { return GetValue(MinimumProperty); }
+            set { SetValue(MinimumProperty, value); }
+        }
+
+        /// <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); }
+        }
+
+        /// <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 double? 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="NumericUpDown"/> class.
+        /// </summary>
+        public NumericUpDown()
+        {
+            Initialized += (sender, e) =>
+            {
+                if (!_internalValueSet && IsInitialized)
+                {
+                    SyncTextAndValueProperties(false, null, true);
+                }
+
+                SetValidSpinDirection();
+            };
+        }
+
+        /// <summary>
+        /// Initializes static members of the <see cref="NumericUpDown"/> class.
+        /// </summary>
+        static NumericUpDown()
+        {
+            CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
+            DefaultValueProperty.Changed.Subscribe(OnDefaultValueChanged);
+            DisplayDefaultValueOnEmptyTextProperty.Changed.Subscribe(OnDisplayDefaultValueOnEmptyTextChanged);
+            FormatStringProperty.Changed.Subscribe(FormatStringChanged);
+            IncrementProperty.Changed.Subscribe(IncrementChanged);
+            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(double oldValue, double 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="FormatString"/> property value changed.
         /// </summary>
@@ -65,7 +418,7 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="oldValue">The old value.</param>
         /// <param name="newValue">The new value.</param>
-        protected virtual void OnIncrementChanged(T oldValue, T newValue)
+        protected virtual void OnIncrementChanged(double oldValue, double newValue)
         {
             if (IsInitialized)
             {
@@ -74,59 +427,739 @@ namespace Avalonia.Controls
         }
 
         /// <summary>
-        /// Called when the <see cref="Increment"/> property has to be coerced.
+        /// Called when the <see cref="IsReadOnly"/> property value changed.
         /// </summary>
-        /// <param name="baseValue">The value.</param>
-        protected virtual T OnCoerceIncrement(T baseValue)
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
         {
-            return baseValue;
+            SetValidSpinDirection();
         }
 
         /// <summary>
-        /// Called when the <see cref="Increment"/> property value changed.
+        /// Called when the <see cref="Maximum"/> property value changed.
         /// </summary>
-        /// <param name="e">The event args.</param>
-        private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e)
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnMaximumChanged(double oldValue, double newValue)
         {
-            if (e.Sender is NumericUpDown<T> upDown)
+            if (IsInitialized)
             {
-                var oldValue = (T)e.OldValue;
-                var newValue = (T)e.NewValue;
-                upDown.OnIncrementChanged(oldValue, newValue);
+                SetValidSpinDirection();
+            }
+            if (Value.HasValue && ClipValueToMinMax)
+            {
+                Value = CoerceValueMinMax(Value.Value);
             }
         }
 
         /// <summary>
-        /// Called when the <see cref="FormatString"/> property value changed.
+        /// Called when the <see cref="Minimum"/> property value changed.
         /// </summary>
-        /// <param name="e">The event args.</param>
-        private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e)
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnMinimumChanged(double oldValue, double newValue)
         {
-            if (e.Sender is NumericUpDown<T> upDown)
+            if (IsInitialized)
             {
-                var oldValue = (string) e.OldValue;
-                var newValue = (string) e.NewValue;
-                upDown.OnFormatStringChanged(oldValue, newValue);
+                SetValidSpinDirection();
+            }
+            if (Value.HasValue && ClipValueToMinMax)
+            {
+                Value = CoerceValueMinMax(Value.Value);
             }
         }
 
-        private static T OnCoerceIncrement(NumericUpDown<T> numericUpDown, T value)
+        /// <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)
         {
-            return numericUpDown.OnCoerceIncrement(value);
+            if (IsInitialized)
+            {
+                SyncTextAndValueProperties(true, Text);
+            }
         }
 
         /// <summary>
-        /// Parse percent format text
+        /// Called when the <see cref="Value"/> property value changed.
         /// </summary>
-        /// <param name="text">Text to parse.</param>
-        /// <param name="cultureInfo">The culture info.</param>
-        protected static decimal ParsePercent(string text, IFormatProvider cultureInfo)
+        /// <param name="oldValue">The old value.</param>
+        /// <param name="newValue">The new value.</param>
+        protected virtual void OnValueChanged(double? oldValue, double? newValue)
         {
-            var info = NumberFormatInfo.GetInstance(cultureInfo);
-            text = text.Replace(info.PercentSymbol, null);
-            var result = decimal.Parse(text, NumberStyles.Any, info);
-            result = result / 100;
-            return result;
+            if (!_internalValueSet && IsInitialized)
+            {
+                SyncTextAndValueProperties(false, null, true);
+            }
+
+            SetValidSpinDirection();
+
+            RaiseValueChangedEvent(oldValue, newValue);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceIncrement(double baseValue)
+        {
+            return baseValue;
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Maximum"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceMaximum(double baseValue)
+        {
+            return baseValue;
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Minimum"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double OnCoerceMinimum(double baseValue)
+        {
+            return baseValue;
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Value"/> property has to be coerced.
+        /// </summary>
+        /// <param name="baseValue">The value.</param>
+        protected virtual double? OnCoerceValue(double? 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(double? oldValue, double? newValue)
+        {
+            var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue);
+            RaiseEvent(e);
+        }
+
+        /// <summary>
+        /// Converts the formatted text to a value.
+        /// </summary>
+        private double? ConvertTextToValue(string text)
+        {
+            double? 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;
+        }
+
+        /// <summary>
+        /// Converts the value to formatted text.
+        /// </summary>
+        /// <returns></returns>
+        private 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);
+
+        }
+
+        /// <summary>
+        /// Called by OnSpin when the spin direction is SpinDirection.Increase.
+        /// </summary>
+        private void OnIncrement()
+        {
+            if (!HandleNullSpin())
+            {
+                var result = Value.Value + Increment;
+                Value = CoerceValueMinMax(result);
+            }
+        }
+
+        /// <summary>
+        /// Called by OnSpin when the spin direction is SpinDirection.Descrease.
+        /// </summary>
+        private void OnDecrement()
+        {
+            if (!HandleNullSpin())
+            {
+                var result = Value.Value - Increment;
+                Value = CoerceValueMinMax(result);
+            }
+        }
+
+        /// <summary>
+        /// Sets the valid spin directions.
+        /// </summary>
+        private void SetValidSpinDirection()
+        {
+            var validDirections = ValidSpinDirections.None;
+
+            // Zero increment always prevents spin.
+            if (Increment != 0 && !IsReadOnly)
+            {
+                if (IsLowerThan(Value, Maximum) || !Value.HasValue)
+                {
+                    validDirections = validDirections | ValidSpinDirections.Increase;
+                }
+
+                if (IsGreaterThan(Value, Minimum) || !Value.HasValue)
+                {
+                    validDirections = validDirections | ValidSpinDirections.Decrease;
+                }
+            }
+
+            if (Spinner != null)
+            {
+                Spinner.ValidSpinDirection = validDirections;
+            }
+        }
+
+        /// <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 NumericUpDown 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 NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)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 NumericUpDown upDown)
+            {
+                var oldValue = (bool) e.OldValue;
+                var newValue = (bool) e.NewValue;
+                upDown.OnDisplayDefaultValueOnEmptyTextChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="Increment"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)e.NewValue;
+                upDown.OnIncrementChanged(oldValue, newValue);
+            }
+        }
+
+        /// <summary>
+        /// Called when the <see cref="FormatString"/> property value changed.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Sender is NumericUpDown upDown)
+            {
+                var oldValue = (string)e.OldValue;
+                var newValue = (string)e.NewValue;
+                upDown.OnFormatStringChanged(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 NumericUpDown 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 NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)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 NumericUpDown upDown)
+            {
+                var oldValue = (double)e.OldValue;
+                var newValue = (double)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 NumericUpDown 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 NumericUpDown upDown)
+            {
+                var oldValue = (double?)e.OldValue;
+                var newValue = (double?)e.NewValue;
+                upDown.OnValueChanged(oldValue, newValue);
+            }
+        }
+
+        private void SetValueInternal(double? value)
+        {
+            _internalValueSet = true;
+            try
+            {
+                Value = value;
+            }
+            finally
+            {
+                _internalValueSet = false;
+            }
+        }
+
+        private static double OnCoerceMaximum(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceMaximum(value);
+        }
+
+        private static double OnCoerceMinimum(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceMinimum(value);
+        }
+
+        private static double OnCoerceIncrement(NumericUpDown upDown, double value)
+        {
+            return upDown.OnCoerceIncrement(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);
+                }
+            }
+        }
+
+        private void DoDecrement()
+        {
+            if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease)
+            {
+                OnDecrement();
+            }
+        }
+
+        private 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<NumericUpDownValueChangedEventArgs> ValueChangedEvent =
+            RoutedEvent.Register<NumericUpDown, NumericUpDownValueChangedEventArgs>(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>
+        private 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;
+        }
+
+        private double? CoerceValueMinMax(double? value)
+        {
+            if (IsLowerThan(value, Minimum))
+            {
+                return Minimum;
+            }
+            else if (IsGreaterThan(value, Maximum))
+            {
+                return Maximum;
+            }
+            else
+            {
+                return value;
+            }
+        }
+
+        private static bool IsLowerThan(double? value1, double? value2)
+        {
+            if (value1 == null || value2 == null)
+            {
+                return false;
+            }
+            return value1.Value < value2.Value;
+        }
+
+        private static bool IsGreaterThan(double? value1, double? value2)
+        {
+            if (value1 == null || value2 == null)
+            {
+                return false;
+            }
+            return value1.Value > value2.Value;
+        }
+
+        private bool HandleNullSpin()
+        {
+            if (!Value.HasValue)
+            {
+                var forcedValue = DefaultValue ?? default(double);
+                Value = CoerceValueMinMax(forcedValue);
+                return true;
+            }
+            return false;
+        }
+
+        private double? ConvertTextToValueCore(string currentValueText, string text)
+        {
+            double? result;
+
+            if (IsPercent(FormatString))
+            {
+                result = decimal.ToDouble(ParsePercent(text, CultureInfo));
+            }
+            else
+            {
+                // Problem while converting new text
+                if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue))
+                {
+                    var shouldThrow = true;
+
+                    // Check if CurrentValueText is also failing => it also contains special characters. ex : 90°
+                    if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _))
+                    {
+                        // 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 (double.TryParse(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 double? GetClippedMinMaxValue(double? result)
+        {
+            if (IsGreaterThan(result, Maximum))
+            {
+                return Maximum;
+            }
+            else if (IsLowerThan(result, Minimum))
+            {
+                return Minimum;
+            }
+            return result;
+        }
+
+        private void ValidateDefaultMinMax(double? 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));
+            }
+        }
+
+        /// <summary>
+        /// Parse percent format text
+        /// </summary>
+        /// <param name="text">Text to parse.</param>
+        /// <param name="cultureInfo">The culture info.</param>
+        private static decimal ParsePercent(string text, IFormatProvider cultureInfo)
+        {
+            var info = NumberFormatInfo.GetInstance(cultureInfo);
+            text = text.Replace(info.PercentSymbol, null);
+            var result = decimal.Parse(text, NumberStyles.Any, info);
+            result = result / 100;
+            return result;
+        }
+
+
+        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;
         }
     }
 }

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

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

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

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

+ 1 - 1
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}" />