Introduction
This article presents a solution to the general problem of binding a TextBox
control to a numeric (decimal
, float
, int
, etc.) value. Because the assumed context is business programming, the article treats decimal
as the most generally-applicable numeric type, even though float
and double
can represent wider ranges of values.
Background
Developers of business software face challenges when accepting user input of numeric values containing decimal points, such as currency values and measurements of various kinds (possibly most often, weight). One may feel compelled to purchase a specialized control that solves the attendant problems, and that option may work well; however, this article aims to show how the problem can be adequately addressed while working with the "plain, vanilla" TextBox
controls provided with the .NET Framework. Although the solution set forth in this article and accompanying code was developed to work with Silverlight, since WPF, ASP.NET and Windows Forms TextBox controls all work very similarly, it should be easy to use this solution with any of the .NET Framework's TextBox
controls, with minimal (if any) modification.
Conversions are required, both in displaying numeric data and in accepting a string
typed in by the user, to later store the data as a value of some numeric type and/or to do calculations with it.
Issues related to formatting numeric values for display are relatively simple. They involve:
- Displaying the data in a particular standard or custom numeric format, such as "rounded to two decimal places, without grouping digits to the left of the decimal point."
- Accounting for the user's culture. For example, a user in the USA will expect to see the '.' character used to represent a decimal point, whereas a user in the Netherlands will expect to see ',' used for that purpose. (Allowing the current thread's culture to take care of this is the default approach, but it is not always an adequate approach, or the most appropriate one.)
Dealing with the issues mentioned above is not complicated. Code like the following will get the job done:
myTextBox.Text = myDecimalValue.ToString("N2", userCulture);
However, users being users, accepting and converting numeric input can get a little bit messy.
- Q. What if the user groups digits by thousands, inserting the group separator appropriate to her/his culture?
- Q. What if the user inputs more or fewer decimal places than expected?
- Q. What if the user types in a "garbage" string that cannot be successfully converted to a numeric value?
- Q. Should anything be done to accommodate a user who enters a currency symbol preceding a currency value, or a percent symbol following a percentage value?
None of those questions seems difficult to answer (although answers may vary), and implementing the desired functionality is not too difficult either. Here are the answers implemented via the downloadable code accompanying this article:
- Q. What if the user groups digits by thousands, inserting the group separator appropriate to her/his culture?
A. This is OK. Thousands separators are ignored when parsing input.
- Q. What if the user inputs more or fewer decimal places than expected?
A. Extend with zero(s) if too few; truncate if too many.
- Q. What if the user types in a "garbage"
string
that cannot be successfully converted to a numeric value?
A. The result of parsing invalid input is zero.
- Q. Should anything be done to accommodate a user who enters a currency symbol preceding a currency value, or a percent symbol following a percentage value?
A. Check for such symbols and remove them if present; then trim what's left, in case the currency symbol was followed by a space or the percent symbol was preceded by one.
Considering the entire suite of problems mentioned above and contemplating that a significant amount of ad hoc code could be written to handle different cases, it becomes clear that an encapsulated solution, a class that comprehensively and dependably solves the suite of problems listed (and perhaps a few others as well), is desirable.
One's first attempt to develop a class that does a standard set of conversions and is an intermediary in binding scenarios will naturally be a converter class (implementing IValueConverter
). However, one may find that it is desirable for the class methods (Convert
and ConvertBack
, or their functional equivalents) to have access to the numeric or string
objects to which their return values will be assigned. This is not normally a feature of converter classes, and may be somewhat difficult and awkward to "graft in."
DecimalStringBinder
, the class presented by this article, technically is not a converter class, because it does not implement IValueConverter
. Therefore, DecimalStringBinder
objects cannot be referenced as converters in XAML binding statements. Instead, DecimalStringBinder
objects can be directly bound to controls via XAML binding statements. They fit comfortably and naturally into the ViewModel
layer of an MVVM-structured solution, as illustrated below. Alternatively, DecimalString
Binder objects can be declared and managed in presentation class code-behind files. Only if there is no confusion to the effect that we're implying that the class implements IValueConverter
, it may make sense to loosely refer to DecimalStringBinder
as a kind of "converter class."
// View ViewModel Model [data contract]
// TextBox.Text <- binding -> DecimalStringBinder <- binding -> numeric property
Using the Code
In this section, I will present an overview of DecimalStringBinder
. Please look to the Points of Interest section for specific examples of how to use the class to accomplish some specific goals and solve some specific problems.
Constructors
No less than seven constructor overloads are provided, which permit an object to be initialized in a variety of ways. Probably the most generally useful overload, when the object will be involved in binding to a UI, is this one:
public DecimalStringBinder
(INotifyPropertyChanged decimalPropertyParentObject, PropertyInfo decimalProperty)
Properties
The bulk of the work done by a DecimalStringBinder
object is done via properties, supported by some private
methods. Here is a listing of the public
properties, with short descriptions:
CultureInfo UserCulture |
Gets or sets the user's culture - not necessarily the same as the culture associated with the bound numeric property. |
int DecimalPlaces |
Gets or sets the number of decimal places used for parsing and display of values. |
Enums.NumericFormat OutputFormat |
Gets or sets the format specifier used by the RefreshStringValue method. |
string CustomOutputFormat |
Gets or sets a custom format string used by the RefreshStringValue method. Overrides OutputFormat if both are specified.... |
NumberStyles NumberStyles |
Gets or sets the NumberStyles value used in parsing an input StringValue . |
string ZeroValue |
Gets or sets a special value (such as the empty string) that is desired to represent the value zero. |
string StringPrefix |
Gets or sets an optional prefix used by RefreshStringValue in formatting numeric values, and recognized as valid and stripped out of input strings (assigned to StringValue ). |
string StringSuffix |
Gets or sets an optional suffix used by RefreshStringValue in formatting numeric values, and recognized as valid and stripped out of input strings (assigned to StringValue ). |
string StringValue |
Gets or sets the string representation of a numeric value. May be bound to an external string property when the DecimalStringBinder object is constructed. |
string UndecoratedStringValue |
Gets the current StringValue with StringPrefix and StringSuffix stripped off. |
decimal DecimalValue |
Gets or sets a numeric value. May be bound to an external numeric property of one of the following types: decimal , double , long or int .... The binding must be created via the DecimalStringBinder constructor. |
bool IsValid |
Gets a value indicating whether the current value of StringValue is a valid representation of a numeric value.... |
object DecimalPropertyParentObject |
Gets the object (if any) that contains the numeric property (if any) to which the DecimalStringBinder object's DecimalValue property is bound. |
PropertyInfo DecimalProperty |
Gets reflection info about the numeric property (if any) to which the DecimalStringBinder object's DecimalValue property is bound. |
object StringPropertyParentObject |
Gets the object (if any) that contains the string property (if any) to which the DecimalStringBinder object's StringValue property is bound. |
PropertyInfo StringProperty |
Gets reflection info about the string property (if any) to which the DecimalStringBinder object's StringValue property is bound. |
Events
DecimalStringBinder
implements the INotifyPropertyChanged
interface, and therefore exposes and raises the PropertyChanged
event when any of the properties listed above is changed.
Methods
Additionally, DecimalStringBinder
provides a single public
method:
void RefreshStringValue |
Sets the StringValue property to the canonical representation of the DecimalValue property's value. |
More extensive and detailed documentation is available in the source code and via Intellisense in Visual Studio. From the list and brief descriptions of features above, I hope the reader can begin to get a sense of what the class attempts to do and the ease with which its behavior can be parameterized via properties, as well as how useful it can be in a wide range of scenarios. DecimalStringBinder
provides a pretty comprehensive set of features to support either one-way binding (possibly with special formatting) or two-way binding.
While perusing the downloadable code accompanying this article, one also will find some useful methods and the small, but interesting, auxiliary class NeverNullString
, which I hope are sufficiently self-explanatory.
The code includes two projects:
- A "sandbox" console application that facilitates exercising the features of
DecimalStringBinder
in a very controlled setting, and discovering or verifying the results of various property settings.
- A Silverlight application that shows how
DecimalStringBinder
objects might be used in a realistic application scenario.
An immediately hands-on, experimental approach to the console application is encouraged. Problems and solutions exposed by the Silverlight application will be discussed below.
Points of Interest
Occasionally, numeric values may be related by a formula, so that changing one value causes one or more other values to change. This kind of situation can give rise to some interesting problems. Imagine a situation in which a user can input either of a pair of currency values related by a formula - one value representing the price of an item having a weight expressed in kilograms or pounds, and the other representing price per unit of weight. When the user modifies one of the values, the other is calculated automatically from the value entered by the user. The formulas by which the currency values are related can be expressed as:
- Price per unit = price per item / units per item, rounded up to 4 decimal places.
- Price per item = price per unit * units per item, rounded down to 2 decimal places.
Let's also assume that a change to one of the items on the righthand side of a formula automatically invokes a recalculation of the item on the lefthand side. This specification seems logical, but certain implementations give rise to a small difficulty.
The problem arises because it's not possible to represent numeric values with an infinite number of decimal places, and the problem becomes especially obvious when we modify values by rounding them to just a few decimal places. Currency values, for example, are often rounded to two decimal places.
In the example mentioned above, if the user enters price per item, price per unit is recalculated - and since price per unit is thereby changed, price per item can be recalculated in response, very possibly producing a value that differs from the one just input by the user. Usually, such "second-guessing the user" behavior is not appropriate.
Also, in the absence of specific intervention to make things work differently, the updating of a bound string from TextBox.Text
happens (in Silverlight, at least) when the TextBox
loses focus. If the value input by the user gets changed when the TextBox
loses focus, this is probably a bad thing.
Maybe we would prefer that the user's input be reflected in recalculations as the user is modifying numeric data, character-by-character. If we strive toward that goal, however, the problem just mentioned becomes intolerably worse, because the user's input may be changed by the application while the user is typing!
We can solve both problems by adding a handler for a TextBox
's KeyUp
event (TextChanged
would also work for this purpose), and whenever any change has occurred, explicitly and immediately binding from TextBox.Text
, causing changes to the numeric data and re-running the appropriate calculations based on which value the user has changed. The code below, found in MainView.xaml.cs, accomplishes this. The ViewModel
properties having names suffixed with "Dsb
" are DecimalStringBinder
objects that are bound to the TextBox.Text
properties via binding statements in the View's XAML.
private void TextBox_KeyUp(object sender, TextChangedEventArgs e)
{
if (e.Key == Key.Tab && sender is TextBox)
{
...
}
else
{
TextBox sendingTextBox = (TextBox)sender;
switch (sendingTextBox.Name)
{
case "WeightTextBox":
_viewModel.WeightDsb.StringValue = sendingTextBox.Text;
_viewModel.CalculateTotalPrice();
break;
case "UnitPriceTextBox":
_viewModel.UnitPriceDsb.StringValue = sendingTextBox.Text;
_viewModel.CalculateTotalPrice();
break;
case "TotalPriceTextBox":
_viewModel.TotalPriceDsb.StringValue = sendingTextBox.Text;
_viewModel.CalculateUnitPrice();
break;
}
}
}
Another nuance of the UI interaction worth mentioning is that the user's input format is "cleaned up" to conform to the canonical display format via truncation or extension, if necessary, when a TextBox
loses focus. This is accomplished by handling the LostFocus
event, as shown here:
private void TextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
{
TextBox sendingTextBox = (TextBox)sender;
switch (sendingTextBox.Name)
{
case "WeightTextBox":
_viewModel.WeightDsb.RefreshStringValue();
break;
case "UnitPriceTextBox":
_viewModel.UnitPriceDsb.RefreshStringValue();
break;
case "TotalPriceTextBox":
_viewModel.TotalPriceDsb.RefreshStringValue();
break;
}
}
The best way to assess the usability of the combination of features described above is to run and play with the Silverlight demo app. My colleagues and I have found that business users enjoy interacting with a UI that includes the features provided, and we haven't needed to purchase a special third-party solution to the problems of binding numeric data to user-modifiable string
s, such as TextBox.Text
.
History
- 25th April, 2010: Initial version