Introduction
This article is about a C#/WPF .NET 4.0 numeric edit control ‘NumberBox
’, which enables users to enter numbers. I have tried to make this control as flexible as possible and not made too many assumptions about use. I have also gone out of my way to develop a control of the highest quality possible. Enabling/disabling, resizing, BorderThickness
and all other standard control properties should work as expected. ‘NumberBox
’ allows numbers to be entered in different ways, so as to support various cultures. The number entered may either use system formatting or be overloaded by whatever the programmer set it up with.
General Concepts
In this simple control, I have added a large number of properties. For practical reasons, it should be possible to set these in style, so you can configure all these at once for all controls used in an application. These are therefore all dependency properties.
For more clarification, I have classified these properties into 3 categories:
- Properties that affect the type and format of value in control. I shall call these ‘value properties’.
- Properties that affect the way user types input into control. The underlying value is not affected. Let us label these ‘Input properties’.
- Properties that only affect the display of value. Call these ‘Display properties’.
In order to have a truly international control, I have used system settings for numbers. On my Windows 7 machine, these can be configured by clicking ‘Control Panel’, then ‘Clock, Language, and Region’, ‘Change the date, time, or number format’ and finally press the ‘Additional settings…’ button on bottom right:
Global Properties
CtrlFlags
public byte CtrlFlags { get; set; }
where byte is a flag composed of one or several of values below:
public enum CtrlFlag : byte
{
AllowMain = 0x0001,
AllowFractions = 0x0002,
AllowNegatives = 0x0004,
AllowScientific = 0x0008,
AllowNegSci = 0x0010,
LoadScientific = 0x0020
}
Input property - default is ‘AllowMain | AllowNegatives
’. For scientific notation, AllowMain
flag has got to be set, on top of AllowScientific
. AllowScientific
on its own has no effect.
It is apparently not possible to set the flag in XMAL. See Silverlight 4 can't set a flag enum type property in XAML. However, this is not a big issue, since it can easily compute the short value and pass this directly. This is OK if you don’t change value of flags (which shouldn’t change anyway). Example: to allow only positive fractions: set to 2, to set all flags, set to 15, to allow main, negatives, and scientific notation, set to 13.
When AllowNegatives
is set, a negative is entered the same way as it is entered in a calculator: simply by pressing the negative symbol button (usually the minus sign '-'). The position of the caret does not matter, as long as the control is getting keyboard input focus.
When AllowScientific
is set, scientific notation is possible. The caret must be either at the beginning or immediately after the main number for it to be possible to enter either an 'e' or 'E'. It is not possible to enter 'e' or 'E' after a space or with caret on a fraction element, when AllowFractions
flag is set. The number following an 'e' or 'E' may only be a positive integer. By default, a number entered with AllowScientific
expands on exit, so it essentially provides a shorthand for entering long numbers. You just need to type "1.23e12
", rather than "1230000000000
". If user enters an 'e' or 'E' with no digits following, assume the entry is "e0
" or "E0", which is 10 power 0, which is 1. Therefore, value in field is 1 on exiting when only 'e' or 'E' was entered or 'e' or 'E' is ignored if following preceding digits. This happens even if ScientificDisplayType
is not set to ExpandOnExit
. The value in Scientific section is simply ignored if too large and cannot be held in a decimal.
When AllowNegSci
flag is set, it is possible to enter a negative scientific number such as 'e-9'. To do this, press the negative symbol (usually '-') when the caret position is after the 'e' or 'E' (and before the space if AllowFractions
also set). If ScientificDisplayType
is set to ExpandOnExit
, it is recommended that DecimalPlaces
is set to -1. Doing otherwise will probably show something like '0.00' (see DecimalPlaces
section below for more information). Only meaningful if AllowScientific
also set.
When LoadScientific
is set, a number will load in scientific format. This flag only works if AllowScientific
is set. For small numbers, AllowNegSci
needs to be also set. The number of digits displayed in coefficient is given by DecimalPlaces
if DecimalPlaces
> 0. Otherwise, SignificantDigits
is used.
When AllowFractions
is set, it is possible to enter fractions. These always consist of integer, and no floating point is allowed in these. There are 2 scenarios:
AllowMain
also set. User has to type space key button (' '), to separate the fraction from the main part. If an entry such as " 1234
" is entered, the space is ignored, and the value is considered to be "1234
" on exiting. If an entry such as "123 12
" or "123 12/
" is input, this is considered as "123 12/1
". If "12 /16
" is typed, this is considered as "12 1/16
". Essentially the value on either side of the slash is considered to be a '1' if left empty.
AllowMain
not set. Typing a space is not allowed. Similarly, if either side of slash '/' is left empty, it is considered as a '1'. If you just enter a number "1234
", this becomes "1234/1
" on exiting.
To recapitulate, numbers are entered as shown in the figure below:
- In orange, negative sign section, which may either be prefix or suffix, depending on culture or setup
- In pink, Main number section
- In light blue, Scientific number section
- In light purple, a space
- In light green, Fraction section
DecValue
public decimal? DecValue { get; set; }
Input property - default value is empty. This property allows us to set and retrieve the value in Main and Scientific section. The value is stored in a decimal. If you need to set/get values in Fraction section, use NumValue
property described below.
NumValue
public NumberValue? NumValue { get; set; }
where:
public struct NumberValue
{
public decimal Main;
public long Numerator, Denominator;
}
Input property - default value is empty. This property allows us to set and retrieve all values. Values in Main and Scientific section are stored in Main
field of NumberValue
. Numerator and denominator are stored in Numerator
and Denominator
fields respectively. The numerator takes the same sign as the main field, which is logical. For example: -1 3/4 = -(1 + 3/4) = -1 -3/4
Properties that Apply when AllowMain Flag is Set
Properties below only apply when AllowMain
flag is set in CtrlFlags
property.
DecimalPlaces
public short DecimalPlaces { get; set; }
Value property - default is 2. This sets/gets the number of decimal places allowed after decimal separator. If set to 0, user can no longer enter the decimal separator. If set to a negative number, the number of decimals after decimal separator is no longer restricted.
The rules for the number of decimal spaces displayed when DecimalPlaces
is set to a negative number are as follows:
- When
ScientificDisplayType
is set to ExpandOnExit
, the number of digits after decimal separator of expanded value is given by initial number of digits entered after decimal symbol minus exponent. If this value is less than zero, no digits displayed after decimal symbol, which itself is not displayed. Example: 1.234e-5, Number of digits after decimal symbol is 3 -(-5) = 8. Other example: 1.67e12, number of digits displayed: 2 - 12 = -10 < 0, so no digits displayed.
- When setting an initial value using
NumValue
or DecValue
:
- If this value is 0, no digits after decimal symbol shown.
- Otherwise, for a value >= 1 or <= -1, will display up to four non zero digits after decimal separator. As soon as it encounters a zero, no longer displays following digits. Example: 12345.6789 will be shown as such. 12.309 will be displayed as 12.3. 1.234567 will be displayed as 1.2346
- Otherwise, for a |value| < 1, computes number of decimals required + an extra four possible, using same rules above. Example: 0.00000001 will be displayed as such, 0.0012345 will be displayed as such, 0.000000012345678 will be rounded to 0.000000012346.
DecimalSeparatorType
public DecimalSeparatorType DecimalSeparatorType { get; set; }
where:
public enum DecimalSeparatorType : byte
{
System_Defined,
Point,
Comma
}
Input property - default is ‘System_Defined
’. In the UK or US, a point is used as decimal separator in a number such as ‘pi = 3.14’. In France, a comma is used instead, as in ‘pi = 3,14’. This property enables a NumberBox
to supersede the system setting.
SignificantDigits
public byte SignificantDigits { get; set; }
Input property. For very small numbers, this property indicates the number of significant digits. For example, 0.00000456789 will display as 0.00000457 if SignificantDigits
is set to 3. 5 by default.
GroupSeparatorType
public GroupSeparatorType GroupSeparatorType { get; set; }
where:
public enum GroupSeparatorType : byte
{
System_Defined,
Comma,
Point,
Space,
HalfSpace
}
Display property - default is ‘HalfSpace
’ (and not ‘System_Defined
’, as I think ‘HalfSpace
’ gives a nicer display). This property gets/sets the symbol usually used for separating blocks of 3 digits in a number. In the US or UK an comma is often used, but sometimes also a space. In France, a point is used instead of a comma. The ‘HalfSpace
’ option is not available as a standard system setting, which is a pity, since I find the gap of a space is too wide.
GroupSize
public short GroupSize { get; set; }
Display property - default is 3. Large numbers are often grouped in blocks of 3 digits. This property enables changes to a value other than 3. In France, telephone numbers are usually shown in blocks of 2 decimals for instance.
Properties that Apply when AllowNegatives Flag is Set
Properties below only apply when AllowNegatives
flag is set in CtrlFlags
property and user has entered a negative number.
NegativeSignSide
public NegativeSignSide NegativeSignSide { get; set; }
where:
public enum NegativeSignSide : byte
{
System_Defined,
Prefix,
Suffix
}
Input property - default is ‘System_Defined
’. In some cultures, negative numbers are suffixed with the negative sign symbol, rather than prefixed. This property allows you to either use the system setting (default), or specify whether negative symbol should be prefix or suffix of number. If you chose ‘System_Defined
’, assume it is prefix if system format is set to ‘(1.1)’, ‘-1.1’ or ‘- 1.1’. Otherwise, it is suffix.
NegativePatternType
public NegativePatternType NegativePatternType { get; set; }
where:
public enum NegativePatternType : byte
{
System_Defined,
Symbol_NoSpace,
Symbol_Space,
Parentheses
}
Display property - default is ‘System_Defined
’. The negative pattern may be shown differently: the system allows: ‘(1.1)’, ‘-1.1’, ‘- 1.1’, ‘1.1-’, ‘1.1 -’. If you chose ‘Symbol_NoSpace
’, format is either ‘-1.1’ or ‘1.1-’, if you chose ‘Symbol_Space
’ , format is either ‘- 1.1’ or ‘1.1 -’. If you chose ‘Parentheses
’, format is ‘(1.1)’. If chose ‘System_Defined
’, the format shall be as set in system, except that this property is superseded by NegativeSignSide
. So, for example, if system is set to ‘1.1 -’, and NegativeSignSide
is set to Prefix
, than the resulting display shall be: ‘- 1.1’, and not ‘1.1 -’. The system setting space is kept.
NegativeTextBrush
public Brush NegativeTextBrush { get; set; }
Display property - default is null
(not set). This property allows you to set negative numbers in a color different from positive number. A common practice is to show negatives in red.
NegativeSignType
public NegativeSignType NegativeSignType { get; set; }
where:
public enum NegativeSignType : byte
{
System_Defined,
Minus
}
Input property - default is ‘System_Defined
’ A negative number is as far as I know represented by the minus sign (‘-’). There is a default setting that allows you to represent negative numbers with a sign other than ‘-’. This property allows you to overload the default setting with a minus sign.
NegativeSignSide = Prefix or System_Defined of type ‘(1.1)’, ‘-1.1’ or ‘- 1.1’. |
NegativeSignSide = Suffix or System_Defined of type ‘1.1-’ or ‘1.1 -’. |
|
|
Properties that Apply when AllowScientific Flag is Set
Properties below only apply when AllowScientific
and AllowMain
flags are set in CtrlFlags
property and user has entered digits immediately following an ‘e’ or ‘E’.
ScientificDisplayType
public ScientificDisplayType ScientificDisplayType { get; set; }
where:
public enum ScientificDisplayType : byte
{
CapitalE,
SmallE,
Pow10,
ExpandOnExit
}
Display property, apart from choice ‘ExpandOnExit
’, which affects input. Default value is ‘ExpandOnExit
’.
Using the Code
Using the code should be extremely straightforward, and complies with XAML standard, apart from the properties for setting/getting values from control. I have provided examples in sample.
Prototype style:
<Style x:Key="NumberBoxStyle" TargetType="{x:Type local:NumberBox}">
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="BorderBrush" Value="Blue"/>
<Setter Property="Foreground" Value="Green"/>
<Setter Property="TextAlignment" Value="Left"/>
<Setter Property="CtrlFlags" Value="15"/>
<Setter Property="DecimalPlaces" Value="3"/>
<Setter Property="DecimalSeparatorType" Value="Point"/>
<Setter Property="GroupSeparatorType" Value="Space"/>
<Setter Property="GroupSize" Value="4"/>
<Setter Property="NegativeSignType" Value="Minus"/>
<Setter Property="NegativePatternType" Value="Parentheses"/>
<Setter Property="NegativeTextBrush" Value="Red"/>
<Setter Property="ScientificDisplayType" Value="Pow10"/>
</Style>
Example of use:
<local:NumberBox x:Name="NumBoxSty1"
Style="{StaticResource NumberBoxStyle}"
HorizontalAlignment="Stretch"
Margin="15,30,15,0" VerticalAlignment="Top"/>
Silverlight 4 Version
I have added a version of control that works in Silverlight 4, which I had to download as not shipped with my version of Visual Studio 2010. Silverlight 3 has too many issues, and a simple XAML statement such as Foreground="{Binding Foreground, ElementName=Root}"
will not work correctly for instance.
The biggest change from WPF was replacing all instances of FormattedText
, not available in Silverlight with instances of TextBox
.
CoerceValueCallback
is not available either, and it is not possible to reset default values of already defined DependencyProperties
. This means that you explicitly need to set BorderThickness
to 1
and Background
to White
in client instance of NumberBox
, otherwise get a transparent NumberBox
with no border. For example:
<local:NumberBox x:Name="FirstNumBoxBox" BorderThickness="1" Background="White"/>
There is no PreviewKeyDown
event and I had to use KeyDown
instead. This event does not always receive messages on pressing buttons. For instance, when first pressing ‘Home’ button, no message was received. This means you cannot systematically set caret to be after negative sign, as with WPF version. I have left the function handling event identical to WPF version in the hope that a future version of Silverlight will have the functionality.
If a NumberBox
’s height is such that characters inside are not displayed in full, then get a blank display when inactive. This is because DisplayNumEdit
’s field ‘Text
’ changes for some reason.
Otherwise, everything important works fine, and all the new dependency properties of WFP version are also available in Silverlight. Behaviour is the same as well.
I feel that with the ever increasing importance of the internet, it was critical to have a web version of control.
Points of Interest
This control supports copy/paste: it is not possible to paste a string that is in the wrong format for control, and when right clicking on ‘NumberBox
’, ‘Paste’ is disabled.
Similarly for drag and drop: it is not possible to drop a string that is in an invalid format. When dropping, the point at which insertion takes place may not be what user expected. However, I think it will be unusual values are entered this way for such a control anyway. In all circumstances, it should never be possible to input a string in wrong format.
This control uses only the standard control ‘TextBox
’, so it should support all themes and there should be no issues here. I have been unable to obtain the standard disabled text color of a control. I have used a standard value #FF6D6D6D
. Unfortunately, SystemColors
does not seem to return any value that corresponds to that in current themes in which my computer is set up with.
I am not entirely comfortable about using a user control with a Grid
, two TextBoxes
and the DisplayNumEdit
, all for a single control. A more lightweight solution might be feasible. However, I might also encounter major obstacles in doing so.
There is no handling for minimum or maximum and no properties for that. If a number is too large, no exception is thrown. The modifications required for such handling are not difficult to implement, but I am leaving such a change to developers using this code. The best solution is probably for DecValue
and NumValue
methods to throw errors. I am reluctant to make any properties throw uncaught exception, because if developer using this sample forgets the try
…catch
block, it is probably this code that is going to get the blame.
Finally, I am sure you have downloaded code before and found loads of issues. I have gone out of my way to fix any problems I have been aware of. However, should you find by any chance a bug, please let me know. I would also like to know of potential improvements.
Acknowledgment
One of the difficulties in writing this control is in guessing user and developer requirements. For this, I looked at comments of a similar control, this time in old MFC: Numeric Edit Control. This gave me the idea of scientific notation and support of different negative formats. Otherwise, display of fractions, showing negatives in red, spacing between sets of 3 digits and the shorthand ExpandOnExit
in scientific notation for typing large numbers came from my experience doing user interface development for a financial Software House I used to work for. I have designed this user control to suit the maximum number of needs.
History
- 1st June, 2011: Initial version
- 11th June, 2011: New version with following bug fixes and enhancements:
- No longer possible to enter negative symbol when ‘
AllowNegatives
’ flag not set.
- If flag ‘
AllowFractions
’ not set, no longer possible to display fractions using NumValue
property.
NegativeTextBrush
is now correctly deactivated when a previously negative entry becomes positive.
- ‘
ExpandOnExit
’ now works correctly when DecimalPlaces
< 0.
- ‘
NumValue
’ and ‘DecValue
’ properties now work correctly when DecimalPlaces
< 0.
- Proper behaviour discussed when
DecimalPlaces
< 0.
- New flag ‘
AllowNegSci
’ that allows a negative exponent.
- 20th June, 2011: Silverlight 4 version added and small bug fix: The negative sign was being removed from scientific values when entering
a negative value – that is a value with 2 negative signs did not work correctly. For example “-1.234e-5” was failing.
- 25th March, 2012: Bug fixes and enhancements. In particular, fixed all issues reported in 'Comments & Discussions' below.
DecValue
and NumValue
are now dependency properties. This allows these to be used inside a DataGrid
. Also these properties now take/return nullable parameters. This is to allow for setting/getting a null, blank field.
NegativeTextBrush
now also applies when NumberBox
has focus (bug).
- New
LoadScientific
flag implemented.
- If call
DecValue
or NumValue
after control is displayed, new value will now be shown (bug - WPF only).
- In SilverLight, fixed an issue when you previously typed a fraction and then erased this, a gap was shown (bug - SilverLight only).
- New property
SignificantDigits
added.
- StrIsFilled function replaced with !string.IsNullOrEmpty.