Title: Preview TextBox changes in C#
One of the more annoying omissions from the Windows Forms controls is a way to preview changes to a TextBox before they occur. For example, suppose you want the user to enter a floating point value in a TextBox. The TextBox class has a TextChanged event that lets you decide whether the user has entered something valid, but at that point it's too late to stop the user from entering gibberish. You can try catching the KeyPress event and deciding whether the new key should be allowed, but that doesn't catch all of the ways the user might change the TextBox control's value. Those ways include:
- Typing a normal character such as X or 7
- Pressing Delete to delete the selected text or the character after the insertion point if nothing is selected
- Pressing Backspace to delete the selected text or the character before the insertion point if nothing is selected
- Pressing Ctrl+X to cut the selected text into the clipboard
- Pressing Ctrl+V to paste whatever is in the clipboard
- Pressing Ctrl+Z to undo the most recent change
- Pressing Shift+Delete to cut the selected text into the clipboard
- Pressing Shift+Insert to paste whatever is in the clipboard
- Right-clicking and selecting Undo from the context menu
- Right-clicking and selecting Cut from the context menu
- Right-clicking and selecting Paste from the context menu
- Right-clicking and selecting Delete from the context menu
I don't know why Microsoft doesn't simply add a TextChanging event to the TextBox control that lets you preview and cancel changes to the text, but this has been a problem with the TextBox control since before .NET was invented so it's unlikely to change any time soon. I've seen lots of posts on the Internet that try to deal with this problem, but the ones I've seen all miss one or more of the cases listed above. (Most forget about Ctrl+V and using the context menu.)
Here's my attempt at solving this problem. This example works explicitly with the txtInteger TextBox.
This post only deals with keyboard events that change the TextBox control's value. My next post will explain how to handle the context menu, a much simpler topic.
The idea is to catch the events where changes to the text might occur, determine what value the TextBox will hold if the change is allowed, and then either accept of cancel the change.
One of the key methods used by this example is GetNewTextBoxValue. This method, which determines what value the TextBox will hold if a particular change is allowed, is shown in the following code.
// Possible change types for the TextBox's value.
private enum EditType
{
NewCharacter,
Cut,
Paste,
Delete,
Backspace,
}
// Return the value that the TextBox will have if this change is allowed.
private string GetNewTextBoxValue(TextBox txt, EditType edit_type,
char new_char, out int selection_start)
{
// Get the pieces of the current text.
selection_start = txt.SelectionStart;
string current_text = txt.Text;
string left_text = current_text.Substring(0, selection_start);
string selected_text = txt.SelectedText;
string right_text =
current_text.Substring(selection_start + txt.SelectionLength);
// Compose the result.
string result = "";
switch (edit_type)
{
case EditType.NewCharacter:
result = left_text + new_char + right_text;
selection_start++;
break;
case EditType.Cut:
result = left_text + right_text;
if (txt.SelectionLength > 0)
{
Clipboard.Clear();
Clipboard.SetText(selected_text);
}
break;
case EditType.Paste:
if (Clipboard.ContainsText())
{
selected_text = Clipboard.GetText();
selection_start += selected_text.Length;
}
result = left_text + selected_text + right_text;
break;
case EditType.Delete:
if (selected_text.Length == 0)
{
// Remove the following character.
if (right_text.Length > 0)
right_text = right_text.Substring(1);
}
else
{
// Remove the selected text.
selected_text = "";
}
result = left_text + selected_text + right_text;
break;
case EditType.Backspace:
if (selected_text.Length == 0)
{
// Remove the previous character.
if (left_text.Length > 0)
{
left_text = left_text.Substring(0, selection_start - 1);
selection_start--;
}
}
else
{
// Remove the selected text.
selected_text = "";
}
result = left_text + selected_text + right_text;
break;
}
// Return the result.
return result;
}
The EditType enumeration lists the possible types of changes that the TextBox control's value might experience.
The GetNewTextBoxValue method takes as parameters the TextBox, the type of change being considered, a new character to add to the text (if the user pressed a normal key such as H or 3), and an integer where the method can indicate the TextBox control's new SelectionStart value if the change is allowed. The method returns the TextBox control's new value.
The method starts by breaking the current text into three pieces: the text to the left of the current selection, the current selection, and the text to the right of the current selection.
The code then uses a switch statement to compose the result that will occur for the different EditType values. You can walk through the code (and possibly experiment with a TextBox at run time) to verify that the code builds the correct result.
Notice that the Cut code copies the selected text to the clipboard. This happens even if the resulting TextBox value is invalid so the change isn't allowed. I figured the user would want the text copied to the clipboard even if you couldn't legally remove the selected text. You can change this if you like.
Notice also that the behavior of the Delete and Backspace changes depends on whether the TextBox currently has any text selected.
After building the result string, the method returns it.
The ShouldCancelTextBoxEvent method shown in the following code calls GetNewTextBoxValue and then decides whether the event that started the change should be canceled.
// Compose the TextBox's new value and call ValueIsValid
// to see if it is valid.
//
// If the change is invalid, beep and return true to indicate
// the event should be canceled.
//
// If the change is valid and assign_value is true, assign the
// value to the TextBox and then return true to indicate that
// the event is no longer needed.
//
// If the change is valid and assign_value is false, then return
// false to indicate that the event still needs to be handled.
private bool ShouldCancelTextBoxEvent(
TextBox txt, EditType edit_type, char new_char, bool assign_value)
{
// Get the new value.
int selection_start;
string new_value =
GetNewTextBoxValue(txt, edit_type, new_char, out selection_start);
// See if it's a valid value.
if (ValueIsValid(new_value))
{
// It's okay. Accept it.
if (assign_value)
{
txt.Text = new_value;
txt.Select(selection_start, 0);
return true;
}
return false;
}
else
{
// The new value is invalid. Complain.
System.Media.SystemSounds.Beep.Play();
return true;
}
}
This code starts by calling GetNewTextBoxValue. It then calls ValueIsValid (described shortly) to see if the new value should be allowed. I moved all of the testing logic into ValueIsValid so you can modify it to handle other data types such as floats or some specialized format such as phone numbers. (The ValueIsValid method is basically the equivalent of the missing TextChanging event.)
The method's assign_value parameter indicates whether ShouldCancelTextBoxEvent should set the TextBox control's value. (You'll understand how this is used when you read about the difference between the user pressing a key such as Ctrl+V and using the context menu to paste text.)
There are three possibilities:
- ValueIsValid returns true and assign_value is true. In this case the code sets the TextBox control's value directly and returns true to indicate that the event that started the change should be discarded with no further action. Changes made in this way do not become part of the TextBox control's Undo buffer so they cannot be undone.
- ValueIsValid returns true and assign_value is false. In this case the code returns false to indicate that the event that started the change should be allowed to proceed and update the TextBox control's value. Changes made in this way become part of the TextBox control's Undo buffer so they can be undone.
- ValueIsValid returns false. In this case the method returns true to indicate that the change is invalid and the event that started it should be discarded.
The following code shows the ValueIsValid method that decides whether the change should be allowed.
// Return true if the value is a valid integer.
private bool ValueIsValid(string text_value)
{
// Do not allow spaces.
if (text_value.Contains(' ')) return false;
// Allow a blank string, -, and +.
if ((text_value.Length == 0) ||
(text_value == "-") ||
(text_value == "+")) return true;
// See if the text parses.
int test_value;
return int.TryParse(text_value, out test_value);
}
This version of ValueIsValid tries to allow only integers. First the code disallows any value that contains a space. Although a value such as " 17 " can be parsed as an integer, it's not really the intent to let the user enter values that contain spaces. This also means the code doesn't need to worry about values such as " -" that aren't integers but might be the start of an integer.
Next the method allows a blank value, a -, or a + because all of those might occur when the user is starting to enter an integer.
If the value passes those tests, the code simply uses int.TryParse to attempt to convert it into an int. It returns true if TryParse returns true, indicating that the value can be parsed as an integer.
That's the end of the code that evaluates values and decides whether they make sense. The rest of the code shown here consists of the event handlers that use that code.
When the user presses a key, the following KeyPress event handler executes to see if that key should be allowed.
// Handle normal characters.
private void txtInteger_KeyPress(object sender, KeyPressEventArgs e)
{
// Do nothing for special characters.
if ((e.KeyChar < ' ') || (e.KeyChar > '~')) return;
// See if the change is valid.
e.Handled = ShouldCancelTextBoxEvent(
txtInteger, EditType.NewCharacter, e.KeyChar, false);
}
If the key isn't a normal visible key, the event handler just returns, allowing the key to be processed normally. Special keys such as Ctrl+X and Backspace are processed by the KeyDown event handler described shortly.
For visible keys, the code calls ShouldCancelTextBoxEvent to see if the new value will be valid. If ShouldCancelTextBoxEvent returns true, the KeyPress event handler flags the event as handled so it does not continue and it is ignored by the TextBox.
Notice that the final parameter passed to ShouldCancelTextBoxEvent is false, indicating that it should not assign the new value to the TextBox if it is valid. If the value is valid, ShouldCancelTextBoxEvent returns false and the event handler sets e.Handled to false so the key is processed normally by the TextBox.
The KeyPress event handler deals with normal characters. The program uses the following KeyDown event handler to deal with control sequences such as Ctrl+X and other special keys such as Backspace and Delete.
// Handle Ctrl+A, Ctrl+X, Ctrl+V, Shift+Insert,
// Shift+Delete, Delete, and Backspace.
private void txtInteger_KeyDown(object sender, KeyEventArgs e)
{
// Look for the necessary key combinations.
bool cancel_event;
if (e.Control && (e.KeyCode == Keys.A))
{
// Handle this one just for convenience.
txtInteger.Select(0, txtInteger.Text.Length);
cancel_event = true;
}
else if (e.Control && (e.KeyCode == Keys.X))
{
cancel_event =
ShouldCancelTextBoxEvent(txtInteger,
EditType.Cut, ' ', false);
}
else if (e.Control && (e.KeyCode == Keys.V))
{
cancel_event =
ShouldCancelTextBoxEvent(txtInteger,
EditType.Paste, ' ', false);
}
else if (e.Shift && (e.KeyCode == Keys.Insert))
{
cancel_event = ShouldCancelTextBoxEvent(
txtInteger, EditType.Paste, ' ', false);
}
else if (e.Shift && (e.KeyCode == Keys.Delete))
{
cancel_event = ShouldCancelTextBoxEvent(
txtInteger, EditType.Cut, ' ', false);
}
else if (e.KeyCode == Keys.Delete)
{
cancel_event =
ShouldCancelTextBoxEvent(txtInteger,
EditType.Delete, ' ', false);
}
else if (e.KeyCode == Keys.Back)
{
cancel_event =
ShouldCancelTextBoxEvent(txtInteger,
EditType.Backspace, ' ', false);
}
else
{
// We didn't handle the event.
// Let the event proceed normally.
cancel_event = false;
}
// If we handled it, stop the event.
if (cancel_event)
{
e.Handled = true;
e.SuppressKeyPress = true;
}
}
To look for control sequences, the code checks the e.Control parameter to see if the Ctrl key is pressed and it uses the e.KeyCode parameter to see if a particular key is also pressed. For example, the user is pressing Ctrl+X if e.Control is true and e.KeyCode is Keys.X. Notice that the code doesn't check other special keys such as Shift and Alt so, for example, Ctrl+V and Ctrl+Shift+V work the same way. This is consistent with the way the TextBox normally behaves.
The code explicitly checks for each of the Ctrl+A, Ctrl+X, Ctrl+V, Delete, and Backspace keys. (Looking for Ctrl+A isn't really necessary, but I added it for convenience to let the user select all of the TextBox control's text. If you want to use Ctrl+A for some other purpose, comment out this code.)
In each case the KeyDown event handler calls ShouldCancelTextBoxEvent passing it the appropriate EditType value. The final parameter to ShouldCancelTextBoxEvent is false so that method doesn't set the TextBox control's value.
The ShouldCancelTextBoxEvent method returns true if the change is invalid. In that case, the KeyDown event handler suppresses the key stroke so it is not processed by the TextBox.
Whew! That's all the code this example uses to preview key board events. My next post will explain how to handle the context menu, which is a lot easier.
This example uses code hard wired to the txtInteger TextBox control. Later posts will make this solution easier to apply to other controls.
This is a pretty complicate example. I think I've covered everything but please post a comment below if you find situations that it doesn't handle properly.
Due to some weirdness on the server, I needed to destroy and re-create this post and unfortunately that removed the comments. Here's a summary.
On 10/24/2012 Markus Goelzner wrote:
I've done that for the Delphi VCL long ago and as you mention it, it is likely to miss a case. For example Shift+Insert is a shortcut to paste, Shift+Delete is shortcut to cut. You should process WM_PASTE, WM_CUT and maybe WM_CLEAR.
On 10/25/2012 I replied:
Thanks for mentioning this. I didn't know about Shift+Insert and Shift+Delete. I'll modify the example to cover them. (I wonder if there aren't too many ways to do these things. Do we really need so many?)
Then on 10/26/2012 I added:
I've added code to check for these key sequences. Please let me know if you find others that aren't handled correctly.
Looking for the messages such as WM_CUT and WM_PASTE would probably be better in some ways because it lets you treat these at a higher level. It doesn't really matter whether the user presses Ctrl+X or Shift+Delete to cut. However it would require subclassing and I'm trying to avoid making a new TextBox subclass.
Download the example to experiment with it and to see additional details.
|