[ Team LiB ] |
3.2 The Form ClassAll windows in a Windows Forms application are represented by objects of some type deriving from the Form class. Of course, Form derives from Control, as do all classes that represent visual elements, so we have already seen much of what it can do in the previous chapter. But we will now look at the features that the Form class adds. You will rarely use the Form class directly—any forms you define in your application will be represented by a class that inherits from Form. Adding a new form in Visual Studio .NET simply adds an appropriate class definition to your project. We will examine how it structures these classes when generating new forms, and we will look at how it cleans up any resource used by the form when it is destroyed. Then, we will consider the different types of forms. Finally, we will look at extender properties. These provide a powerful way of extending the behavior of all controls on a form to augment the basic Control functionality. 3.2.1 The Forms DesignerMost forms are designed using the Forms Designer in Visual Studio .NET. This is not an essential requirement—the designer just generates code that you could write manually instead. It is simply much easier to arrange the contents of a form visually than it is to write code to do this. When you add a new form to a project, a new class definition is created. The Designer always uses the same structure for the source code of these classes. They begin with private fields in C# and Friend fields in VB to hold the contents of the form. (The Designer inserts new fields here as you add controls to the form.) Next is the constructor, followed by the Dispose and InitializeComponent methods; these are all described below. If this is the main form in your application, the program's entry point (the Main method described above) will follow in C# programs; in VB programs, it will be added by the compiler at compile time, but will not be displayed with the form's source code. Finally, any event handlers for controls on your form will be added at the end of the class. The Designer does not make it obvious where you are expected to add any code of your own, such as fields or methods other than event handlers. This is because it doesn't matter—Visual Studio .NET is pretty robust about working around you. It is even happy for you to move most of the code that it generates if you don't like the way it arranges things, with the exception of the code inside the InitializeComponent method, which you should avoid modifying by hand. (The editor hides this code by default to discourage you from changing it.) 3.2.1.1 InitializationAny freshly created form will contain a constructor and an InitializeComponent method. The job of these methods is to make sure a form is correctly initialized before it is displayed. The generated constructor is very simple—it just calls the InitializeComponent method. The intent here is that the Forms Designer places all its initialization code in InitializeComponent, and you will write any initialization that you require in the constructor. The designer effectively owns InitializeComponent, and it is recommended that you avoid modifying its contents, because this is liable to confuse the Designer. So when you look at the source code for a form class, Visual Studio .NET conceals the InitializeComponent method by default—it is lurking behind a line that appears as "Windows Form Designer generated code."[4] You can see this code by clicking on the + symbol at the left of this line in the editor.
Although the theory is that you will never need to modify anything inside this generated code, you may occasionally have to make edits. If you do make such changes by hand, you must be very careful not to change the overall structure of the method, as this could confuse the Designer, so it is useful to know roughly how the method is arranged. It begins by creating the objects that make up the UI: each control on the form will have a corresponding line calling the new operator, and store the result in the relevant field. In C#, for example, such code appears as follows: this.button1 = new System.Windows.Forms.Button(); this.label1 = new System.Windows.Forms.Label(); this.textBox1 = new System.Windows.Forms.TextBox(); and in VB, it appears as follows: Me.Button1 = New System.Windows.Forms.Button() Me.Label1 = New System.Windows.Forms.Label() Me.TextBox1 = New System.Windows.Forms.TextBox() Next, there will be a call to the SuspendLayout method, which is inherited from the Control class. Layout is discussed in detail later on, but the purpose of this call is to prevent the form from attempting to rearrange itself every time a control is set up. Then each control is configured in turn—any necessary properties are set (position, name, and tab order, at a minimum), and event handlers (in C# only) are added. In C#, this looks like the following: this.textBox1.Location = new System.Drawing.Point(112, 136); this.textBox1.Name = "textBox1"; this.textBox1.TabIndex = 2; this.textBox1.Text = "textBox1"; this.textBox1.TextChanged += new System.EventHandler(this.textBox1_TextChanged); The corresponding VB code appears as follows: Me.TextBox1.Location = New System.Drawing.Point(112, 136) Me.TextBox1.Name = "TextBox1" Me.TextBox1.TabIndex = 2 Me.TextBox1.Text = "TextBox1" After this, the form's size is set and then all the controls are added to its Controls collection. (Simply creating controls and storing them in private fields is not enough to make them appear on screen—they must be explicitly added to the form on which they are to appear; this process will be discussed in detail later.) Finally, the ResumeLayout method, which is inherited from the Control class, is called. This is the counterpart of the earlier call to SuspendLayout, and it indicates to the form that the various additions and modifications are complete, and that it won't be wasting CPU cycles when it manages its layout. This call will also cause an initial layout to be performed, causing any docked controls to be positioned appropriately. 3.2.1.2 DisposalThe other method created on all new forms is the Dispose method. This runs when the form is destroyed and frees any resources that were allocated for the form. In fact, all controls have two Dispose methods: one public, supplied by the framework, and one protected, which you usually write yourself. To understand why, we must first look at the way resources are normally released in .NET. The CLR has a garbage collector, which means that when objects fall out of use, the memory used by those objects will eventually be freed automatically. Classes can have special functions called finalizers, which are run just before the garbage collector frees an object. Classes in the .NET Framework that represent expensive resources such as window handles usually have finalizers that release these resources. So in the long run, there will be no resource leaks—everything will eventually be freed either by the garbage collector or by the finalizers that the garbage collector calls. Unfortunately, the garbage collector only really cares about memory usage, and only bothers to free objects when it is low on memory. This means that a very long time (minutes or even hours) can pass between an object falling out of use and the garbage collector noticing and running its finalizer. This is unacceptable for many types of resources, especially the kinds used by GUI applications. (Although current versions of Windows are much more forgiving than the versions of old, hogging graphical resources has never been a good idea and is best avoided even today.) So the .NET Framework defines a standard idiom for making sure such resources are freed more quickly, and the C# language has special support for this idiom. Objects that own expensive resources should implement the IDisposable interface, which defines a single method, Dispose. If code is using such an object, as soon as it has finished with the object it should call its Dispose method, allowing it to free the resources it is using. (Such objects usually also have finalizers, so if the client code forgets to call Dispose, the resources will be freed eventually, if somewhat late. But this is not an excuse for not calling the method.) The Control class (and therefore any class deriving from it) implements IDisposable, as do most of the classes in GDI+, so almost everything you use in Windows Forms programming relies on this idiom. Fortunately, the C# language has special support for it. The using keyword can automatically free disposable resources for us at the end of a scope: using(Brush b = new SolidBrush(this.ForeColor)) { ... do some painting with the brush ... } When the code exits the block that follows the using statement, the Brush object's Dispose method will be called. (The Brush class is part of GDI+, and it implements IDisposable; this example is typical of redraw code in a custom control.) The most important feature of this construct is that it will call Dispose regardless of how we leave the block. Even if the code returns from the middle of the block or throws an exception, Dispose will still be called, because the compiler puts this code in a finally block for us.[5]
Forms typically have a lot of resources associated with them, so it is not surprising that they are always required to support this idiom. In fact, all user elements are—the Control class enforces this because it implements IDisposable. The good news is that most of the work is done for us by the Control class, as is so often the case. It provides an implementation that calls Dispose on all the controls contained by the form and frees all resources that the Windows Forms framework obtained on your behalf for the form. But it also provides us with the opportunity to free any resources that we may have acquired that it might not know about. (For example, if you obtain a connection to a database for use on your form, it is your responsibility to close it when the form is disposed.) The picture is complicated slightly by the fact that there are two times at which resource disposal might occur. Not only must all resources be freed when Dispose is called, they must also be freed if the client has failed to call Dispose by the time the finalizer runs. The model used by the Control class[6] enables you to use the same code for both situations: any code to free resources allocated by your form lives in an overload of the Dispose method, distinguished by its signature: void Dispose(bool) (in C#) or Sub Dispose(Boolean) (in VB). This method will be called in both scenarios—either when the user calls IDispose.Dispose or when the finalizer runs.
It is important to distinguish between timely disposal and finalization when cleaning up resources. In a finalizer, it is never possible to be sure whether any references you hold to other objects are still valid: if the runtime has determined that your object is to be garbage collected, it is highly likely that it will also have decided that the objects you are using must be collected too. Because the CLR makes no guarantees of the order in which finalizers are run, it is entirely possible that any objects to which you hold references have already had their finalizers run. In this case, calling Dispose on them could be dangerous—most objects will not expect to have their methods called once they have been finalized. So most of the time, your Dispose method will only want to do anything when the object was explicitly disposed of by the user. The only resources you would free during finalization would be those external to the CLR, such as any temporary files created by your object or any handles obtained through interop. The Dispose method that you are intended to override is protected, so it cannot be called by external code. It will be called by the Control class if the user calls the public Dispose method (IDispose.Dispose). In this case, the parameter passed to the protected Dispose method will be true. It will also be called when the finalizer runs, in which case the parameter will be false. (Note that this method will only be called once—if IDispose.Dispose is called, the Control class disables the object's finalizer.) So the parameter indicates whether resources are being freed promptly or in a finalizer, allowing you to choose the appropriate behavior. Consider the code generated by the Designer, as shown in Examples Example 3-5 and Example 3-6. Example 3-5. The default protected Dispose method in C#protected override void Dispose( bool disposing ) { if( disposing ) { if (components != null) { components.Dispose(); } } base.Dispose( disposing ); } Example 3-6. The default protected Dispose method in VBProtected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then If Not (components Is Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub This checks to see if the public Dispose method was called, and if it was, it disposes of the components object, if present. (The components object is a collection of any non-Control components in use on the form, e.g., data sources.) But if finalization is in progress (i.e., the disposing parameter is false), it doesn't bother, for the reasons detailed above. If you add any code to this Dispose method, it too will normally live inside the if(disposing) { ... } block.
There are two very important rules you must stick to if you need to modify this resource disposal code in your form. First, you must always call the base class's Dispose method in your Dispose method, because otherwise the Control class will not release its resources correctly. Second, you should never define your own finalizer in a form—doing so could interact badly with the Control class's own finalizer; the correct place to put code to release resources in a form (or any other UI element) is in the overridden protected Dispose method. This is precisely what the code generated by the forms designer does, as shown in Examples Example 3-5 and Example 3-6. You may be wondering what the components member is for, and why it needs to be disposed of. It is a collection of components, and its job is to dispose of those components—if you add a component such as a Timer to a form, the Forms Designer will automatically generate code to add that component to the components collection. In fact, it does this by passing components as a construction parameter to the component, e.g.: this.timer1 = new System.Windows.Forms.Timer(this.components); The component will then add itself to the components collection. As you can see from Examples Example 3-5 and Example 3-6, the default Dispose method supplied by the Designer will call Dispose on the components collection. This in turn will cause that collection to call Dispose on each component it contains. So if you are using a component that implements IDispose, the easiest way to make sure it is freed correctly is simply to add it to the components collection. The Forms Designer does this automatically for any components that require disposal. (It determines which require disposal by examining their constructors—if a component supplies a constructor that takes an IContainer as a parameter, it will use that constructor, passing components as the container.) You can also add any objects of your own to the collection: components.Add(myDisposableObject); or: components.Add(myDisposableObject) 3.2.2 Showing Modal and Non-Modal FormsAll forms created by Visual Studio .NET will conform to the structure just described. But as with dialogs in classic Windows applications, there are two ways in which they can be shown: forms can exhibit either modal or non-modal behavior. A modal form is one that demands the user's immediate attention, and blocks input to any other windows the application may have open. (The application enters a mode where it will only allow the user to access that form, hence the name.) Forms should be displayed modally only if the application cannot proceed until the form is satisfied. Typical examples would be error messages that must not go unnoticed or dialogs that collect data from the user that must be supplied before an operation can be completed (e.g., the File Open dialog—an application needs to know which file it is supposed to load before it can open it). You select between modal and non-modal behavior when you display the form. The Form class provides two methods for displaying a form: ShowDialog, which displays the form modally, and Show, which displays it non-modally. The Show method returns immediately, leaving the form on screen. (The event handling mechanism discussed earlier can deliver events to any number of windows.) A non-modal form has a life of its own once it has been displayed; it may even outlive the form that created it. By contrast, the ShowDialog method does not return until the dialog has been dismissed by the user. Of course, this means that the thread will not return to the Application class's main event-handling loop until the dialog goes away, but this is not a problem because the framework will process events inside the ShowDialog method. However, events are handled differently when a modal dialog is open—any attempts to click on a form other than the one being displayed modally are rejected. Other forms will still be redrawn correctly, but will simply beep if the user tries to provide them with any input. This forces the user to deal with the modal dialog before progressing. There is a more minor (and somewhat curious) difference between modal and non-modal use of forms: resizable forms have a subtly different appearance. When displayed modally, a form will always have a resize grip at the bottom righthand corner. Non-modal forms only have a resize grip if they have a status bar. Be careful with your use of modal dialogs, because they can prove somewhat annoying for the user: dialogs that render the rest of the application inaccessible for no good reason are just frustrating. For example, older versions of Internet Explorer would prevent you from scrolling the main window if you had a search dialog open. If you wanted to look at the text just below the match, you had to cancel the search to do so. Fortunately this obstructive and needless use of a modal dialog has been fixed—Internet Explorer's search dialog is now non-modal. To avoid making this kind of design error in your own applications, you should follow this guideline: do not make your dialogs modal unless they really have to be. 3.2.2.1 Closing formsHaving displayed a form, either modally or non-modally, we will want to close it at some point. There are several ways in which a form can be closed. From a programmer's point of view, the most direct approach is to call its Close method, as follows: this.Close(); // C# Me.Close() ' VB A form may also be closed automatically by the Windows Forms framework in response to user input; for example, if the user clicks on a form's close icon, the window will close. However, if you want to prevent this (as you might if, for example, the window represents an unsaved file), you can do so by handling the Form class's Closing event. The framework raises this event just before closing the window, regardless of whether the window is being closed automatically or by an explicit call to the Close method. The event's type is CancelEventHandler; its Boolean Cancel property enables us to prevent the window from closing if necessary. Examples Example 3-7 and Example 3-8 illustrate the use of this property when handling the Closing event. Example 3-7. Handling the Closing event in C#private void MyForm_Closing(object sender, System.ComponentModel.CancelEventArgs e) { if (!IsWorkSaved()) { DialogResult rc = MessageBox.Show( "Save work before exiting?", "Exit application", MessageBoxButtons.YesNoCancel); if (rc == DialogResult.Cancel) { e.Cancel = true; } else if (rc == DialogResult.Yes) { SaveWork(); } } } Example 3-8. Handling the Closing event in VBPrivate Sub MyForm_Closing(sender As Object, _ e As System.ComponentModel.CancelEventArgs) If Not IsWorkSaved() Then Dim rc As DialogResult = MessageBox.Show( _ "Save work before exiting?", _ "Exit application", _ MessageBoxButtons.YesNoCancel) If rc = DialogResult.Cancel Then e.Cancel = True Else If rc = DialogResult.Yes Then SaveWork() End If End If End Sub The form in Examples Example 3-7 and Example 3-8 checks to see if there is unsaved work. (IsWorkSaved is just a fictional method for illustrating this example—it is not part of the framework.) If there is, it displays a message box giving the user a chance to save this work, abandon it, or cancel, which keeps the window open. In the latter case, this code informs the framework that the window should not be closed after all by setting the Cancel property of the CancelEventArgs argument to true. If you write an MDI application (i.e., an application that can display multiple documents as children of a single main frame), the framework treats an attempt to close the main window specially. Not only does the main window get a Closing and Closed event, so does each child window. The child windows are asked first, so if each child represents a different document, each child can prompt the user if there is unsaved work. But none of the children are closed until all of the windows (the children and the main window) have fired the Closing event. This means the close can be vetoed by any of the windows. The close will only happen if all the child windows and the main window are happy. If nothing cancels the Closing event, the window will be closed, and the Closed event will be raised. If the form is shown non-modally, the framework then calls the form's Dispose method to make sure that all the form's resources are freed. This means once a non-modal form has been closed, you cannot reuse the object to display the form a second time. If you call Show on a form that has already been closed, an exception will be thrown. For modal dialogs, however, it is common to want to use the form object after the window has closed. For example, if the dialog was displayed to retrieve information from the user, you will want to get that information out of the object once the window closes. Modal dialogs are therefore not disposed of when they are closed, and you must call Dispose yourself, as shown in Examples Example 3-9 and Example 3-10. You should make sure that you use any properties or methods that you need before calling Dispose (i.e., inside the using block). Example 3-9. Disposing of a modal dialog in C#using (LoginForm lf = new LoginForm()) { lf.ShowDialog(); userID = lf.UserID; password = lf.Password; } Example 3-10. Disposing of a modal dialog in VBTry Dim lf As New LoginForm() lf.ShowDialog() userID = lf.UserID password = lf.Password Finally If.Dispose() End Try Although the framework will automatically try to close a window when its close icon is pressed, it is common to want to close a form as the result of a button click. It turns out that if the button does nothing more than close the form, you do not need to write a click handler to make this happen. The Windows Forms framework will automatically close the form when any button with a DialogResult is clicked. So we will now look at dialog results. 3.2.2.2 Automatic button click handlingA dialog might be closed for several different reasons. Instead of clicking the OK button, the user might attempt to cancel the dialog by clicking on its close icon or Cancel button, or by pressing the Escape key. Most applications will distinguish between such cancellation and normal completion, and some may make a finer distinction still, such as a message box with Yes, No, and Cancel buttons. Windows Forms provides support for automatically managing the various ways of closing a window without having to write click handlers. It also makes it easy for users of a form to find out which way a form was closed. Both of these facilities revolve around dialog results. The Form class's ShowDialog method returns a value indicating how the dialog was dismissed. The returned value corresponds to the DialogResult property of the button with which the user closed the window. The following code shows an excerpt from the initialization of a form containing two buttons, buttonOK and buttonCancel (the Forms Designer will generate such code if you set a button's DialogResult property in the Properties window): buttonOK.DialogResult = DialogResult.OK; buttonCancel.DialogResult = DialogResult.Cancel; Any code that shows this dialog will be able to determine which button was clicked from ShowDialog's return code. The returned value can also be retrieved later from the DialogResult property of the Form object. The type of the ShowDialog method's return value and of the DialogResult property of both the Form object and of individual Button controls is also DialogResult, which is an enumeration type containing values for the most widely used dialog buttons: OK, Cancel, Yes, No, Abort, Retry, and Ignore. To handle button clicks without an event handler, you must set a button's DialogResult property to any value other than the default (DialogResult.None). Then clicking that button will cause the framework to close the form and return that value. If you want, you can still supply a Click event handler for the button, which will be run before the window is closed. But the window will be closed whether you supply one or not (unless there is a Closing handler for the form that cancels the close, as described earlier). It is also possible to return a dialog result without using a Button control. If you wish to close the form in response to some event that did not originate from a button, you can also set the Form class's DialogResult property before calling Close. But what about when the form is cancelled by pressing the Escape key? We normally want the form to behave in the same way regardless of how it is dismissed. Specifically, we would like to run the same event handler and return the same DialogResult in all three cases. This turns out to be simple because the Windows Forms framework can fake a click on the Cancel button when the Escape key is pressed. All we need to do is tell the form which is our Cancel button (which could be any button—it doesn't have to be labeled Cancel)—with the Form class's CancelButton property: this.CancelButton = buttonCancel; // C# Me.CancelButton = buttonCancel ' VB If buttonCancel has a handler registered for its Click event, that handler will be called either when the button is clicked, or when the Escape key is pressed. In both cases, the same two things to happen: first, the Click handler (if there is one) is called, then the window is closed. The Click handler for the button indicated by the CancelButton property does not need to take any special steps to close the window.
As with all buttons, if you specify a DialogResult other than None for the Cancel button, that value will be used as the dialog result. However, the button referred to by the CancelButton property is unusual in that if this property is set to None, it behaves as though it were set to Cancel: the form will be closed, and the dialog result will be Cancel. (Also, when you choose a CancelButton in the Forms Designer, it sets the button's DialogResult property to Cancel automatically. This seems to be overkill, because it would return Cancel in any case.) As well as supporting a CancelButton, a form can also have an AcceptButton. If set, this will have a Click event faked every time the user presses the Enter key while on the form. However, this turns out to be less useful than the CancelButton because this behavior is disabled if the control that currently has the focus does something with the Enter key. For example, although Button controls behave as though clicked when Enter is pressed, if some button other than the AcceptButton has the focus, that button will get a Click event, not the AcceptButton. If a multiline TextBox control has the focus, it will process the Enter key instead. So if your form consists of nothing but buttons and multiline text boxes, there is no point in setting the AcceptButton property. Note that unlike the CancelButton, if you do assign an AcceptButton, the form will only be closed automatically when this button is clicked if you explicitly set the accept button's DialogResult property to something other than None. We have now seen how to create, display, and dismiss forms. But of course, a form's main role is to act as a container of other controls—empty windows are rarely useful. So we will now look in more detail at the nature of control containment in the Windows Forms framework. |
[ Team LiB ] |