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).
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:
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.
<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:
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:
[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:
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:
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.
protected TextBox TextBox;
private void AttachTextBox()
{
var textBox = GetTemplateChild("PART_TextBox") as TextBox;
if (textBox != null)
{
TextBox = textBox;
}
}
Attaching the RepeatButtons is a no-brainer at this point.
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
- _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
:
#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.
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.
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.
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()
.
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:
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()
:
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:
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:
- NumericUpDown loses focus
- User uses keyboard arrows or the buttons on the side to increase/decrease
Value
- 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:
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:
private void AttachTextBox()
{
var textBox = GetTemplateChild("PART_TextBox") as TextBox;
if (textBox != null)
{
TextBox = textBox;
TextBox.LostFocus += TextBoxOnLostFocus;
}
}
Create the TextBoxOnLostFocus()
method and update Value
with the content of TextBox:
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:
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:
private void IncreaseValue()
{
var value = ParseStringToDecimal(TextBox.Text);
value++;
Value = value;
}
private void DecreaseValue()
{
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:
private readonly RoutedUICommand _updateValueStringCommand =
new RoutedUICommand("UpdateValueString", "UpdateValueString", typeof (NumericUpDown));
The command has to be bound to its InputBinding:
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:
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:
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:
private void RemoveFocus()
{
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:
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:
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:
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:
private static void OnMaxValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var maxValue = (Decimal) e.NewValue;
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 > 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:
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
:
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:
private void IncreaseValue()
{
var value = ParseStringToDecimal(TextBox.Text);
CoerceValueToBounds(ref value);
if (value <= MaxValue)
{
value++;
}
Value = value;
}
private void DecreaseValue()
{
var value = ParseStringToDecimal(TextBox.Text);
CoerceValueToBounds(ref value);
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:
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:
private static void OnMaxValueChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
var maxValue = (Decimal) e.NewValue;
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 > 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:
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:
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:
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:
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:
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:
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:
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:
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
.
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;
}
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:
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:
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:
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:
private static void OnMaxDecimalPlacesChanged(DependencyObject element,
DependencyPropertyChangedEventArgs e)
{
var control = (NumericUpDown) element;
control.InvalidateProperty(DecimalPlacesProperty);
}
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:
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:
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
var valueString = value.ToString(control.Culture);
var decimalPlaces = control.GetDecimalPlacesCount(valueString);
if (decimalPlaces > control.DecimalPlaces)
{
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
:
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:
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
var valueString = value.ToString(control.Culture);
var decimalPlaces = control.GetDecimalPlacesCount(valueString);
if (decimalPlaces > control.DecimalPlaces)
{
if (control.IsDecimalPointDynamic)
{
control.DecimalPlaces = decimalPlaces;
if (decimalPlaces > control.DecimalPlaces)
{
value = control.TruncateValue(valueString, control.DecimalPlaces);
}
}
else
{
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:
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
:
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:
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:
private static object CoerceValue(DependencyObject element, object baseValue)
{
var control = (NumericUpDown) element;
var value = (Decimal) baseValue;
control.CoerceValueToBounds(ref value);
var valueString = value.ToString(control.Culture);
var decimalPlaces = control.GetDecimalPlacesCount(valueString);
if (decimalPlaces > control.DecimalPlaces)
{
if (control.IsDecimalPointDynamic)
{
control.DecimalPlaces = decimalPlaces;
if (decimalPlaces > control.DecimalPlaces)
{
value = control.TruncateValue(valueString, control.DecimalPlaces);
}
}
else
{
value = control.TruncateValue(valueString, decimalPlaces);
}
}
else if (control.IsDecimalPointDynamic)
{
control.DecimalPlaces = decimalPlaces;
}
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:
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!
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:
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:
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)
{
decimal value = ParseStringToDecimal(TextBox.Text);
CoerceValueToBounds(ref value);
if (value >= MinValue)
{
if (minor)
{
value += MinorDelta;
}
else
{
value += MajorDelta;
}
}
Value = value;
}
private void DecreaseValue(Boolean minor)
{
decimal value = ParseStringToDecimal(TextBox.Text);
CoerceValueToBounds(ref value);
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.
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:
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:
private void AttachTextBox()
{
var textBox = GetTemplateChild("PART_TextBox") as TextBox;
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:
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)
{
decimal value = ParseStringToDecimal(TextBox.Text);
CoerceValueToBounds(ref value);
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)
{
decimal value = ParseStringToDecimal(TextBox.Text);
CoerceValueToBounds(ref value);
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:
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:
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;
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:
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:
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:
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:
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).