Textbox controls are often used to enter dates, times, currency values or preformatted numeric data such as phone numbers. A control that allows its user to only enter a specific type of data is useful for several reasons:
- The user may do less typing if the control automatically fills in characters such as dashes or slashes.
- The user gets immediate feedback if he/she enters an invalid character.
- The entered data is much more likely to be valid when the user "submits" it.
The classes presented in this article provide these benefits for the most popular types of data that may be entered: dates, times, decimals, integers, currency amounts, formatted numerics and restricted alphanumerics.
This is my first real project using C#/.NET, having come from C++/MFC and Java. I decided that a great way to learn was to take something I'm already familiar with and convert it to C#. This would allow me to learn the new technology as well as to make note of the differences. This article is the result of converting my Validating Edit Controls code, originally written in C++/MFC. The effort took around a month to complete and was a terrific learning experience. Enjoy!
There are two groups of classes, the behavior classes and the textbox classes, both of which are contained in the
AMS.TextBox namespace. The behavior classes are designed to alter the standard behavior of textboxes so that the user can only enter a specific type of text into the control. For example, the
DateBehavior only allows a date value to be entered into the textbox associated with it. The rest of the classes are simple
TextBox-derived controls containing specific behaviors. For example, the
DateTextBox control has the
DateBehavior field inside of it and a property used to retrieve it.
Here's a listing of all the classes. If you need specific documentation on the available methods and properties, you may view the AMS.TextBox.chm help file, or just use the editor's Intellisense.
Base class for all behavior classes. It has some basic functionality.
Prohibits input of one or more characters and restricts length.
Used to input a decimal number with a maximum number of digits before and/or after the decimal point.
Only allows a whole number to be entered.
Inserts a monetary symbol in front of the value and a separator for the thousands.
Allows input of a date in the mm/dd/yyyy or dd/mm/yyyy format, depending on the locale.
Allows input of a time with or without seconds and in 12 or 24 hour format, depending on the locale.
|Allows input of a date and time by combining the above two classes.|
Takes a mask containing '#' symbols, each one corresponding to a digit. Any characters between the #s are automatically inserted as the user types. It may be customized to accept additional symbols.
Supports the Alphanumeric behavior.
Supports the Numeric behavior.
Supports the Integer behavior.
Supports the Currency behavior.
Supports the Date behavior.
Supports the Time behavior.
Supports the DateTime behavior.
Supports the Masked behavior.
Takes a mask and adopts its behavior to any of the above classes.
I've built all these classes into a DLL so that the
TextBox-derived classes may be used inside the Visual Studio .NET IDE as custom controls. Here are the steps required for adding them to your project and using them in your code:
- Open your own project inside Visual Studio .NET.
- Open the file containing the form you wish to add the control(s) to.
- Right click on the Toolbox and select "Customize Toolbox".
- Click on the .NET Components tab.
- Click the Browse button and select the AMS.TextBox.dll file.
- The 9 controls will be added to the Customize toolbox and will be check marked. Click on the Namespace column to sort by it, so you can see them more easily.
- If you wish, remove the checkmark from the ones you're not going to use.
- Click OK. The controls will then appear on the toolbox.
- Drag and drop them to your form and use them like regular
- The behavior-related properties will appear under the Behavior category in the Properties toolbox of the form designer.
Points of Interest
While porting these classes from C++, I came upon several important differences in how things are done. I decided to make note of these differences for future reference and added them here for anyone who may work on a similar task. If you want to read on, I recommend you first become familiar with the C++ classes.
In the .NET world, Edit boxes are called
TextBoxes (and Text controls are called
Labels). So I knew that to keep things as .NETish as possible I had to rename my classes. I replaced the "Edit" suffix with "TextBox" and I put everything into a namespace called
AMS.TextBox to keep the names as simple as possible.
No Hungarian Notation
For years I've used my own variation of Hungarian notation in C++ code. I thought it was a great way to ease the pain of maintenance.
Then I started writing Java code and I had to conform to the standards set by my group: no Hungarian notation. So I stopped using it, at least for Java. And to my surprise after about a month, I wasn't missing it! It was actually liberating to write variables without the extra baggage of the type prefix. It also made the code easier to read. I came to the realization that in well written, modular code it's rarely necessary to make each variable's type evident within the name itself. It's just overkill and it makes the name larger and more cluttered.
So I gave it up for good in Java, while I kept it in my existing C++ code for the sake of consistency. I had even replaced the
m_ prefix (for member variables) with the
Then came C# and this project. I knew that Microsoft's convention had been to drop Hungarian notation for .NET, so I happily continued on as I had with Java. But I faced a small problem. I wanted to make the code CLS compliant so that it could be used and extended by any .NET language. This caused a problem for protected variables with names like "separator" and then a corresponding property called "Separator". Since the names were the same except for the casing, the code was not CLS compliant (for case-insensitive languages). So I decided to switch from using
this. for member variables back to the old
convention, which I had always liked.
And that's the notation I use in this project. No Hungarian notation except for the
m_ for all member variables (fields).
No Multiple Inheritance
When I originally wrote my C++ classes, I made the decision to split the
CEdit classes from their behaviors - I believe this follows the Bridge pattern. This strategy gave me the flexibility of being able to plug the behaviors into multiple
CEdit-derived classes as needed, which worked great for the
CAMSMultiMaskedEdit class. In addition, I used C++ multiple-inheritance feature to conveniently inherit the classes from
CEdit and their respective behavior(s) simultaneously.
As we all know, .NET does not support multiple implementation inheritance, so I had to come up with an alternative. I initially decided to forgo the idea of the
TextBox-derived classes having the methods and properties of their respective behaviors, which multiple inheritance so conveniently allowed me. Instead I added a read-only property to each class called
Behavior which returns the
Behavior-derived object currently associated with the
TextBox object. So any behavior-specific action would be taken via this property.
This approach made life much easier for me, the library developer, but not for anyone using the library. Whereas in C++ you could do this:
CAMSDateTimeEdit dt ...
int day = dt.GetDay();
In C# now you would need to do this:
DateTimeTextBox dt ...
int day = dt.Behavior.Day;
I went with this design for some time until I came to the conclusion that it just wasn't right. It was a step backward, and all because I didn't want to spend extra time wrapping the
Behavior's public members in the
TextBox-derived classes. So I bit the bullet and did it; I went through each
TextBox class and added its corresponding
Behavior's public methods and properties as members of the class. This essentially turned the
TextBox classes as wrappers of their
Behavior classes. It was a lot of extra work (caused by the lack of MI), but I think it was worth it. Now the
TextBox classes are similar to their C++ counterparts:
DateTimeTextBox dt ...
int day = dt.Day;
This is not only more intuitive but also makes the
TextBox classes more friendly for the form designer.
For the C++ classes, I decided to make the
CAMSEdit::Behavior class work only with classes derived from
CAMSEdit. This definitely made life easier, since the
CAMSEdit class was mine and I could enhance it with whatever methods were needed by the
Behavior classes (i.e.
IsReadOnly). But the problem was that this created a tight coupling between the
Behavior classes and
CAMSEdit, which any new class would have to account for.
For C#, I changed the
Behavior classes to be much more independent. Now they work with any classes derived from
System.Windows.Forms.TextBoxBase. This gives them the flexibility to be associated with just about any
TextBox class and not just the ones derived some class of mine.
Additionally, I made it very easy to associate
Behavior classes to textboxes. You just instantiate the behavior class and pass the textbox object in the constructor. Here's an example:
MyTrustyTextBox textbox = new MyTrustyTextBox();
TimeBehavior behavior = new TimeBehavior(textbox);
That's it! From that point on, the textbox behaves according to the rules of that behavior. This is in sharp contrast to C++ where not only did the class need to be derived from
CAMSEdit, but you also needed to forward several message handlers to the associated
Behavior, as explained next.
If you look at the C++ code, you'll notice that the
Behavior classes rely on their associated
CAMSEdit object to forward the relevant messages to them (
_OnKeyDown, etc.). Well, thanks to delegates I didn't need to do that in C#. All I had to do was make the
Behavior class add event handlers to the textbox object that would call methods in the
Behavior class. And since these methods are declared virtual in the
Behavior class, then all the derived classes needed to do was, override them to provide their own functionality. This is a more elegant approach that in C++ would have ended up looking more like a hack if I had decided to implement it.
In addition, while in C++ I handled the messages directly (i.e.,
WM_KEYDOWN, etc.), .NET does not provide direct handlers for some of these messages. The only way to do it, as far as I could see, was to override
WndProc inside the textbox classes themselves and trap the messages there inside a
Instead, I decided to try the available event handlers to see if they could do the job. So I used the
KeyPress event for
WM_KILLFOCUS. They worked just fine and allowed the
Behavior classes do all the work, as described above.
Behavior classes are all about validations - basically ensuring that the user enters the proper data into the textbox. Some validations are performed as the user types while others happen when the user leaves the control.
As you may know, the
System.Windows.Forms.Control class contains properties and events designed to help the developer validate the control's value when control loses focus. I decided to take advantage of this built-in mechanism and move a lot of the functionality in the old
OnKillFocus handlers to a
Validating event hander I added to the
Behavior class. This handler not only validates the data, but also gives error feedback to the user via a message beep, message box, or a small icon (
ErrorProvider). It can even be configured to automatically set the control to a valid value if necessary.
This is all accomplished by modifying the
Flags property and setting the corresponding
ValidationFlag value(s). Here's an example of how to make a beeping sound and show an icon if the control's value is empty or not valid when the
Validating event is triggered:
DateTimeTextBox dt ...
| (int)ValidatingFlag.Beep, true);
Of course, this also requires that the textbox's
CausesValidation property is set to
true, which by default it is. As an alternative, you may invoke all this functionality yourself via the textbox's
Validate method. It is called by the
Validating event handler to set the
First of all, I just have to say that I love properties! They're a welcome addition to C# (and they should have been part of Java). When converting these classes, a lot of methods became excellent candidates for properties. So I happily went and converted all of them.
Then I took a second look and reconsidered what I had done. I found that while a lot of methods were undeniably property-material, others were a bit more questionable. The most prominent one was
Behavior.GetValidText. This one initially appeared like another method worthy of becoming a property with a getter. However, I later decided that properties should be treated by the programmer as convenient ways of quickly reading attributes of an object. If you look at the code for most
GetValidText implementations, there is quite a bit of processing going on in there, much more than the typical
return someField; which you find in most property getters. In other words, the "valid text" is not really a property of the behavior. It needs to be deciphered every time the method is called. So leaving it as a method does not give the impression that it's readily available and quickly retrieved.
This was the criteria I used when deciding which methods to turn into properties. If the property's getter had a simple implementation and the property itself made sense for the class, then I converted it; otherwise I left it as a method.
After I had finished porting the classes, I decided it would be nice to document the code. The C++ code already had comments on the top of every method, so that gave me a head start. However I decided to explore the XML Documentation tags to see what additional benefits I could gain.
My first impression is that they were very verbose. Most of them require an opening and closing tag, which if written on separate lines can make each section take at least 3 lines! For methods that take multiple parameters that would mean an extra 3 lines of comment per parameter. That's a lot of space taken up in comments!
The benefits were another story. You spend some time up-front putting up with all the verbosity, but the end result is nicely formatted online help. This would also mean that I wouldn't have to spend extra time documenting every method within this article, like I did for the C++ one. You just add the DLL and its XML file to your project - Visual Studio takes care of the IntelliSense for you automatically!
So I did it! I manually documented every method, property and field in the classes, even the private ones, right in the code. To cut down on the waste of space produced by the opening and closing tags, I decided to only put the opening tags on lines by themselves. Closing tags would simply go as part of the last line of the section. I also added a couple of spaces for indentation to the contents to make them easier to read. Here's an example:
There's a tool called NDoc that generates help files from source code, in a variety of formats. I used it to generate an MSDN-style help file, AMS.TextBox.chm, which I've included in the download. Enjoy!
I'd like to thank Gerd Klevesaat for helping me understand the complexities of dealing with controls having sub-properties. I wanted to give users of my textbox controls, the ability to directly manipulate the various properties of the
Behavior property, right from the form designer. After many trials and tribulations, I decided not to implement such functionality since it doesn't work as it should, but it was fine since I ended up wrapping most of the behavior public methods and properties inside the textbox classes.
- Version 1.0 - Sep 15, 2003
- Version 1.1 - Nov 7, 2003
- Fixed a problem related to the form designer, which was generating code for properties that I had marked with the
Browsable(false) attribute, such as
Year. The designer would set these to 0 and at run time they would raise exceptions since 0 isn't a valid value. To prevent this problem, I used the
ControlDesigner class to manually remove those properties at design time. Thanks to Spiros Prantalos for reporting this problem.
- Fixed a bug in the
NumericBehavior class reported by Wojtek Swieboda (see below).
- Added a description for the
AMS.TextBox namespace to the help file (AMS.TextBox.chm).
- Version 1.2 - Nov 26, 2003
- Added a couple of
NumericBehavior.LostFocusFlag values to allow the
LostFocus handler to be called when the
Text property is set or the text changes.
- Added the new
CallHandlerWhenTextPropertyIsSet flag to
CurrencyBehavior so that when the
Text property is set, the value is automatically appended with ".00" (if necessary). Thanks to Yan Khai Ng for bringing it to my attention.
- Version 1.3 - Jan 9, 2004
- Changed the
Second getters to be less strict about the expected format. This allows the
Text property of the Date, Time, and DateTime controls to be properly bound to database columns. Thanks to Terry Carroll for reporting the problem.
- Fixed a bug in the Alphanumeric behavior that wasn't removing invalid characters as expected.
- Version 2.0 - Jan 30, 2004
- Added extra event handlers to intercept when objects are added to the
DataBindings collection of the control. This fixes problems with Date, Time, DateTime, and Numeric controls bound to nullable database columns. It also fixes problems with the Currency textbox which was not being properly converted to a
Decimal when bound. Thanks to Terry Carroll, ACanadian, and MattH for reporting the problem.
- Added a check to the ReadOnly property to prevent the Up/Down arrows from changing the Date or Time controls when read-only. Thanks to Terry Carroll for reporting this problem.
- Changed the default range of values for the Integer textbox to the min and max 32-bit values to prevent a crash in the VS IDE. Thanks to mdenzin for reporting the problem and suggesting this fix.
- Fixed a bug that was causing validations to fail when the control was empty. Thanks to marcelloz for helping me notice the bug.
- Added a static
ErrorCaption property to the
Behavior class to be used in error message boxes for failed validations. Thanks to marcelloz for giving me the idea.