Click here to Skip to main content
15,868,016 members
Articles / Desktop Programming / WPF

Making a Parsable Text box in .NET 3.5

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
2 Aug 2009GPL35 min read 26.6K   200   18   2
A parsable text box validates and parses text inside it to strongly typed .NET objects

Introduction

The standard WPF controls provided by the .NET 3.5 SP1 framework do not contain a range of controls like numerical text boxes. Very often developers need these text boxes to limit the user input to accept integers or floating point, etc. I developed these controls while working on a hobby project of mine. While at it, I thought it would be nice to extend these controls to beyond numerical data types and in general, parse any text into strongly typed .NET objects. This article describes how to extend the library to target any .NET data type that you want to create, starting from plain text input.

Note: Unless and until mentioned otherwise, we will be referring to input that has to be parsed as a System.Double object. We will call its text counterpart as floating point representation.

Background

The objective is to get textboxes that accept a text input that can be parsed into a strongly typed .NET object. Now, most people will enter text character by character (possibly press back spaces, or cursor keys and edit/remove previously entered characters or add new characters at any location) and so it is not sufficient to intercept the TextInput event and check if Double.TryParse returns true or false. That said, we also want to prevent users from entering characters that once entered will never allow the input to become a valid floating point number even if any characters are appended to their input. Also, we will like to notify them that they need to append characters that will make their input valid.

For example, in the following screenshot, the top row shows examples of input that can become valid (bottom row) after some specific characters are appended to the input.

MyControls

Some input can never be made valid, meaning no character combination can be appended to rectify it. Example, -1.2. -1.. -- 12p 12e-1. etc. We shall not allow the text box to reach such a state.

Since the main purpose of this article is to demonstrate how to extend this control, let's review its design in some detail. If I were to tell you to write a program to accept a string input character by character and determine if the input you have received so far constitutes a valid floating point number, chances are that you will start thinking in terms of a finite automata. Meaning you will define some states and jump from one state to other depending on some pre-defined (i.e., design time) rules. Some of these states would be final --meaning that if you are on it the input is valid, else it is invalid. While there is nothing wrong with this approach and you can build correct programs using this approach, the code will look a bit messy. Furthermore, if you were to extend this code to accept variations in the float representations (like accepting the thousand separator or the exponential part then you will have to add new states, new jumps, etc.

Now, let's utilize something known as a regular expression. Regular expressions are equivalent to a finite automata --meaning for every finite automata, there exists a regular expression (on the same set of alphabets) and vice versa. So, if we can define a regular expression for a valid floating point pattern, all we need is to find out if the input matches the regular expression. To do so, you will need to again write an automata code and the whole point will be defeated. But, some good people have already written code to match any text against any regular expression and it is available under the System.Text.RegularExpressions namespace in .NET.

So first and foremost, we need two regular expressions: one for patterns that are valid and one of the patterns for which a character combination exists, that when appended to it shall make it valid. For System.Double objects, these can be:

^[-+]?(\d+,)*\d+(\.?\d+)?([eE]?[-+]?\d+)?$
^[-+]?((\d+,)*(\d+\.?\d*)?)?([eE]([-+]?\d*)?)?$

respectively. Note that we have deliberately kept the patterns more restrictive than what Double.TryParse can tolerate. That decision is purely aesthetic. You can make it as lenient as you want, as long as you know how to make System.Double objects out of it.

Now, we also need to somehow disallow patterns other than the ones discussed above. So, we will handle the OnPreviewTextInput event of TextBox class and save the currently inputted text in it. After the text has been entered (i.e., the TextInput event), we can see if it does not match the above patterns and restore that text. You can prove by induction that we shall never save non conformant inputs by this approach.

To provide visual hints, we declare two properties (auto-properties to be precise):

C#
/// <summary>
/// Gets/Sets the brush to indicate that the current input needs
/// characters to be appended to it so that it becomes valid.
/// </summary>
public Brush InvalidInputBrush { get; set; }

/// <summary>
/// Gets/Sets the brush to indicate that the control holds valid input text
/// in it.
/// </summary>
public Brush ValidInputBrush { get; set; }

protected override void OnPreviewTextInput(
    System.Windows.Input.TextCompositionEventArgs e)
{
    //We always keep text that is ok, so save the last ok text
    m_oldText = this.Text;
    base.OnPreviewTextInput(e);
}

protected override void OnTextChanged(TextChangedEventArgs e)
{
    base.OnTextChanged(e);
    /*
     * Empty text is ok.
     * Text that is valid is obviously valid.
     * Text that can become valid later by "appending" characters is a valid input
     * but it won't update the this.Value. We can also give a hint to the user.
     */
    if (String.IsNullOrEmpty(this.Text))
    {
    	m_value = default(T);
    	this.Background = this.ValidInputBrush;
    }
    else if (this.ValidT.IsMatch(this.Text))
    {
	T t;
	//We can still fail, like overflow exceptions, etc. so double check    
	if (this.TryParse(this.Text, out t))
		m_value = t;
	else
		this.RestoreOldText();

	this.Background = this.ValidInputBrush;
    }
    else if (this.PotentialT.IsMatch(this.Text))
    {
        this.Background = this.InvalidInputBrush;
    }
    else
    {
        //In other words, cancel the effect of this text input
        this.RestoreOldText();
    }
}

private void RestoreOldText()
{
    this.Text = m_oldText;
    //The following code ensures that the cursor is always at the end
    this.SelectionStart = this.Text.Length;
    this.SelectionLength = 0;
}

private bool TryParse(string text_, out double d)
{
    return Double.TryParse(text_, out d);
}

Now, to make it extendable, let's put all this code in an abstract class Parsable<T> : TextBox that exposes the following contract (our brushes above are part of this contract):

C#
/// <summary>
/// Gets/Sets the value corresponding to the input.
/// </summary>
public T Value { get; set; }

/// <summary>
/// Gets a value indicating if the current input is valid.
/// </summary>
public bool IsValid { get; private set; }

/// <summary>
/// Gets/Sets the brush to indicate that the current input needs
/// characters to be appended to it so that it becomes valid.
/// </summary>
public Brush InvalidInputBrush { get; set; }

/// <summary>
/// Gets/Sets the brush to indicate that the control holds valid input text
/// in it.
/// </summary>
public Brush ValidInputBrush { get; set; }

The abstract methods are declared below:

C#
/// <summary>
/// Parses text to an object of type <typeparamref name="T"/> and
/// returns a value indicating if the text was successfully parsed.
/// </summary>
/// <param name="text_">The text to parse.</param>
/// <param name="t_">The object in which to hold the parsed object,
/// to be passed uninitialized.</param>
/// <returns>True if the text could be parsed as <typeparamref name="T"/>;
/// false otherwise.</returns>
protected abstract bool TryParse(string text_, out T t_);

/// <summary>
/// Gets the text that when parsed by <see cref="TryParse"/> will return 
/// an object equal to <paramref name="t_"/>.
/// </summary>
/// <param name="t_">The object.</param>
/// <returns>The text that when parsed by <see cref="TryParse"/> will return 
/// an object equal to <paramref name="t_"/>.</returns>
protected abstract string ToText(T t_);

/// <summary>
/// Gets the regular expression for all text for which a certain character
/// combination exists that when appended to it will make it a valid input.
/// </summary>
protected abstract Regex PotentialT { get; }

/// <summary>
/// Gets the regular expression for valid input text.
/// </summary>
protected abstract Regex ValidT { get; }

You will notice that there is an abstract method named ToText. This method is used to set the text when this.Value is assigned to.

So, to extend this, all you need to do is to implement these methods. Three examples of these are in the attached source code.  As an exercise, try with Social Security Numbers. (If you have never designed a class for SSNs and have simply passed them as strings, then this should be another lesson in writing good code. If string objects have a special significance, then they should have a class of their own. This greatly simplifies your code architecture.)

Using the Code

To use this control in a WPF form, do this:

XML
<my:LongIntegerTextBox Grid.Column="1" Grid.Row="1"
    HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5,5,5,5"
    Width="100" Height="30" InvalidInputBrush="LightPink" ValidInputBrush="White"/>

The property this.Value will give you the most recent valid input by the user.

History

  • 2nd August 2009: Version 1.0 upload

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


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

Comments and Discussions

 
GeneralNot necessary to create a new control Pin
Verbiest Kristof4-Aug-09 23:31
Verbiest Kristof4-Aug-09 23:31 
GeneralUse NumberFormatInfo Pin
zlezj2-Aug-09 9:17
zlezj2-Aug-09 9:17 

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

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