.NET 2 the Max
Tap the Power of Inherited WinForms
Mix inheritance and Windows Forms to solve recurring UI-related problems.
Technology Toolbox: VB.NET, C#
Everything is an object in the marvelous world of managed code, from simple entities, such as numbers and strings, to complex UI objects, such as controls and forms. As a matter of fact, a form is simply a class that inherits from a .NET type named System.Windows.Forms.Form. Thanks to inheritance, your form knows automatically how to behave when a user drags its borders to resize it or clicks on the upper-right button to close it. In addition, Visual Studio .NET knows how to display the form at design time, so that you can drop controls on its surface and have VS.NET generate the code that instantiates control objects and set their properties accordingly.
Forms don't need to inherit directly from the System.Windows.Forms.Form type. Instead, they might inherit from another class you've defined in the same executable or in a separate DLL, provided the other class inherits from the Form type, either directly or indirectly. In many cases, inheriting from an intermediate class can offer plenty of advantages, as I'll demonstrate in this month's column.
Use a Base Form for Your Dialog Boxes
Consider the simple task of creating a dialog box with the typical OK and Cancel buttons (see Figure 1). Have you ever considered how many actions you need to create a functional dialog box? You must change the form's FormBorderStyle to FixedDialog, add the two buttons, set their names and captions, possibly anchor them to the right or bottom border (so you don't have to move them if you need to resize the form later), set the form's AcceptButton and CancelButton properties, and finally write the handlers for the buttons' Click events. The entire procedure might take you about one minute, but if you multiply this minute for each and every dialog box you've created in recent years, you'll reckon that's a lot of wasted time and code.
Thanks to form inheritance (otherwise known as visual inheritance, in VS.NET jargon), you can prepare a DialogFormBase class that will act as the base form for all the dialog boxes you'll define in the future. You create such a base form as if you were creating a regular form, except you ensure that the Modifiers property of all controls is set to Protected, instead of the default Friend (VB.NET) or Private (C#) scope. If you omit this step, you won't be able to access these controls and change their properties from inside derived forms.
Next comes the critical part of the entire operation. You can create a useful and reusable base form if you stick to some design principles, even though they force you to write slightly more code than you'd do in a regular form. Specifically, all the events raised by controls in the base form should delegate to a protected and overridable procedure, which in turn performs the default action for that control (see Listing 1). You'll see the benefits to this verbose approach in a moment (download the companion code samples for this column.
Define a login form that inherits from DialogFormBase. Many books and articles suggest you use the Add Inherited Form command from the Project menu to create a derived form, but it's not necessary. In fact, you can create a new form as usual, then switch to the code window and change its base class so it inherits from DialogFormBase rather than the default System.Windows.Forms.Form type. This edit is all you need to make the LoginForm class inherit all its properties and behavior from the DialogFormBase class. You can drop the remaining controls later, as well as move the two buttons that the form inherited from its base class (see Figure 2).
You'd need to implement a robust password validation routine in a real-world app, but let's suppose for our purposes that you simply want to check that the username isn't empty and that the password is a specific value:
Public UserName As String
Protected Overrides Sub OnOkClick( _
e As EventArgs)
' You should use a more robust
' password policy here!
If txtUserName.Text <> "" AndAlso _
txtPassword.Text = "vsm" Then
' Let the caller know who
' logged in
Me.UserName = _
txtUserName.Text
MyBase.OnOkClick(e)
Else
MessageBox.Show("Wrong password")
End If
End Sub
Here's the C# version:
public string UserName;
protected override void OnOkClick(EventArgs e)
{
if ( txtUserName.TextLength != 0 &&
txtPassword.Text == "vsm" )
{
// let the caller know who logged
// in
this.UserName = txtUserName.Text;
base.OnOkClick(e);
}
else
{
MessageBox.Show(
"Wrong password");
}
}
Notice that the code might close the form by setting the DialogResult property right in OnOkClick, but that would be a mistake in object-oriented design terms. It's better to perform the default action by calling the base form's OnOkClick procedure, so that you can add more functionality in the base form later and be sure that all derived forms inherit the new functionality.
Define a Global Forms Collection
Many VB6 developers complain that Windows Forms don't offer a global Forms collection, which is occasionally necessary to check whether a given form has been loaded already or to perform a given action on all the open forms of a running program. I'll show you how you can solve this problem with a few lines of code and improve the design of your application at the same time.
All your forms should inherit from a base form you define separately, so that you can easily add new features in one place and use them in all your forms. Such a base form has no controls at all and contains this code:
Public Shared ReadOnly Forms As New _
ArrayList
Protected Overrides Sub OnLoad(ByVal e _
As EventArgs)
MyBase.OnLoad(e)
Forms.Add(Me)
End Sub
Protected Overrides Sub OnClosed(ByVal _
e As EventArgs)
Forms.Remove(Me)
MyBase.OnClosed(e)
End Sub
Here's the C# version:
public static ArrayList Forms = new
ArrayList();
protected override void OnLoad(EventArgs
e)
{
base.OnLoad(e);
Forms.Add(this);
}
protected override void
OnClosed(EventArgs e)
{
Forms.Remove(this);
base.OnClosed (e);
}
You can use the BaseForm.Forms collection to iterate over all the loaded forms. For example, this code closes all the forms of type OrderForm:
' VB.NET
For Each f As Form In New _
ArrayList(BaseForm.Forms)
If TypeOf f Is OrderForm Then _
f.Close()
Next
// C#
foreach ( Form f in new
ArrayList(BaseForm.Forms) )
{
if ( frm is OrderForm ) frm.Close();
}
Notice that the action of closing a form removes an element from the global Forms collection; therefore, you must loop over a clone of the collection to avoid an exception at the iteration that occurs after a remove operation.
Highlight the Control With the Focus
You open up many new possibilities by having all your forms inherit from a common base class. For example, consider the task of changing the background color of the control that has the focus. Solving this problem once and for all in the BaseForm class requires only a handful of statements (see Listing 2). The base class adds a new public property, named FocusBackColor. You can select a different background color for focused controls on a form-by-form basis, because this property now appears in the Properties window of all the forms that inherit from BaseForm (see Figure 3).
Changing the BackColor of a control when it gets the focus requires trapping the Enter event for all the controls on the form. You need a recursive GetChildControls method to retrieve the controls hosted in a container, such as a GroupBox or a Panel control. This method has a protected scope, so you can use it for other purposes from inside any form that derives from BaseForm. For example, you can put this method to good use when you need to clear all the TextBox controls placed anywhere on the form:
' VB.NET
For Each ctrl As Control In _
GetChildControls(Me)
If TypeOf ctrl Is TextBox Then _
DirectCast(ctrl, TextBox).Clear()
Next
// C#
foreach ( Control ctrl in
GetChildControls(this) )
{
if ( ctrl is TextBox )
(ctrl as TextBox).Clear();
}
Once you understand the mechanism, you can extend the base form with tons of other properties and features. For example, all my forms have a SelectOnFocus property. If this property is true, the contents of any TextBox control are selected automatically when it gets the input focus. You can implement this new feature by adding the SelectOnFocus property procedure and a few lines in the Control_Enter method:
' VB.NET
If SelectOnFocus And TypeOf ctrl Is _
TextBox Then
DirectCast(ctrl, _
TextBox).SelectAll()
End If
// C#
if ( SelectOnFocus && ctrl is TextBox )
(ctrl as TextBox).SelectAll();
These simple techniques can let you achieve a virtually endless number of improvements to the standard behavior. For example, you might trap the MouseEnter and MouseLeave events of all the controls on the form to display a short description of the control in a status bar when the mouse hovers on it.
Create Multilined Tooltips and Help Messages
The Tooltip and the HelpProvider controls support multilined tooltips and help messages, respectively, yet Visual Studio .NET doesn't let you enter a newline character at design time for the ToolTipText and HelpString properties. You have a similar problem with other nonprintable characters, most notably the Tab character.
Adding this feature in a base form is easy, though, because you need to add only a few lines of code in the OnLoad protected method (see Listing 3). When this code is compiled, all the controls in a derived form will expose the new ToolTipText and HelpString extended properties (plus a few other properties added by the HelpProvider control). You can insert a newline in these properties at design time by means of the \n sequence, and you can insert a tab character by means of the \t sequence (see Figure 4). You can extend this mechanism easily to enable insertion of control characters at design time in other controls, such as Labels.
The techniques I've described in this column are simple examples of what you can achieve with form inheritance. I suggest that you always inherit your forms from a common class and enhance this base class with properties and methods that all the forms in all your applications will share.
Portions of this article are based on Chapter 27 of Practical Guidelines and Best Practices for Microsoft Visual Basic .NET and Visual C# Developers by Francesco Balena and Giuseppe Dimauro [Microsoft Press, 2005, ISBN: 0735621721].
About the Author
Francesco Balena has authored several programming books, including Programming Microsoft Visual Basic .NET Version 2003 [Microsoft Press]. He speaks regularly at VSLive! and other conferences, founded the .Net2TheMax family of sites, and is the principal of Code Architects Srl, an Italian software company that offers training and programming tools for .NET developers. Reach him at [email protected].