Stopbyte

Free WPF Numeric Spinner (NumericUpDown)

In this article we’ll be discussing how you can build your own fully functional & professional Numeric Spinner (also known on WinForms as NumericUpDown) Something like the screenshot below:

WPF NumericUpDown - Stopbyte.com

At the end of this article, you can get a fully functional code & XAML that you can plug into your project and start using it immediately without any changes.

1. Theme (Generic.xaml)

To make it easier to change your Numeric Spinner’s theme (colors), I’ve defined few theme colors within Generic.xaml that can be used by the UpDownBox alongside the rest of your App.

<!-- THEME BASIC -->
<Color x:Key="ThemeColor">#40a6d1</Color>
<Color x:Key="ThemeRedColor">#d14040</Color>
<Color x:Key="ThemeColorDark">#3992b8</Color>
<Color x:Key="ThemeColorDarker">#FF688CAF</Color>
<Color x:Key="ThemeColorInactive">#4cd1ff</Color>
<Color x:Key="ThemeColorActive">#FF3BACDC</Color>
<SolidColorBrush x:Key="ThemeBrush" Color="{DynamicResource ThemeColor}" />
<SolidColorBrush x:Key="ThemeBrushDark" Color="{DynamicResource ThemeColorDark}" />
<SolidColorBrush x:Key="ThemeBrushDarker" Color="{DynamicResource ThemeColorDarker}" />
<SolidColorBrush x:Key="ThemeBrushInactive" Color="{DynamicResource ThemeColorInactive}" />
<SolidColorBrush x:Key="ThemeBrushActive" Color="{DynamicResource ThemeColorActive}" />
<SolidColorBrush x:Key="ThemeRedBrush" Color="{DynamicResource ThemeRedColor}" />

<SolidColorBrush x:Key="Theme_Brush_Bg" Color="White" />
<SolidColorBrush x:Key="Theme_Brush_SilverBorder" Color="Silver" />

Simply copy those lines into your Generic.xaml or App.xaml, you may modify the colors to get a different look & feel for your Numeric Spinner Control.

Next, we’ll start implementing the control’s layout on XAML.

2. XAML Implementation

As you can see below, all it takes is a TextBox and two Buttons within a two columns Grid, And we are already almost there. You can also notice that I’ve used the attribute x:FieldModifier="private" to abstract the internal layout items (Buttons & TextBox) from the rest of the code.

<Border>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="22" />
        </Grid.ColumnDefinitions>
        <TextBox x:Name="tb_main" x:FieldModifier="private" FontWeight="Medium" FontSize="14" VerticalContentAlignment="Center" Padding="5,1" 
                 Grid.Column="0" Grid.RowSpan="2" Text="0" />
        <Button x:Name="cmdUp" x:FieldModifier="private" Grid.Column="1" Grid.Row="0" Width="auto" Height="auto" Click="cmdUp_Click">
            <Button.Content>
                    <Image Source="/VentoApp2.0;component/Resources/Images/arrow_up.png" Width="9" Stretch="Uniform" VerticalAlignment="Center" HorizontalAlignment="Center" />
            </Button.Content>
        </Button>
        <Button x:Name="cmdDown" x:FieldModifier="private" Grid.Column="1" Grid.Row="1" Width="auto" Height="auto" Click="cmdDown_Click">
            <Button.Content>
                <Image Source="/VentoApp2.0;component/Resources/Images/arrow_down.png" Stretch="Uniform" Width="9" VerticalAlignment="Center" HorizontalAlignment="Center" />
            </Button.Content>
        </Button>
        <Border BorderBrush="Gray" IsHitTestVisible="False" BorderThickness="1" CornerRadius="4" Grid.RowSpan="2" 
                Grid.ColumnSpan="2" Padding="0" Margin="0" />
    </Grid>
</Border>

Tips:

I noticed that the outer border isn’t fully clipping to its “round” edges, so I’ve decided to use Border.OpacityMask to get that effect.

I ended up with something like this:

<Border>
        <Border.OpacityMask>
            <VisualBrush>
                <VisualBrush.Visual>
                    <Border Background="Black" SnapsToDevicePixels="True"
                            CornerRadius="4"
                            Width="{Binding ActualWidth, RelativeSource={RelativeSource FindAncestor, AncestorType=Border}}"
                            Height="{Binding ActualHeight, RelativeSource={RelativeSource FindAncestor, AncestorType=Border}}" />
                </VisualBrush.Visual>
            </VisualBrush>
        </Border.OpacityMask>
        .....
</Border>

3. Back-end C# Code

First of all I’ve implemented few useful DependencyProperty’s for basic functionalities:

ValueProperty - Decimal :

The main property, I’ve bound this property to the TextBox’s Text property we’ve created earlier on XAML. I added this line of code to my .ctor :

        tb_main.SetBinding(TextBox.TextProperty, new Binding("Value") { ElementName = "root_numeric_spinner",
            Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged });

Implementation:

public readonly static DependencyProperty ValueProperty = DependencyProperty.Register(
            "Value",
            typeof(decimal),
            typeof(NumericSpinner),
            new PropertyMetadata(new decimal(0)));

        public decimal Value
        {
            get { return (decimal)GetValue(ValueProperty); }
            set
            {
                if (value < MinValue)
                    value = MinValue;
                if (value > MaxValue)
                    value = MaxValue;
                SetValue(ValueProperty, value);
            }
        }

It validates the value first to avoid setting any value outside the allowed boundaries.

StepProperty - Decimal:

Decimal number by which to increase/decrease the current Value whenever the user hits up/down buttons.

Implementation:

public readonly static DependencyProperty StepProperty = DependencyProperty.Register(
    "Step",
    typeof(decimal),
    typeof(NumericSpinner),
    new PropertyMetadata(new decimal(0.1)));

public decimal Step
{
    get { return (decimal)GetValue(StepProperty); }
    set
    {
        SetValue(StepProperty, value);
    }
}

DecimalsProperty - int

Specifies the number of decimals to display after the “.”.

Implementation:

public readonly static DependencyProperty DecimalsProperty = DependencyProperty.Register(
            "Decimals",
            typeof(int),
            typeof(NumericSpinner),
            new PropertyMetadata(2));

        public int Decimals
        {
            get { return (int)GetValue(DecimalsProperty); }
            set
            {
                SetValue(DecimalsProperty, value);
            }
        }

MinValueProperty - Decimal

Minimum value allowed for the numeric spinner. It’s set to decimal.MinValue by default.

Implementation:

public readonly static DependencyProperty MinValueProperty = DependencyProperty.Register(
            "MinValue",
            typeof(decimal),
            typeof(NumericSpinner),
            new PropertyMetadata(decimal.MinValue));

        public decimal MinValue
        {
            get { return (decimal)GetValue(MinValueProperty); }
            set
            {
                if (value > MaxValue)
                    MaxValue = value;
                SetValue(MinValueProperty, value);
            }
        }

It first checks the MaxValue, to avoid setting MinValue larger than MaxValue.

MaxValueProperty - Decimal

Maximum value allowed for the numeric spinner. It’s set to decimal.MaxValue by default.

Implementation:

public readonly static DependencyProperty MaxValueProperty = DependencyProperty.Register(
    "MaxValue",
    typeof(decimal),
    typeof(NumericSpinner),
    new PropertyMetadata(decimal.MaxValue));

public decimal MaxValue
{
    get { return (decimal)GetValue(MaxValueProperty); }
    set
    {
        if (value < MinValue)
            value = MinValue;
        SetValue(MaxValueProperty, value);
    }
}

This also checks the MinValue, to avoid setting MaxValue lower than allowed MinValue.

Extra Functions

Now done with properties, it’s time to add some extra code (events, event handlers,…etc) and we are all done.

Event Handlers

So first, you might have noticed, the Up & Down buttons had a Click event handler set, here is the implementation (it’s simple and clean):

private void cmdUp_Click(object sender, RoutedEventArgs e)
{
    Value += Step;
}

private void cmdDown_Click(object sender, RoutedEventArgs e)
{
    Value -= Step;
}

Events

I’ve been lazy enough, so couldn’t implement an event for each separate property, So instead identified these two:

public event EventHandler PropertyChanged;
public event EventHandler ValueChanged;

PropertyChanged: Called whenever any of the properties’ above change (including ValueProperty)
ValueChanged: Called whenever the ValueProperty's value changes.

And then added a ValueChanged handler to all the dependency properties above, through DependencyPropertyDescriptor.

DependencyPropertyDescriptor.FromProperty(ValueProperty, typeof(NumericSpinner)).AddValueChanged(this, PropertyChanged);
DependencyPropertyDescriptor.FromProperty(ValueProperty, typeof(NumericSpinner)).AddValueChanged(this, ValueChanged);
DependencyPropertyDescriptor.FromProperty(DecimalsProperty, typeof(NumericSpinner)).AddValueChanged(this, PropertyChanged);
DependencyPropertyDescriptor.FromProperty(MinValueProperty, typeof(NumericSpinner)).AddValueChanged(this, PropertyChanged);
DependencyPropertyDescriptor.FromProperty(MaxValueProperty, typeof(NumericSpinner)).AddValueChanged(this, PropertyChanged);

Notice: ValueProperty has two handlers, ValueChanged & PropertyChanged.

Making It Perfect

As an extra “unnecessary” measure I implemented this method:

/// <summary>
/// Revalidate the object, whenever a value is changed...
/// </summary>
private void validate()
{
    // Logically, This is not needed at all... as it's handled within other properties...
    if (MinValue > MaxValue) MinValue = MaxValue;
    if (MaxValue < MinValue) MaxValue = MinValue;
    if (Value < MinValue) Value = MinValue;
    if (Value > MaxValue) Value = MaxValue;

    Value = decimal.Round(Value, Decimals);
}

I attached it to the PropertyChanged event at .ctor :

PropertyChanged += (x, y) => validate();

It’s being called whenever a property value changes, and it validates its value against the rest of properties.

Source Code:

Finally; It’s time to get you the full source code for this Numeric Spinner, I shared the full source code publicly on Github:

You may simply download the 3 files that make up this control:

  1. Generic.xaml You may also copy the content of this file directly into your project’s Generic.xaml file.

  2. NumericSpinner.xaml Main XAML layout.

  3. NumericSpinner.xaml.cs Backend source code.

That’s it, Hope that helps!

It’s all about decimals here in this Spinner. What about simple integers? I just want the spinner to insert integer numbers. Could you please give me the new code to achieve this?
Thanks and best regards,
Jacques

Try this:

public static class InputLimit
{
    public static string GetIntegerValueProxy(TextBox obj) => (string)obj.GetValue(IntegerValueProxyProperty);

    public static void SetIntegerValueProxy(TextBox obj, string value) => obj.SetValue(IntegerValueProxyProperty, value);

    // Using a DependencyProperty as the backing store for IntegerValueProxy.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IntegerValueProxyProperty =
        DependencyProperty.RegisterAttached("IntegerValueProxy", typeof(string), typeof(InputLimit),
            new FrameworkPropertyMetadata("0", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, CoerceIntegerValueProxy));

    private static object CoerceIntegerValueProxy(DependencyObject d, object baseValue)
    {
        if (int.TryParse(baseValue as string, out _)) return baseValue;
        return DependencyProperty.UnsetValue;
    }
}