Click here to Skip to main content
15,867,986 members
Articles / Desktop Programming / WPF

Creating a NumericUpDown control from scratch

Rate me:
Please Sign up or sign in to vote.
4.98/5 (36 votes)
6 Nov 2014CPOL28 min read 79.1K   3.7K   46   2
Shows the entire process of creating a full-fledged NumericUpDown control in WPF.

A word of introduction

After stalking CodeProject for what seems an eternity, I have decided that it's probably time for me to start contributing to this great site.

This is the first article I have ever written on any subject, but I can honestly say that I've put my maximum effort into writing this as best as I could. If there are things that I have done wrong or haven't explained properly, please tell me so that I may improve this article and possibly my own skills.  

A word on the nature of this article

I don't really know if this is a tutorial or a step-by-step guide. I guess it's a bit of both, really. I guess I have written it as a step-by-step guide for intermediate programmers interlaced with tutorial explanations for beginners. The code is all here, though, there aren't any unexplained surprises in the downloadable source.

I am counting on you reading this article as a whole, it was never designed to be a "come-and-pick-what-feature-you-like" kind of guide, the features depend on each other more often than not. Also, they are not implemented in any order of importance or usefulness - some of the obviously missing features like default text in TextBox get implemented at the very end, because they were not very interesting or consequential, just necessary to finish the control (they could wait). 

You will need at least a .NET Framework 3.5, although a .NET Framework 4.0 or later (namely 4.5) is recommended.

What you'll learn  

You will learn how to create a custom control called NumericUpDown accompanied by a default look in Generic.xaml and a Demo application using all aspects of the control (as shown below).  Image 1

Figure 1 - A screenshot of the demo application, showing the NumericUpDown that we'll be creating. All the buttons pertain to the individual aspects of the control.

Other than the obvious functionality above, this NumericUpDown also supports:

  • Canceling typed-in unconfirmed changes by pressing Esc
  • Confirming typed-in changes by pressing Enter 
  • Increasing and decreasing the value with keyboard arrows or PageUp/PageDown
  • Zeroing-out the value when the two buttons on the side get right-clicked  

Creating the basic control  

We will need a solution named CustomControls with two projects:

  • CustomControl (a WPF Custom Control Library)
  • Demo (a WPF Application)

I am assuming that you download the source code and work with the Demo source that is included. Use it as a basis for the Demo application and simply uncomment the individual methods in MainWindow.xaml.cs when you have implemented them.

Open CustomControl1.cs and rename it to NumericUpDown.cs. Your code should look like this:

C#
namespace CustomControls
{
    public class NumericUpDown : Control
    {
        static NumericUpDown()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (NumericUpDown),
                                                     new FrameworkPropertyMetadata(
                                                         typeof (NumericUpDown)));
        }
    }
} 

OverrideMetadata is the function that takes care of applying the default theme in Generic.xaml, so we really want to leave that in.

Speaking of Generic.xaml, let's modify it now. This template is really just a TextBox and two RepeatButtons with some custom graphics. The very important parts are the names.

XML
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomControls">
    <Style TargetType="{x:Type local:NumericUpDown}">
        <Setter Property="HorizontalAlignment" Value="Center" />
        <Setter Property="HorizontalContentAlignment" Value="Right" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="BorderBrush" Value="Gray" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="Width" Value="100" />
        <Setter Property="Height" Value="26" />
        <Setter Property="Focusable" Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:NumericUpDown}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Focusable="False">
                        <Grid Width="{TemplateBinding Width}"
                              Height="{TemplateBinding Height}"
                              VerticalAlignment="Center"
                              Focusable="False">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>

                            <TextBox x:Name="PART_TextBox"
                                     VerticalAlignment="Center"
                                     HorizontalContentAlignment="Right" />

                            <Grid Grid.Column="1">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="*" />
                                    <RowDefinition Height="*" />
                                </Grid.RowDefinitions>
                                <RepeatButton x:Name="PART_IncreaseButton"
                                              Grid.Row="0"
                                              Width="20"
                                              Margin="0, 1, 2, 0">
                                    <Path Margin="1"
                                          Data="M 0 20 L 35 -20 L 70 20 Z"
                                          Fill="#FF202020"
                                          Stretch="Uniform" />
                                </RepeatButton>
                                <RepeatButton x:Name="PART_DecreaseButton"
                                              Grid.Row="1"
                                              Width="20"
                                              Margin="0, 0, 2, 1">
                                    <Path Margin="1"
                                          Data="M 0 0 L 35 40 L 70 0 Z"
                                          Fill="#FF202020"
                                          Stretch="Uniform" />
                                </RepeatButton>
                            </Grid>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

As you can see, the TextBox and the two RepeatButtons have strange names starting with 'PART_'. I will explain the purpose of these strange names later. For now, it's important that the template above draws pretty much the same NumericUpDown as shown in the first screenshot. I think that nothing above should really come as a surprise, except maybe the Path.Data variable that's in the RepeatButtons. If you've never drawn custom graphics with Path before, here's the necessary information.

These are the layers that NumericUpDown is composed of:Image 2

Figure 2 - Shows the different layers composing the NumericUpDown control.

Tying it together

We have the bare-bones NumericUpDown class that does nothing interesting and we have the template in Generic.xaml that gets applied by the aforementioned OverrideMetadata method. Everything seems to flow pretty smoothly, right? Well, the only problem is that we have no way of accessing the TextBox and the two RepeatButtons! They aren't added to the code as variables as one might expect, so how are we supposed to get references to them? Well, here come TemplatePartAttributes to the rescue. As the link says, TemplatePartAttributes basically advertise what elements we expect to have in the template. We need three parts and so we also need three attributes:

C#
[TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "PART_IncreaseButton", Type = typeof(RepeatButton))]
[TemplatePart(Name = "PART_DecreaseButton", Type = typeof(RepeatButton))] 

The names that you use are not important (although they do have to match the ones in XAML, obviously), but it's a good practice to start template part names with PART_ to indicate what they are. Now, just advertising that this control has three template parts isn't enough, we need to obtain a reference that we can use - that's a bit more complicated.

We cannot just 'attach' (obtain a reference to) these template parts in the constructor, because if someone were to apply a different template, the references would be invalidated. Instead, we need to strike at the exact moment the template is applied. OnApplyTemplate() is perfect for this. Override the method and modify it as below:

C#
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    AttachToVisualTree();
}

AttachToVisualTree() is just a convenience method to call the three real methods that do the actual attaching. Those are:

C#
private void AttachToVisualTree()
{
   AttachTextBox();
   AttachIncreaseButton();
   AttachDecreaseButton();
}

Pretty self-explanatory, no? Let's go through each of these functions and tie the three template parts to the code-behind.

Attaching the template parts

Getting reference to the TextBox is as easy as calling GetTemplateChild() and passing it the name of the template part, 'PART_TextBox' in our case. Let's do that now.

C#
protected TextBox TextBox;

private void AttachTextBox()
{
    var textBox = GetTemplateChild("PART_TextBox") as TextBox;

    // A null check is advised
    if (textBox != null)
    {
        TextBox = textBox;
    }
}

Attaching the RepeatButtons is a no-brainer at this point.

C#
protected RepeatButton IncreaseButton;

private void AttachIncreaseButton()
{
    var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
    if (increaseButton != null)
    {
        IncreaseButton = increaseButton;
        IncreaseButton.Focusable = false;
    }
} 
protected RepeatButton DecreaseButton;

private void AttachDecreaseButton()
{
    var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
    if (decreaseButton != null)
    {
        DecreaseButton = decreaseButton;
        DecreaseButton.Focusable = false;
    }
}

Don't forget to store all three references, we'll be using them later. Also notice how we set the Focusable properties to false, they must not receive any focus.

Creating the basic functionality

In order to display a value and be able to modify it, we need to

  • Create a dependency property
  • Value - holds the actual Decimal number, does all the coercion and notification
  • Create commands
  • _minorIncreaseValueCommand - represents a command to increase the value
  • _minorDecreaseValueCommand - represents a command to decrease the value
  • Bind the commands to the template parts that use them

That's a lot of stuff to get a single number on the screen, better get to it then.

Creating the dependency property

NumericUpDown control is all about changing Value, right? Sadly, no. Even though the coercion (correcting) logic will be inside Value, we will have to keep the TextBox up to date with the current representation of the number inside Value. Let's start by creating Value:

C#
#region Value

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value", typeof (Decimal), typeof (NumericUpDown),
                                new PropertyMetadata(0m, OnValueChanged, CoerceValue));

public Decimal Value
{
    get { return (Decimal) GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static void OnValueChanged(DependencyObject element,
                                    DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceValue(DependencyObject element, object baseValue)
{
    var value = (Decimal) baseValue;

    return value;
}

#endregion

That seems quite complicated, but it's not that bad - most of it is just boilerplate code. ValueProperty is basically a dictionary of all the meta-data for the instances of Value property (that we can access through the get/set accessors), as explained in the awesome description that Robert Rossney wrote - be sure to read it. As I'm sure you've noticed, the property above specifies three arguments as PropertyMetadata.

The first argument is a default value that the DependencyProperty has upon its creation, 0m (0 decimal) in our case. The second is a PropertyChangedCallback delegate that gets called only when the property really changes (doesn't get triggered when the same value is set). The last one is a CoerceValueCallback delegate that gets called when Value is set. It allows you to coerce (correct) the input into something usable and then have it assigned to the property.

Creating the commands

At this point you might ask why not just use event handlers for everything? Well, event handlers would actually prove to be much more complicated and verbose. You'll see for yourself in a little while. We need two commands - one command to increase Value and one to decrease it.

C#
using System.Windows.Input;

private readonly RoutedUICommand _minorIncreaseValueCommand =
    new RoutedUICommand("MinorIncreaseValue", "MinorIncreaseValue", typeof(NumericUpDown));

private readonly RoutedUICommand _minorDecreaseValueCommand =
    new RoutedUICommand("MinorDecreaseValue", "MinorDecreaseValue", typeof(NumericUpDown)); 

Look here for a quick reference on the constructor's arguments. We also need to specify what 'triggers' these commands. Each of the two RepeatButtons has a Command property - perfect for our needs. Let's assign the commands right away.

C#
private void AttachIncreaseButton()
{
    var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
    if (increaseButton != null)
    {
        IncreaseButton = increaseButton;
        IncreaseButton.Focusable = false;
        IncreaseButton.Command = _minorIncreaseValueCommand;
    }
}

private void AttachDecreaseButton()
{
    var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
    if (decreaseButton != null)
    {
        DecreaseButton = decreaseButton;
        DecreaseButton.Focusable = false;
        DecreaseButton.Command = _minorDecreaseValueCommand;
    }
}

The commands are created, they get triggered when needed, but they still don't do anything. We need to tell the commands what their callback is - what function they call when they're triggered. For that, let's create a new function named AttachCommands(). In this function we will eventually attach all the various commands that we'll be needing as we add more functionality.

C#
private void AttachCommands()
{
    CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
    CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));
} 

So what exactly did we just do? First, we have created a CommandBinding that binds each of the commands to its respective function, namely IncreaseValue() and DecreaseValue() and then added those command bindings to the CommandBindings collection of our NumericUpDown control. The last thing that remains is to create the two functions that we called - IncreaseValue() and DecreaseValue().

C#
private void IncreaseValue()
{
    Value++;
}

private void DecreaseValue()
{
    Value--;
}

Understand that these functions are far from complete. We will be adding more stuff as we go along, but to get the most basic functionality (increasing and decreasing Value), we need at least this much.

Now, there is still one thing we need to do to display something inside TextBox. We may have bound the commands to increase and decrease Value, but there is nothing updating the TextBox, so it stays empty. The best place to take Value and create a string representation that we can use is in the CoerceValue() method that we've created before. Change CoerceValue() as follows:

C#
using System.Globalization;

private static object CoerceValue(DependencyObject element, object baseValue)
{
    var control = (NumericUpDown) element;
    var value = (Decimal) baseValue;
    
    control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture);

    return value;
} 

Because these methods are static, we have to acquire a reference to the NumericUpDown that we're currently in and work on that reference's variables.

Don't forget to call AttachCommands() from OnApplyTemplate():

C#
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    AttachToVisualTree();
    AttachCommands();
}

If you compile and launch now, you will still see an empty NumericUpDown. If you press the two buttons on the side though, you will see a number.

Adding input bindings

Manipulating Value by the little RepeatButtons is all well and good, but it isn't enough. It would be great if we could also use keyboard arrows. Thankfully, that is the easiest thing ever, thanks to the commands. All we have to do is somehow trigger the _minorIncreaseValueCommand and _minorDecreaseValueCommand when the respective keyboard arrows are pressed - that's where InputBindings come into play. We just have to register an InputBinding with our command and the command will be fired every time a keyboard arrow is pressed. Modify AttachCommands() as below:

C#
private void AttachCommands()
{
    CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
    CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));

    CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox), 
      new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
}

And that's it! If you compile and launch now, you can increase/decrease the value just by pressing keyboard arrows (don't forget the TextBox must have focus).


Update

As the user Ichters kindly pointed out, registering the input binding through the CommandManager will not work correctly with multiple NumericUpDowns on a single Window, resulting in the commands working only for the last NumericUpDown.

I am terribly sorry, but I just don't have any time to go through this article, change every occurence and check if it compiles/works and then do the same with the downloadable source code. All I can do is recommend that you register the commands as shown below:

TextBox.InputBindings.Add(new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
TextBox.InputBindings.Add(new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));

I'm guessing all the other commands should be registered the same way, I cannot make any promises.

Best of luck!


Keyboard input

No NumericUpDown control would be complete without the ability to input the value directly into the TextBox. Sadly, it's not just a simple matter of parsing the TextBox.Text into a Decimal every time it changes and assigning Value with it, we need to update Value only when the user really finishes editing the TextBox.

There are three ways to detect if the user has finished editing the TextBox:

  1. NumericUpDown loses focus
  2. User uses keyboard arrows or the buttons on the side to increase/decrease Value
  3. User presses the enter (return) key

Before we handle the individual cases, we need to create a method that will convert the text input into a Decimal number that we can use. Let's create a method to do that for us:

C#
private Decimal ParseStringToDecimal(String source)
{
    Decimal value;
    Decimal.TryParse(source, out value);

    return value;
}

This function simply parses source into a number. The TryParse() method is awesome, because no matter what we try to parse, it always returns a number (it never throws an exception).

Let's take care of the first and easiest case - NumericUpDown losing its focus. For that, we need to intercept the LostFocus event as shown below:

C#
private void AttachTextBox()
{
    var textBox = GetTemplateChild("PART_TextBox") as TextBox;

    // A null check is advised
    if (textBox != null)
    {
        TextBox = textBox;
        TextBox.LostFocus += TextBoxOnLostFocus;
    }
}

Create the TextBoxOnLostFocus() method and update Value with the content of TextBox:

C#
private void TextBoxOnLostFocus(object sender, RoutedEventArgs routedEventArgs)
{
    Value = ParseStringToDecimal(TextBox.Text);
} 

If TextBox loses its focus, Value will automatically be updated with TextBox's contents and then TextBox.Text and ValueString get set to Value. The process looks like this:

Image 3

Figure 3 - Shows what happens when NumericUpDown loses its focus (The number 401 is just a randomly picked number). The whole process after CoerceValue() is completely redundant at this point but becomes essential later - do not remove it.

Now that we have taken care of the first case, let's continue with the second one. Before we can increment or decrement a number, we have to be sure that we do it on the latest one, which is always in the TextBox (even if Value gets set programatically, OnValueChanged() immediately updates TextBox). The easiest thing to do is simply acquire a Decimal representation of the current contents of TextBox and manipulate that, as shown below:

C#
private void IncreaseValue()
{
    // Get the value that's currently in the TextBox.Text
    var value = ParseStringToDecimal(TextBox.Text);

    value++;

    Value = value;
} 
C#
private void DecreaseValue()
{
    // Get the value that's currently in the _textBox.Text
    var value = ParseStringToDecimal(TextBox.Text);

    value--;
            
    Value = value;
} 

We don't know if the user changed the number in the TextBox, or if it's just a consecutive increase/decrease that uses the previous Value unchanged, so we always acquire the latest Decimal representation and work on that.

To address the third case, we need to handle the return key. We need a new command for this action:

C#
private readonly RoutedUICommand _updateValueStringCommand =
    new RoutedUICommand("UpdateValueString", "UpdateValueString", typeof (NumericUpDown));

The command has to be bound to its InputBinding:

C#
private void AttachCommands()
{
    CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
    CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));

    CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox), 
      new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
} 

Nothing here should surprise you, it's the same thing we've already done twice. The only thing that remains is to tell the command what to do. Well, what should it do, exactly? Look at the code below:

C#
private void AttachCommands()
{
    CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue()));
    CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue()));  
    CommandBindings.Add(new CommandBinding(_updateValueStringCommand, (a, b) =>
                                           {
                                               Value = ParseStringToDecimal(TextBox.Text);
                                           }));

    CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox), new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
}

When you press enter, Value gets assigned the most recent Decimal representation of whatever is in TextBox.Text. If you type in some random gibberish or a malformed number like "--4", the TryParse() method inside ParseStringToDecimal() will just return 0, so no invalid number can ever get to Value.

Removing focus

Everytime you click one of the RepeatButtons, we need to remove focus from our TextBox without making the buttons focusable (to get rid of that ugly keyboardfocus-glow). We need to intercept left mouse click on our RepeatButtons as shown below: 

C#
private void AttachIncreaseButton()
{
    var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
    if (increaseButton != null)
    {
        IncreaseButton = increaseButton;
        IncreaseButton.Focusable = false;
        IncreaseButton.Command = _minorIncreaseValueCommand;
        IncreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
    }
}

private void AttachDecreaseButton()
{
    var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
    if (decreaseButton != null)
    {
        DecreaseButton = decreaseButton;
        DecreaseButton.Focusable = false;
        DecreaseButton.Command = _minorDecreaseValueCommand;
        IncreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
    }
}

The RemoveFocus() method is quite simple. Create and fill is as follows:

C#
private void RemoveFocus()
{
    // Passes focus here and then just deletes it
    Focusable = true;
    Focus();
    Focusable = false;
}

That's all there is to this. Focus gets passed to the NumericUpDown and then it's 'destroyed' by turning off the focusability.

Adding MaxValue and MinValue boundaries

Now is an excellent time to implement a limit on the numbers that Value can contain. As the names suggest, MaxValue denotes the maximum number that Value can have and MinValue does just the opposite.

We will need two DependencyProperties. Let's start with MaxValue first:

C#
public static readonly DependencyProperty MaxValueProperty =
    DependencyProperty.Register("MaxValue", typeof (Decimal), typeof (NumericUpDown),
                                new PropertyMetadata(1000000000m, OnMaxValueChanged,
                                                        CoerceMaxValue));
public Decimal MaxValue
{
    get { return (Decimal) GetValue(MaxValueProperty); }
    set { SetValue(MaxValueProperty, value); }
}

private static void OnMaxValueChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceMaxValue(DependencyObject element, Object baseValue)
{
    var maxValue = (Decimal) baseValue;

    return maxValue;
}

Nothing surprising here, so let's add the other one right away:

C#
public static readonly DependencyProperty MinValueProperty =
    DependencyProperty.Register("MinValue", typeof (Decimal), typeof (NumericUpDown),
                                new PropertyMetadata(0m, OnMinValueChanged,
                                                        CoerceMinValue));

public Decimal MinValue
{
    get { return (Decimal) GetValue(MinValueProperty); }
    set { SetValue(MinValueProperty, value); }
}

private static void OnMinValueChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceMinValue(DependencyObject element, Object baseValue)
{
    var minValue = (Decimal) baseValue;

    return minValue;
}

Now that we have these two basic DependecyProperties, we can start working on some robust boundary system. Now, as I'm sure you know, when two opposite ends of a boundary meet, one should 'push' the other one - as shown in the figure below:

Image 4

Figure 4 - Shows a basic boundary system. Keep in mind that all number in this figure are randomly selected to showcase the problem.

So what exactly do we have to do to implement this? It's actually very easy. We just have to make sure that should one of the boundaries shift over the other one, we move it too. To achieve this, you just have to edit both OnMaxValueChanged() and OnMinValueChanged() as follows:

C#
private static void OnMaxValueChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;
    var maxValue = (Decimal) e.NewValue;

    // If maxValue steps over MinValue, shift it
    if (maxValue < control.MinValue)
    {
        control.MinValue = maxValue;
    }
}

private static void OnMinValueChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;
    var minValue = (Decimal) e.NewValue;

    // If minValue steps over MaxValue, shift it
    if (minValue > control.MaxValue)
    {
        control.MaxValue = minValue;
    }
} 

We have the boundaries, let's apply them to the Value every time it changes. Let's create a convenience function to do that for us:

C#
private void CoerceValueToBounds(ref Decimal value)
{
    if (value < MinValue)
    {
        value = MinValue;
    }
    else if (value > MaxValue)
    {
        value = MaxValue;
    }
}

This method returns a value that keeps to the interval of <MinValue; MaxValue>.

Let's go and use this method when we coerce Value:

C#
private static object CoerceValue(DependencyObject element, object baseValue)
{
    var control = (NumericUpDown) element;
    var value = (Decimal) baseValue;

    control.CoerceValueToBounds(ref value);
    
    control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture);

    return value;
}

There is still some work to do before this works as planned. Modify IncreaseValue() and DecreaseValue() as follows:

C#
private void IncreaseValue()
{
    // Get the value that's currently in the TextBox.Text
    var value = ParseStringToDecimal(TextBox.Text);

    // Coerce the value to min/max
    CoerceValueToBounds(ref value);

    // Only change the value if it has any meaning
    if (value <= MaxValue)
    {
        value++;
    }

    Value = value;
} 
C#
private void DecreaseValue()
{
    // Get the value that's currently in the TextBox.Text
    var value = ParseStringToDecimal(TextBox.Text);

    // Coerce the value to min/max
    CoerceValueToBounds(ref value);

    // Only change the value if it has any meaning
    if (value >= MinValue)
    {
        value--;
    }

    Value = value;
}

There is a lot happening here. First, we obtain the contents of our TextBox and get a number out of it. The number is then coerced to the MaxValue and MinValue boudaries. So far, nothing should be a surprise.

The if statement makes sure that we aren't trying to change value if it doesn't make sense to do so. If MinValue is 0 and Value is also 0, there is no point in trying to decrease Value. At the end, we simply assign Value.

Finally, we need to make sure that if MaxValue and MinValue ever change, Value is immediately updated (should it suddenly fall out of the boundaries). There is a problem though, imagine that MinValue is 4, MaxValue is 7 and the Value has been set to 2 and consider the figure below:

Image 5

Figure 5 - Shows how "desired value" can affect a DependencyProperty.

While this functionality might be useful elsewhere, it is highly undesirable for our use. We can rid ourselves of it quite easily, as the two modified methods below show:

C#
private static void OnMaxValueChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;
    var maxValue = (Decimal) e.NewValue;

    // If maxValue steps over MinValue, shift it
    if (maxValue < control.MinValue)
    {
        control.MinValue = maxValue;
    }

    if (maxValue <= control.Value)
    {
        control.Value = maxValue;
    }
}

private static void OnMinValueChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;
    var minValue = (Decimal) e.NewValue;

    // If minValue steps over MaxValue, shift it
    if (minValue > control.MaxValue)
    {
        control.MaxValue = minValue;
    }

    if (minValue >= control.Value)
    {
        control.Value = minValue;
    }
} 

Now every time one of these is changed, Value will self-correct itself to keep to the boundary if necessary. If a change in the boundary (MinValue and MaxValue) causes Value to fall out, Value will get explicitly set to the number it would get coerced to otherwise. When Value gets set explicitly, its desired value is set to the same number, so even if you later relax the boundary, Value will not shift (because it already is at the desired value). It's a neat trick that works great for our needs.

Here is how the process looks like now. The backward assignment to TextBox is no longer useless but essential:

Image 6

Figure 6 - Shows what haopens after TextBox loses focus. The process of coercing Value and assigning it back to TextBox is very important at this point.

Adding decimal places

Finally we get to the ability to specify how many decimal places TextBox shows.

As always, we have to start with a DependencyProperty:

C#
public static readonly DependencyProperty DecimalPlacesProperty =
    DependencyProperty.Register("DecimalPlaces", typeof (Int32), typeof (NumericUpDown),
                                new PropertyMetadata(0, OnDecimalPlacesChanged,
                                                        CoerceDecimalPlaces));

public Int32 DecimalPlaces
{
    get { return (Int32) GetValue(DecimalPlacesProperty); }
    set { SetValue(DecimalPlacesProperty, value); }
}

private static void OnDecimalPlacesChanged(DependencyObject element,
                                            DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceDecimalPlaces(DependencyObject element, Object baseValue)
{
    var decimalPlaces = (Int32) baseValue;

    return decimalPlaces;
} 

Now that we have the DependencyProperty, let's think for a bit. How are we actually going to change the amount of decimal places? Well, it all comes down to a single line in the CoerceValue() method:

C#
control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture); 

What the ToString() above does is format the Decimal number in value to a String according to the rules of CurrentCulture. Just think about all the different ways you can input a value in the correct format.

  • 1,000 (one thousand)
  • 1,000,000.000 (one million)
  • 1.5 (one and a half)

What do the numbers above have in common? Culture! They are all formatted according to the en-US culture format. Look at these values next:

  • 1 000 (one thousand)
  • 1 000 000,000 (one million)
  • 1,5 (one and a half)

Damn, things are getting complicated. It seems that countries like Denmark, Czech Republic and Russia use this strange way of separating numbers and even a decimal comma instead of a period.

"Why are you bringing this up?" you may ask. Well, because if you programmed the NumericUpDown only for people in America and someone from, let's say Czech Republic tried to use them, they'd have a hard time understanding why their decimal comma disappears (since it's treated as a thousand separator).

I'm sure there are more cultures with even stranger culture formats, so how are we supposed to take care of them all? Well, as mentioned above, we can access CultureInfo.CurrentCulture to get most of the relevant information. As the name CurrentCulture suggests, it provides culture information on the computer that is currently executing our application.

CurrentCulture isn't just that, though. We can also use it to make our lives much easier when we are formatting the value. How? Well, CurrentCulture actually specifies how many decimal places are visible when a number is parsed into a String! All we have to do is get a copy of CurrentCulture, modify its amount of decimal places and then use it instead of CultureInfo.CurrentCulture.

Let's start by creating a field for our own culture info:

protected readonly CultureInfo Culture;

Now we need to create a copy of CultureInfo.CurrentCulture. The best place to do so is the instance constructor. Create it and modify it as below:

public NumericUpDown()
{
    Culture = (CultureInfo) CultureInfo.CurrentCulture.Clone();
}

By default (at least on my system), CultureInfo.CurrentCulture specifies 2 decimal places. We have to override this behaviour and use our DecimalPlaces DependencyProperty instead. As it so happens, the constructor is the place to do even this, so let's further modify it as follows:

public NumericUpDown()
{
    Culture = (CultureInfo) CultureInfo.CurrentCulture.Clone();

    Culture.NumberFormat.NumberDecimalDigits = DecimalPlaces;
} 

There is a little complication, though - the number of decimal places cannot be a negative number and by the architecture's limitation, there can only be up to 28 decimal places. Therefore, we must coerce the input in DecimalPlaces. Modify CoerceDecimalPlaces() as follows:

C#
private static object CoerceDecimalPlaces(DependencyObject element, Object baseValue)
{
    var decimalPlaces = (Int32) baseValue;

    if (decimalPlaces < 0)
    {
        decimalPlaces = 0;
    }
    else if (decimalPlaces > 28)
    {
        decimalPlaces = 28;
    }

    return decimalPlaces;
}

Any number that doesn't conform to these two limits will be coerced before it's assigned to the DecimalPlaces property.

All that remains is to update our Culture variable every time DecimalPlaces changes. Modify OnDecimalPlacesChanged() as follows:

C#
private static void OnDecimalPlacesChanged(DependencyObject element,
                                            DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;
    var decimalPlaces = (Int32) e.NewValue;

    control.Culture.NumberFormat.NumberDecimalDigits = decimalPlaces;
} 

The variable decimalPlaces had already been checked in the coercion method, so there's no worry in this assignment.

As I already said, all the formatting is done by a single function, ToString(). As of now, it's being used as follows:

C#
control.TextBox.Text = value.ToString(CultureInfo.CurrentCulture);  

We already know that we have to substitute CultureInfo.CurrentCulture with our Culture variable, but that won't be enough. We also have to specify a format by which the value is (duh) formatted. In our case that's "F", which stands for Fixed-point. You can read more about formats here.

To specify the format and use our own Culture, modify the CoerceValue() method as follows:

C#
private static object CoerceValue(DependencyObject element, object baseValue)
{
    var control = (NumericUpDown) element;
    var value = (Decimal) baseValue;

    control.CoerceValueToBounds(ref value);

    if (control.TextBox != null)
    {
        control.TextBox.Text = value.ToString("F", control.Culture);
    }

    return value;
}  

Splendid, Value is henceforth formatted entirely according to our own rules.  Don't forget to add the null check, because TextBox might not exist when this method is run for the first time.

The very last thing that remains is to update Value every time DecimalPlaces changes. Modify OnDecimalPlacesChanged() as follows:

C#
private static void OnDecimalPlacesChanged(DependencyObject element,
                                            DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;
    var decimalPlaces = (Int32) e.NewValue;

    control.Culture.NumberFormat.NumberDecimalDigits = decimalPlaces;

    control.InvalidateProperty(ValueProperty);
} 

InvalidateProperty() is new to us. All it does is tell Value to invalidate itself (run the CoerceValue() method again). Why is that good? Well, our formatting function (ToString()) is in there. When it formats value, it uses our updated Culture variable with different amount of decimal places - exactly what we need.

Limiting decimal places

Limiting the amount of decimal places will certainly come in handy, so let's implement it.

Unexpectedly, it all starts with two DependencyProperties, MaxDecimalPlaces and MinDecimalPlaces.

C#
public static readonly DependencyProperty MaxDecimalPlacesProperty =
    DependencyProperty.Register("MaxDecimalPlaces", typeof(Int32), typeof(NumericUpDown),
                                new PropertyMetadata(28, OnMaxDecimalPlacesChanged,
                                                        CoerceMaxDecimalPlaces));

public Int32 MaxDecimalPlaces
{
    get { return (Int32)GetValue(MaxDecimalPlacesProperty); }
    set { SetValue(MaxDecimalPlacesProperty, value); }
}

private static void OnMaxDecimalPlacesChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceMaxDecimalPlaces(DependencyObject element, Object baseValue)
{
    var maxDecimalPlaces = (Int32) baseValue;

    return maxDecimalPlaces;
} 
C#
public static readonly DependencyProperty MinDecimalPlacesProperty =
    DependencyProperty.Register("MinDecimalPlaces", typeof(Int32), typeof(NumericUpDown),
                                new PropertyMetadata(0, OnMinDecimalPlacesChanged,
                                                        CoerceMinDecimalPlaces));

public Int32 MinDecimalPlaces
{
    get { return (Int32)GetValue(MinDecimalPlacesProperty); }
    set { SetValue(MinDecimalPlacesProperty, value); }
}

private static void OnMinDecimalPlacesChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceMinDecimalPlaces(DependencyObject element, Object baseValue)
{
    var minDecimalPlaces = (Int32) baseValue;

    return minDecimalPlaces;
} 

MaxDecimalPlaces is by default set to 28, MinDecimalPlaces to 0, both will make use of the PropertyChangedCallback and CoerceValueCallback.

First, let's add the obvious coercion - MaxDecimalPlaces must not exceed 28 and MinDecimalPlaces must be greater than 0. We are basically just shifting the coercion from DecimalPlaces property into these two. Modify CoerceMaxDecimalPlaces() as follows:

C#
private static object CoerceMaxDecimalPlaces(DependencyObject element, Object baseValue)
{
    var maxDecimalPlaces = (Int32)baseValue;
    var control = (NumericUpDown) element;

    if (maxDecimalPlaces > 28)
    {
        maxDecimalPlaces = 28;
    }
    else if (maxDecimalPlaces < 0)
    {
        maxDecimalPlaces = 0;
    }
    else if (maxDecimalPlaces < control.MinDecimalPlaces)
    {
        control.MinDecimalPlaces = maxDecimalPlaces;
    }

    return maxDecimalPlaces;
} 

Nothing special here, just notice the second if statement making sure that MaxDecimalPlaces doesn't cross the other boundary (because otherwise we could push MinDecimalPlaces to negative numbers through MaxDecimalPlaces).

Let's move on to the other one:

C#
private static object CoerceMinDecimalPlaces(DependencyObject element, Object baseValue)
{
    var minDecimalPlaces = (Int32)baseValue;
    var control = (NumericUpDown) element;

    if (minDecimalPlaces < 0)
    {
        minDecimalPlaces = 0;
    }
    else if (minDecimalPlaces > 28)
    {
        minDecimalPlaces = 28;
    }
    else if (minDecimalPlaces > control.MaxDecimalPlaces)
    {
        control.MaxDecimalPlaces = minDecimalPlaces;
    }

    return minDecimalPlaces;
}

Again, notice the second if statement. Otherwise, everything as you'd expect.

Before we move on, let's remove the (now obsolete) boundary checks from DecimalPlaces and replace them with our new boundary system. Modify CoerceDecimalPlaces() as follows:

C#
private static object CoerceDecimalPlaces(DependencyObject element, Object baseValue)
{
    var decimalPlaces = (Int32) baseValue;
    var control = (NumericUpDown) element;

    if (decimalPlaces < control.MinDecimalPlaces)
    {
        decimalPlaces = control.MinDecimalPlaces;
    }
    else if (decimalPlaces > control.MaxDecimalPlaces)
    {
        decimalPlaces = control.MaxDecimalPlaces;
    }

    return decimalPlaces;
} 

The code should be pretty self-explanatory, we're just checking for arbitrary boundary ends instead of the hardcoded 28 and 0.

DecimalPlaces will actually make use of the desired value, so there's no need to avoid it. In that case, we can just invalidate DecimalPlaces everytime either of the boundary ends change. Modify OnMaxDecimalPlaces() and OnMinDecimalPlacesChanged() as follows:

C#
private static void OnMaxDecimalPlacesChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;

    control.InvalidateProperty(DecimalPlacesProperty);
} 
C#
private static void OnMinDecimalPlacesChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;

    control.InvalidateProperty(DecimalPlacesProperty);
}

And that's it. Now everytime DecimalPlaces changes, Value is updated with the right amount of decimal places, and everytime either of the decimal boundary ends changes, DecimalPlaces gets updated, thus updating Value (pardon the redundant explanation). It's all one big happy circle.

Truncation of overflowing decimal places

We have a NumericUpDown with some pretty cool functionality, but think about a little scenario here. What if DecimalPlaces is 0, but some user inputs "0.01" into the TextBox? Well, Value gets set to 0.01! You won't see it, because DecimalPlaces influences the formatting into showing no decimal places, but they are there. And should you bind something to Value, you'll be in for quite a surprise when you discover that it had retained the fraction an proudly holds the number 0.01.

The solution? Get rid of the decimal places that don't belong. Sounds quite simple, but it isn't, because we have to somehow count the amount of decimal places that Value has. Pretty much the only way to do that is create a text representation and figure out how many numbers there are after the decimal symbol. The method below does just that:

C#
using System.Linq;

public Int32 GetDecimalPlacesCount(String valueString)
{
    return valueString.SkipWhile(c => c.ToString(Culture)
            != Culture.NumberFormat.NumberDecimalSeparator).Skip(1).Count();
}

This ugly demon-child of linq actually finds the first decimal point and then counts all characters that follow. If you don't know what LINQ is, checking out dotnetperls is a good start.

The second method we need is one that can remove an arbitrary number of decimal places from a Decimal number. Look at the method below:

private Decimal TruncateValue(String valueString, Int32 decimalPlaces)
{
    var endPoint = valueString.Length - (decimalPlaces - DecimalPlaces);
    var tempValueString = valueString.Substring(0, endPoint);

    return Decimal.Parse(tempValueString, Culture);
}

This method just deletes the unbelonging characters from the end (the unneeded decimal places) and parses the changed string back into a Decimal. While not the cleanest solution, it works on all possible numbers.

Let's use these two methods so that you can see how it all falls together. Modify CoerceValue() as follows:

C#
private static object CoerceValue(DependencyObject element, object baseValue)
{
    var control = (NumericUpDown) element;
    var value = (Decimal) baseValue;

    control.CoerceValueToBounds(ref value);

    // Get the text representation of Value
    var valueString = value.ToString(control.Culture);

    // Count all decimal places
    var decimalPlaces = control.GetDecimalPlacesCount(valueString);

    if (decimalPlaces > control.DecimalPlaces)
    {
        // Remove all overflowing decimal places
        value = control.TruncateValue(valueString, decimalPlaces);
    }

    if (control.TextBox != null)
    {
        control.TextBox.Text = value.ToString("F", control.Culture);
    }

    return value;
}

The hardest part of this were by far the two methods, so the rest should really be a breeze. First, we get a text representation of value. There is no need to specify format there, because numbers that contain decimal point will automatically be formatted with it. Then we count the decimal places and check if value has more of them than is allowed (by DecimalPlaces property). If it does, we remove the places that don't belong and finally format value into TextBox.

Dynamic decimal point 

Right now, if DecimalPlaces is 0 and user inputs 3.14, the input will get truncated to 3. What if we instead wanted to allow the user to dictate the number of decimal places himself, so that if he wishes to manipulate numbers with two decimal places instead, he can simply change it by typing-in a number with that many decimal places? Sounds cool, let's work on that.

First, we'll need to implement a DependencyProperty that will decide if the user is allowed to change DecimalPlaces with his input, or if the input gets truncated. We'll call this property IsDecimalPointDynamic:

C#
public static readonly DependencyProperty IsDecimalPointDynamicProperty =
    DependencyProperty.Register("IsDecimalPointDynamic", typeof(Boolean), typeof(NumericUpDown),
                                new PropertyMetadata(false));

public Boolean IsDecimalPointDynamic
{
    get { return (Boolean)GetValue(IsDecimalPointDynamicProperty); }
    set { SetValue(IsDecimalPointDynamicProperty, value); }
}

This property will be checked every time that Value is assigned. Modify CoerceValue() as follows:

C#
private static object CoerceValue(DependencyObject element, object baseValue)
{
    var control = (NumericUpDown) element;
    var value = (Decimal) baseValue;

    control.CoerceValueToBounds(ref value);

    // Get the text representation of Value
    var valueString = value.ToString(control.Culture);

    // Count all decimal places
    var decimalPlaces = control.GetDecimalPlacesCount(valueString);

    if (decimalPlaces > control.DecimalPlaces)
    {
        if (control.IsDecimalPointDynamic)
        {
            // Assigning DecimalPlaces will coerce the number
            control.DecimalPlaces = decimalPlaces;

            // If the specified number of decimal places is still too much
            if (decimalPlaces > control.DecimalPlaces)
            {
                value = control.TruncateValue(valueString, control.DecimalPlaces);   
            }
        }
        else
        {
            // Remove all overflowing decimal places
            value = control.TruncateValue(valueString, decimalPlaces);
        }
    }
    else if (control.IsDecimalPointDynamic)
    {
        control.DecimalPlaces = decimalPlaces;
    }

    if (control.TextBox != null)
    {
        control.TextBox.Text = value.ToString("F", control.Culture);
    }

    return value;
}

We check if the DecimalPlaces property can be dynamically changed or not. If it can, we have to assign DecimalPlaces as soon as we know how many decimal places valueString has. Why? Because we need to know if the amount of decimal places that valueString has doesn't overstep MinDecimalPlaces and MaxDecimalPlaces (by assigning DecimalPlaces, the number is coerced, remember?). If it doesn't, cool - we go straight to formatting value. If it does, on the other hand, we need to truncate the overflowing decimal places (to the coerced value in DecimalPlaces, this time). If there's less decimal places, the second if statement takes care of changing DecimalPlaces.

The last thing we need to do before this works correctly is shown below:

C#
private static void OnDecimalPlacesChanged(DependencyObject element,
                                            DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;
    var decimalPlaces = (Int32) e.NewValue;

    control.Culture.NumberFormat.NumberDecimalDigits = decimalPlaces;

    if (control.IsDecimalPointDynamic)
    {
        control.IsDecimalPointDynamic = false;
        control.InvalidateProperty(ValueProperty);
        control.IsDecimalPointDynamic = true;
    }
    else
    {
        control.InvalidateProperty(ValueProperty);
    }
}

Why such a check? Well, assume that user types in "3.14", IsDecimalPointDynamic is set to true and DecimalPoint has no limitations. In this case, DecimalPlaces gets merrily set to 2 (the amount of decimal places in 3.14) and everything seems okay. It isn't okay, though. If you then attempt to change DecimalPlaces in any way, Value will override all your attempts.

Every change in DecimalPlaces invalidates Value (the call to InvalidateProperty(), in OnDecimalPlacesChanged(), if you remember), so, when value goes through its CoerceValue() method, it notices that IsDecimalPointDynamic is true and assigns DecimalPlaces to 2 (the amount of decimal places in the number in TextBox), thus overriding whatever DecimalPlaces has been set to before. Whew, that's kinda confusing.

Adding the thousand separator

The thousand separator is certainly very useful for larger numbers. If you don't know what a thousand separator is, just a quick peek here should be enough.

Let's start with a DependendyProperty named IsThousandSeparatorVisible:

C#
public static readonly DependencyProperty IsThousandSeparatorVisibleProperty =
    DependencyProperty.Register("IsThousandSeparatorVisible", typeof(Boolean), typeof(NumericUpDown),
                                new PropertyMetadata(false, OnIsThousandSeparatorVisibleChanged));

public Boolean IsThousandSeparatorVisible
{
    get { return (Boolean)GetValue(IsThousandSeparatorVisibleProperty); }
    set { SetValue(IsThousandSeparatorVisibleProperty, value); }
}

private static void OnIsThousandSeparatorVisibleChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
}

We will use the PropertyChangedCallback to invalidate Value (that reformats TextBox with (or without) the thousand separators).

Modify OnIsThousandSeparatorVisibleChanged() as follows:

C#
private static void OnIsThousandSeparatorVisibleChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown)element;

    control.InvalidateProperty(ValueProperty);
} 

Now we have to actually change the type of formatting based on this DependencyProperty. Modify CoerceValue() as follows:

C#
private static object CoerceValue(DependencyObject element, object baseValue)
{
    var control = (NumericUpDown) element;
    var value = (Decimal) baseValue;

    control.CoerceValueToBounds(ref value);

    // Get the text representation of Value
    var valueString = value.ToString(control.Culture);

    // Count all decimal places
    var decimalPlaces = control.GetDecimalPlacesCount(valueString);

    if (decimalPlaces > control.DecimalPlaces)
    {
        if (control.IsDecimalPointDynamic)
        {
            // Assigning DecimalPlaces will coerce the number
            control.DecimalPlaces = decimalPlaces;

            // If the specified number of decimal places is still too much
            if (decimalPlaces > control.DecimalPlaces)
            {
                value = control.TruncateValue(valueString, control.DecimalPlaces);   
            }
        }
        else
        {
            // Remove all overflowing decimal places
            value = control.TruncateValue(valueString, decimalPlaces);
        }
    }
    else if (control.IsDecimalPointDynamic)
    {
        control.DecimalPlaces = decimalPlaces;
    }

    // Change formatting based on this flag
    if (control.IsThousandSeparatorVisible)
    {
        if (control.TextBox != null)
        {
            control.TextBox.Text = value.ToString("N", control.Culture);
        }
    }
    else
    {
        if (control.TextBox != null)
        {
            control.TextBox.Text = value.ToString("F", control.Culture);
        }
    }

    return value;
} 

Yes, that's right, the secret to adding thousand separators is to change the format from "F" to "N" - Number. Again, don't forget the null check.

Adding minor and major deltas 

To add this functionality, we will have to modify the two callbacks of our commands. Also, this is where two entirely new commands make an appearance:

  • _majorIncreaseValueCommand
  • _majorDecreaseValueCommand

So, in case you've been wondering why exactly did the names of the previous commands start with "minor", here's your answer: minor commands utilize MinorDelta, major commands utilize MajorDelta.

To start with, let's create the DependencyProperties for each of the deltas:

C#
public static readonly DependencyProperty MinorDeltaProperty =
    DependencyProperty.Register("MinorDelta", typeof(Decimal), typeof(NumericUpDown),
                                new PropertyMetadata(1m, OnMinorDeltaChanged,
                                                        CoerceMinorDelta));

public Decimal MinorDelta
{
    get { return (Decimal)GetValue(MinorDeltaProperty); }
    set { SetValue(MinorDeltaProperty, value); }
}

private static void OnMinorDeltaChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceMinorDelta(DependencyObject element, Object baseValue)
{
    var minorDelta = (Decimal)baseValue;

    return minorDelta;
} 

The only differing parts of these two properties are the default values, 1m and 10m. Never forget the m specifier, the control will refuse to compile otherwise!

C#
public static readonly DependencyProperty MajorDeltaProperty =
    DependencyProperty.Register("MajorDelta", typeof(Decimal), typeof(NumericUpDown),
                                new PropertyMetadata(10m, OnMajorDeltaChanged,
                                                        CoerceMajorDelta));

public Decimal MajorDelta
{
    get { return (Decimal)GetValue(MajorDeltaProperty); }
    set { SetValue(MajorDeltaProperty, value); }
}

private static void OnMajorDeltaChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
}

private static object CoerceMajorDelta(DependencyObject element, Object baseValue)
{
    var majorDelta = (Decimal)baseValue;

    return majorDelta;
} 

Now that we have the properties, we need to program the standard boundary system. Modify OnMinorDeltaChanged() as follows:

C#
private static void OnMinorDeltaChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var minorDelta = (Decimal)e.NewValue;
    var control = (NumericUpDown)element;

    if (minorDelta > control.MajorDelta)
    {
        control.MajorDelta = minorDelta;
    }
} 

OnMajorDeltaChanged() will do much the same:

C#
private static void OnMajorDeltaChanged(DependencyObject element,
                                        DependencyPropertyChangedEventArgs e)
{
    var majorDelta = (Decimal) e.NewValue;
    var control = (NumericUpDown) element;

    if (majorDelta < control.MinorDelta)
    {
        control.MinorDelta = majorDelta;
    }
}

We've already done something similar to this, so there really isn't anything else to say. One boundary pushes the other one to make sure that they can never cross - pretty simple.

We can use both deltas right away. Modify IncreaseValue() and DecreaseValue() as follows:

private void IncreaseValue(Boolean minor)
{
    // Get the value that's currently in the _textBox.Text
    decimal value = ParseStringToDecimal(TextBox.Text);

    // Coerce the value to min/max
    CoerceValueToBounds(ref value);

    // Only change the value if it has any meaning
    if (value >= MinValue)
    {
        if (minor)
        {
            value += MinorDelta;
        }
        else
        {
            value += MajorDelta;
        }
    }

    Value = value;
}
private void DecreaseValue(Boolean minor)
{
    // Get the value that's currently in the _textBox.Text
    decimal value = ParseStringToDecimal(TextBox.Text);

    // Coerce the value to min/max
    CoerceValueToBounds(ref value);

    // Only change the value if it has any meaning
    if (value <= MaxValue)
    {
        if (minor)
        {
            value -= MinorDelta;
        }
        else
        {
            value -= MajorDelta;
        }
    }

    Value = value;
}

Instead of just increasing/decreasing Value by one, we use MinorDelta and MajorDelta, based on the minor flag.

To provide the boolean that will decide which delta is used, we need the new commands:

private readonly RoutedUICommand _majorDecreaseValueCommand =
    new RoutedUICommand("MajorDecreaseValue", "MajorDecreaseValue", typeof(NumericUpDown));

private readonly RoutedUICommand _majorIncreaseValueCommand =
    new RoutedUICommand("MajorIncreaseValue", "MajorIncreaseValue", typeof(NumericUpDown));

Now only do we need to bind the new commands, we also need to modify the way IncreaseValue() and DecreaseValue() are invoked due to the added argument.

C#
private void AttachCommands()
{
    CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue(true)));
    CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue(true)));
    CommandBindings.Add(new CommandBinding(_majorIncreaseValueCommand, (a, b) => IncreaseValue(false)));
    CommandBindings.Add(new CommandBinding(_majorDecreaseValueCommand, (a, b) => DecreaseValue(false)));
    CommandBindings.Add(new CommandBinding(_updateValueStringCommand, (a, b) =>
                                           {
                                               Value = ParseStringToDecimal(TextBox.Text);
                                           }));

    CommandManager.RegisterClassInputBinding(typeof (TextBox),
                    new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
    CommandManager.RegisterClassInputBinding(typeof (TextBox),
                    new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox),
                    new KeyBinding(_majorIncreaseValueCommand, new KeyGesture(Key.PageUp)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox),
                    new KeyBinding(_majorDecreaseValueCommand, new KeyGesture(Key.PageDown)));
    CommandManager.RegisterClassInputBinding(typeof (TextBox),
                    new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
}

We are no longer calling IncreaseValue() and DecreaseValue() without any arguments. The flag that we're passing is none other than the minor flag, so that IncreaseValue() and DecreaseValue() use the appropriate delta to modify Value. Also, notice that both major commands are bound to the PageUp/PageDown keys. You can try it right away, focus the NumericUpDown and press either of these keys to increase Value by 10 (the default).  

Allowing auto selection

While I don't really like this feature, I see it pretty regularly in various user interfaces. And I admit, most of the time you probably want to write a completely different number instead of editing the individual digits, so I can see why it might be useful.

At the beginning, there's always a DependencyProperty:

C#
public static readonly DependencyProperty IsAutoSelectionActiveProperty =
    DependencyProperty.Register("IsAutoSelectionActive", typeof(Boolean), typeof(NumericUpDown),
                                new PropertyMetadata(false));

public Boolean IsAutoSelectionActive
{
    get { return (Boolean)GetValue(IsAutoSelectionActiveProperty); }
    set { SetValue(IsAutoSelectionActiveProperty, value); }
}

No PropertyChangedCallback or CoerceValueCallback needed.

We also need to know when TextBox gets clicked, so let's intercept the PreviewMouseLeftButtonUp event. To do so, modify AttachTextBox() method as follows:

C#
private void AttachTextBox()
{
    var textBox = GetTemplateChild("PART_TextBox") as TextBox;

    // A null check is advised
    if (textBox != null)
    {
        TextBox = textBox;
        TextBox.LostFocus += TextBoxOnLostFocus;
        TextBox.PreviewMouseLeftButtonUp += TextBoxOnPreviewMouseLeftButtonUp;
    }
}

The TextBoxOnPreviewMouseLeftButtonUp() (just rolls right off your tongue, doesn't it?) method does the following:

C#
private void TextBoxOnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
    if (IsAutoSelectionActive)
    {
        TextBox.SelectAll();
    }
}

Pretty simple - everytime TextBox gets clicked (provided that IsAutoSelectionActive is true), we select the whole content of TextBox. The selection goes away on second click automatically, so it does allow you to edit individual digits anyway. A win-win scenario right here.

Value wrap-around

The wrap around works by setting Value to the end of a boundary, opposite from the one Value crosses when it's increased or decreased. There's nothing too major here, it shouldn't be a problem to understand.

We'll need a DependencyProperty to know if we can wrap or not:

public static readonly DependencyProperty IsValueWrapAllowedProperty =
    DependencyProperty.Register("IsValueWrapAllowed", typeof(Boolean), typeof(NumericUpDown),
                                new PropertyMetadata(false));

public Boolean IsValueWrapAllowed
{
    get { return (Boolean)GetValue(IsValueWrapAllowedProperty); }
    set { SetValue(IsValueWrapAllowedProperty, value); }
}

No CoerceValueCallback or PropertyChangedCallback needed.

We need to wrap Value. Modify both IncreaseValue() and DecreaseValue() as follows:

private void IncreaseValue(Boolean minor)
{
    // Get the value that's currently in the _textBox.Text
    decimal value = ParseStringToDecimal(TextBox.Text);

    // Coerce the value to min/max
    CoerceValueToBounds(ref value);

    // Only change the value if it has any meaning
    if (value >= MinValue)
    {
        if (minor)
        {
            if (IsValueWrapAllowed && value + MinorDelta > MaxValue)
            {
                value = MinValue;
            } 
            else
            {
                value += MinorDelta;
            }
        }
        else
        {
            if (IsValueWrapAllowed && value + MajorDelta > MaxValue)
            {
                value = MinValue;
            }
            else
            {
                value += MajorDelta;
            }
        }
    }

    Value = value;
}
private void DecreaseValue(Boolean minor)
{
    // Get the value that's currently in the _textBox.Text
    decimal value = ParseStringToDecimal(TextBox.Text);

    // Coerce the value to min/max
    CoerceValueToBounds(ref value);

    // Only change the value if it has any meaning
    if (value <= MaxValue)
    {
        if (minor)
        {
            if (IsValueWrapAllowed && value - MinorDelta < MinValue)
            {
                value = MaxValue;
            }
            else
            {
                value -= MinorDelta;
            }
        }
        else
        {
            if (IsValueWrapAllowed && value - MajorDelta < MinValue)
            {
                value = MaxValue;
            }
            else
            {
                value -= MajorDelta;
            }
        }
    }

    Value = value;
}

And that's it. Now everytime you try to increase or decrease Value past one of the boundaries (provided that IsValueWrapAllowed is true), it will get set to the opposite boundary end.

The default TextBox content

Ah yes. Up until now, NumericUpDown always started up empty. I know, I know, it's easily remedied. Just modify the instance constructor as follows:

public NumericUpDown()
{
    Culture = (CultureInfo) CultureInfo.CurrentCulture.Clone();
           
    Culture.NumberFormat.NumberDecimalDigits = DecimalPlaces;

    Loaded += OnLoaded;
}

Yes, we have to intercept this event, because only then can we be sure that TextBox has already been attached.

The OnLoaded() method looks like this:

private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
    InvalidateProperty(ValueProperty);
}

All we have to do here is invalidate Value, that will automatically fill TextBox. Pretty simple.

Zeroing-out on right click

Let's implement a piece of functionality that I saw and instantly loved. Basically, you can right-click any of the buttons on the side and zero-out Value. It's very handy and saves a lot of time.

All we have to do is intercept right-clicks on both of the RepeatButtons and while at it, remove the focus:

C#
private void AttachIncreaseButton()
{
    var increaseButton = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
    if (increaseButton != null)
    {
        IncreaseButton = increaseButton;
        IncreaseButton.Focusable = false;
        IncreaseButton.Command = _minorIncreaseValueCommand;
        IncreaseButton.PreviewMouseRightButtonDown += ButtonOnPreviewMouseRightButtonDown;
        IncreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
    }
}

private void AttachDecreaseButton()
{
    var decreaseButton = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
    if (decreaseButton != null)
    {
        DecreaseButton = decreaseButton;
        DecreaseButton.Focusable = false;
        DecreaseButton.Command = _minorDecreaseValueCommand;
        DecreaseButton.PreviewMouseRightButtonDown += ButtonOnPreviewMouseRightButtonDown;
        DecreaseButton.PreviewMouseLeftButtonDown += (sender, args) => RemoveFocus();
    }
}

We use the same event handler for both of the buttons. Create ButtonOnPreviewMouseRightButtonDown() as follows:

C#
private void ButtonOnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
    Value = 0;
}

And that's it. We don't have to care if Value can get set to 0. If it can't, Value will just coerce it.

Adding the ability to cancel unconfirmed changes

This piece of functionality is very useful. When you write something into the TextBox and you don't confirm it, you will be able to just get rid of the typed-in value by pressing escape, thus cancelling the unconfirmed change.

First, we need to set up our TextBox to allow undo. Modify AttachTextBox() as below:

private void AttachTextBox()
{
    var textBox = GetTemplateChild("PART_TextBox") as TextBox;

    // A null check is advised
    if (textBox != null)
    {
        TextBox = textBox;
        TextBox.LostFocus += TextBoxOnLostFocus;
        TextBox.PreviewMouseLeftButtonUp += TextBoxOnPreviewMouseLeftButtonUp;
                
        TextBox.UndoLimit = 1;
        TextBox.IsUndoEnabled = true;
    }
}

This will allow us to revert preciselly one change.

Next, we need to create a command named _cancelChangesCommand, a pretty descriptive name. Add it as shown below:

C#
private readonly RoutedUICommand _cancelChangesCommand =
            new RoutedUICommand("CancelChanges", "CancelChanges", typeof(NumericUpDown));

As you probably already know, now we need to bind the command to its respective callback and assign it an InputBinding. Modify AttachCommands() as below:

C#
private void AttachCommands()
{
    CommandBindings.Add(new CommandBinding(_minorIncreaseValueCommand, (a, b) => IncreaseValue(true)));
    CommandBindings.Add(new CommandBinding(_minorDecreaseValueCommand, (a, b) => DecreaseValue(true)));
    CommandBindings.Add(new CommandBinding(_majorIncreaseValueCommand, (a, b) => IncreaseValue(false)));
    CommandBindings.Add(new CommandBinding(_majorDecreaseValueCommand, (a, b) => DecreaseValue(false)));
    CommandBindings.Add(new CommandBinding(_updateValueStringCommand, (a, b) =>
                                           {
                                               Value = ParseStringToDecimal(TextBox.Text);
                                           }));
    CommandBindings.Add(new CommandBinding(_cancelChangesCommand, (a, b) => CancelChanges()));

    CommandManager.RegisterClassInputBinding(typeof (TextBox),
                    new KeyBinding(_minorIncreaseValueCommand, new KeyGesture(Key.Up)));
    CommandManager.RegisterClassInputBinding(typeof (TextBox),
                    new KeyBinding(_minorDecreaseValueCommand, new KeyGesture(Key.Down)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox),
                    new KeyBinding(_majorIncreaseValueCommand, new KeyGesture(Key.PageUp)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox),
                    new KeyBinding(_majorDecreaseValueCommand, new KeyGesture(Key.PageDown)));
    CommandManager.RegisterClassInputBinding(typeof (TextBox),
                    new KeyBinding(_updateValueStringCommand, new KeyGesture(Key.Enter)));
    CommandManager.RegisterClassInputBinding(typeof(TextBox),
                    new KeyBinding(_cancelChangesCommand, new KeyGesture(Key.Escape)));
}

The _cancelChangedCommand calls CancelChanges() - a very simple method as shown below:

C#
private void CancelChanges()
{
    TextBox.Undo();
}

This method is really primitive, I believe no explanation is needed.

There is one last thing we need to do, though. Modify OnValueChanged() as follows:

C#
private static void OnValueChanged(DependencyObject element,
                                    DependencyPropertyChangedEventArgs e)
{
    var control = (NumericUpDown) element;

    control.TextBox.UndoLimit = 0;
    control.TextBox.UndoLimit = 1;
}

Setting the UndoLimit to 0 erases any undo information that TextBox is saving. Why do this? It's because if we didn't do this, you'd be able to undo any change (even confirmed ones) and that is not what we need. After erasing the undo information, we restore the limit to 1.

This functionality certainly isn't complex, but I find it remarkably useful.

Conclusion

And that's it. Now we should have a fully functioning NumericUpDown control that you can use.  I hope that you enjoyed reading this article as much as I enjoyed writing it (actually, proof-reading it was probably the most boring thing I've done all year).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugException when change value programmatically Pin
Member 1521347313-Jan-22 17:17
Member 1521347313-Jan-22 17:17 
QuestionPlease explain for a jerk Pin
Member 150912736-Mar-21 10:22
Member 150912736-Mar-21 10:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.