[ Team LiB ] |
3.3 ContainmentAll useful forms contain some controls. There is more to this containment relationship than meets the eye, and if you are familiar with the old Win32 parent/child relationship, you will find that things do not work in quite the same way. We will look at the control nesting facilities supplied by both the Control class and the ContainerControl class, paying particular attention to the implications of containment for focus and validation events. 3.3.1 Parents and OwnersControls rarely exist in complete isolation—top-level windows usually contain some controls, and all non-top-level controls are associated with a window. In fact, Windows Forms defines two kinds of relationships between controls. There is the parent/child relationship, which manages containment of controls within a single window. There is also a looser association that can exist between top-level windows, which is represented by the owner/owned relationship. 3.3.1.1 Parent and childA child window is one that is completely contained by its parent. For example, any controls that you place on a form are children of that form. A child's position is specified relative to its parent, and the child is clipped to the parent's bounds—i.e., only those parts of the child completely inside the parent are visible. Forms can be children too: document windows in an MDI application are children of the main MDI frame. A control's parent is accessible through its Parent property (of type Control). If you examine this property on a control on a form, you will typically find that it refers to that form. However, many controls can behave as both a parent and a child—if you place a button inside a group box on a form, the button's parent will be the group box, and the group box's parent will be the form. We can also find out if a control has any children—they are available through its Controls property, of type Control.ControlCollection. Examples Example 3-11 and Example 3-12 show this property being used to attach a Click event handler to all controls on a form. (Note that this only attaches itself to direct children of the form. It will not handle clicks from controls nested inside other controls, e.g., a button inside a panel. This could be fixed by writing a recursive version of the method.) Example 3-11. Iterating through child controls with C#private void AddClickHandlers() { foreach(Control c in Controls) { c.Click += new EventHandler(AnyClick); } } private void AnyClick(object sender, System.EventArgs e) { Control clicked = (Control) sender; Debug.WriteLine(string.Format("{0} clicked", clicked.Name)); } Example 3-12. Iterating through child controls with VBPrivate Sub AddClickHandlers() Dim c As Control For Each c in Controls AddHandler c.Click, AddressOf AnyClick Next End Sub Private Sub AnyClick(sender As Object, e As EventArgs) Dim clicked As Control = DirectCast(sender, Control) Console.WriteLine(String.Format("{0} clicked", clicked.Name)) End Sub The parent/child relationship can be established through either the Parent property or the Controls property. A child control's Parent property can be set to refer to a parent. Alternatively, you can use the Controls property on the parent—this is a collection that has Add and AddRange methods to add children. The Forms Designer uses the latter. If you examine the InitializeComponent method generated by the Designer for a form with some controls on it, you will see something like this towards the end of the function in a C# project: this.Controls.AddRange(new System.Windows.Forms.Control[] { this.checkBox1, this.btnCancel, this.btnOK}); In a VB project, the code appears as follows: Me.Controls.AddRange(New System.Windows.Forms.Control() _ {Me.checkBox1, Me.btnCancel, Me.btnOK}) (checkBox1, btnCancel and btnOK are controls that would have been initialized earlier in the method.) This code would have worked equally well if the Designer had set the Parent property to this in C# or to Me in VB on each of these controls, but using Controls.AddRange is slightly more efficient, because it allows all the controls to be attached to the form in one operation. When nesting is in use, you will see a similar call to the AddRange method. For example, if you create a panel with some controls in it, those controls will be added with a call to Controls.AddRange on the panel. This panel itself would then be added to the form's Controls collection. A control might not have a parent—its Parent property could be null (in C#) or Nothing (in VB). Such controls are called top-level windows. Top-level windows are contained directly by the desktop, and usually have an entry in the taskbar. For normal Windows Forms applications, a top-level window is a form of some kind.[7]
3.3.1.2 OwnershipOwnership defines a rather less direct association between windows than parenting. It allows a group of windows, such as an application window and its associated tool windows, to behave as a single entity for certain operations such as minimizing and activation. Ownership is used to group related forms. It is often used for toolbox windows—when an application is minimized, any associated tool windows it displays should also be minimized. Likewise, when the application is activated (i.e., brought to the front by a mouse click or Alt-Tab), the tool windows should also be activated. You can automate this behavior by setting up an ownership association between the tool windows and the main windows. Unlike parenting, ownership only exists between top-level windows, because an owned form is never contained by its owner. (For example, undocked toolbars can usually be moved completely outside the main window, which would not be possible if they were children of that window.) Although an owned form may live outside or overlap its owner, it will always appear directly in front of it in the Z-order.[8] Bringing the owner to the foreground will cause all the forms it owns to appear in front of it. (This is not the same thing as a top-most form, which is described below.) Bringing an owned form to the front will have the same effect as bringing its owner to the front. Minimizing an owner causes all its owned windows to be minimized too, although an owned window can be minimized without minimizing the owner.
Owned windows typically don't need their own representation on the Windows taskbar because they are subordinate to their owners. Because activating an owned window implicitly activates the owner and vice versa, it would merely clutter up the taskbar to have entries for both. So owned forms normally have their ShowInTaskBar properties set to false. The following code fragments (in VB and C#) show a new form being created, owned, and displayed: // defining an owner form in C# MyForm ownedForm = new MyForm(); ownedForm.ShowInTaskbar = false; AddOwnedForm(ownedForm); ownedForm.Show(); ' defining an owner form in VB Dim ownedForm As New [MyForm] ownedForm.ShowInTaskbar = False AddOwnedForm(ownedForm) ownedForm.Show() (This fragment would be inside some method on the owner form, such as its constructor.) AddOwnedForm is a method of the Form class that adds a form to the list of owned forms. (Using ownedForm.Owner = this; or ownedForm.Owner = Me would have exactly the same effect; as with parenting, the ownership association can be set up from either side.) Note the use of the ShowInTaskBar property to prevent this window from getting its own entry in the taskbar. All owned forms are closed when their owning form is closed. Because they are considered wholly subordinate to the owner, they don't receive the Closed or Closing events when the main form closes (although they do if they are closed in isolation.) So if you need to handle these events, you must do so in the owning form. 3.3.1.3 Top-most formsIt is important not to confuse owned forms with top-most forms. (These in turn should not be confused with top-level forms, as defined earlier.) Superficially, they may seem similar: a top-most form is one that always appears on top of any non-top-most forms. Viewed in isolation, owned forms may look like they are doing the same thing—an owned form always appears on top of its owner. However, top-most forms are really quite different—they will appear on top of all other windows, even those from other applications. If you need a form to sit above all other windows, set its TopMost property to true. Certain kinds of popup might need to set this property to true—your application might need to display some visual alert that should be visible regardless of what windows are currently open, much like Windows Messenger does. But exercise good taste—making all windows top-most is pointless because ultimately only one window can really be at the very top (the top-most window with the highest Z-order), and it can be very annoying for the user to be unable to hide a top-most window. If you decide to make a window top-most, unless it is a short-lived pop-up window, you should provide a way of disabling this behavior, as the Windows Task Manager does with its Always on Top menu option. Owned forms and top-most forms are useful when we need to control the ordering of forms either with respect to all other windows on the desktop or just between specific groups of forms. But arguably the most important relationship is the one between parent and child controls—this association is fundamental to the way controls are contained within a window. Although the parent/child relationship is managed by the Control class, there can be complications with focus management for nested controls. This issue is dealt with by the ContainerControl class, which we will look at now. 3.3.2 Control and ContainerControlAs we have seen, the ability to act as a container of controls (i.e., to be a parent) is a feature supplied by the Control class. Its Controls property manages the collection of children. Only certain control types elect to present this container-like behavior in the Designer (e.g., the Form, Panel, and GroupBox controls), but more bizarre nesting can be arranged if you write the code by hand—it is possible to nest a button inside another button, for example. This is not useful, but it is possible as a side effect of the fact that containment is a feature provided by the base Control class. But if you examine the Form class closely, you will see that it inherits from a class called ContainerControl. You might be wondering why we need a special container control class when all controls can support containment. The answer is that ContainerControl has a slightly misleading name. ContainerControl only really adds one feature to the basic Control.[9] The main purpose of a ContainerControl is to provide focus management.
Sometimes you will build groups of controls that act together as a single entity. The most obvious example is a form, which is both a group of controls and also a distinct entity in the UI. But as we will see in Chapter 5, it is possible to build non-top-level controls composed from multiple other controls (so-called user controls). Such groups typically need to remember which of their constituent controls last had the focus. For example, if a form has lost the focus, it is important that when the form is reactivated, the focus returns to the same control as before. Imagine how annoying it would be if an application forgot which field you were in every time you tabbed away from it. And we also expect individual controls on a form to remember where they were—when the focus moves to a list control, we expect it to remember which list item was selected previously, and we expect tree controls to remember which tree item last had the focus. Users expect UI elements to remember such state in between losing the focus and reacquiring it. (Most users probably wouldn't be conscious of the fact that they expect this, but they would soon complain if you were to provide them with an application that forgot where it was every time it lost the focus.) So the Windows Forms framework helpfully provides us with this functionality in the ContainerControl class. Most of the time, you don't really need to think about ContainerControl. It should be used whenever you build a single UI element that consists of several controls, but because the Form class and the UserControl class (see Chapter 5) both inherit from ContainerControl, you are forced into doing the right thing. Note that the Panel and GroupBox classes do not derive from ContainerControl, even though they usually contain other controls. This is because they do not aim to modify focus management in any way—they are essentially cosmetic. Focus for controls nested inside these controls is managed in exactly the same as it would have been if they were parented directly by the form, because a ContainerControl assumes ownership not just for its children, but for all its descendants. (Of course, if it has any ContainerControl descendants, it will let those manage their own children; each ContainerControl acts as a boundary for focus management.) 3.3.2.1 Focus and validationAs discussed in the previous chapter, focus management is closely related to validation. A control whose CausesValidation property is true will only normally be validated when two conditions are met: first, it must have had the focus; and second, some other control whose CausesValidation property is also true must subsequently receive the focus. (Any number of controls whose CausesValidation property is false may receive the focus in between these two events.) Because ContainerControl groups a set of controls together and manages the focus within that group, it has an impact on how validation is performed. When the focus moves between controls within a ContainerControl, the validation logic works exactly as described above. But when the focus moves out of a ContainerControl that is nested within another ContainerControl (e.g., a UserControl on a Form), things are a little more complex. Figure 3-2 shows a form (which is a ContainerControl) and a UserControl. We will discuss the UserControl class in Chapter 5, but for now, the important things to know are that it derives from ContainerControl and that it is treated as a single entity by the containing form (the form will not be able to see the individual text boxes and labels inside the control). All the text boxes have Validating event handlers, and all the controls have their CausesValidation properties set to true. Currently, the focus is in the Foo text box. Figure 3-2. Validation and ContainerControl nestingWhen the focus moves to Bar, the rules of validation say that Foo must be validated. This is not a problem—both controls are inside the same ContainerControl (MyUserControl). It is responsible for their focus management, so it will ensure that Foo is validated. But what would happen if instead the focus moved to Quux? Quux is not inside the user control—its focus is managed by another ContainerControl, the form. The form knows nothing of the Foo and Bar fields—these are just encapsulated implementation details of the user control. But it will correctly determine that MyUserControl should be validated because both MyUserControl and Quux have their CausesValidation property set to true. Fortunately, when any ContainerControl (such as a UserControl) is validated, it remembers which of its member controls last had the focus, and validates that. So in this case, when the focus moves from Foo to Bar, the form validates MyUserControl, which in turn validates Foo. 3.3.3 Ambient PropertiesRegardless of whether your controls are all children of the form, nested inside group boxes and panels, or nested within a ContainerControl for focus management, you will want your application to look consistent. When you modify certain properties of a form's appearance, all the controls on the form should pick up the same properties. For example, if you change the background color of your form, you will probably want any controls on the form to use the same background color. It would be tedious if you had to set such properties manually on every single control on the form. Fortunately you don't have to—by default, the main visual properties will propagate automatically. The properties that behave like this are known as ambient properties. The ambient properties on the Control class are Cursor, Font, ForeColor, and BackColor. It is useful to understand exactly how ambient properties work—the Forms Designer in Visual Studio .NET doesn't show you everything that is going on, and the results can therefore sometimes be a little surprising. Using the Designer, you could be forgiven for assuming that if you don't set a visual property of a control, it will just have a default value. For example, the background color of a button will seem to be SystemColors.Control. However, a control distinguishes between a property that has had its value set and a property that hasn't. So when you don't set the BackColor of a control, it's not that the BackColor has a default value; it actually has no value at all. This is obfuscated somewhat by the fact that when you retrieve a control's BackColor, you will always get a nonempty value back. What is not obvious is that this value didn't necessarily come from the control in question. If you ask a control for its background color when the background color has not been set on that control, it starts looking elsewhere to find out what its color should be. If a control doesn't know what value a particular property should have, the first place it looks is its parent. So if you put a button on a form, then read that button's BackColor without having set it, you are implicitly reading the form's BackColor. But what if there is no parent to ask? A Form might have no parent, so what does it do when asked for its BackColor if none has been specified? At this point it attempts to see if it is being hosted in an environment that supplies it with an AmbientProperties object. To find this out, it uses the Control class's Site property, and if this is non-null, it will call its GetService method to determine whether the environment can supply an AmbientProperties object. Usually there will be no site, in which case, it finally falls back to returning its default value. (This will be the case if the form is just being run as a standalone application; you usually only get a site when being hosted in something like Internet Explorer.) So what impact do these ambient properties have on your application's behavior? Their effect is that unless you explicitly specify visual properties for your controls, they will automatically pick up appropriate values from their surroundings. If a control is being hosted in some environment that supplies values for these ambient properties, such as Internet Explorer, it will use those. Otherwise, the system-wide defaults will be used. Some controls deliberately ignore certain ambient properties, either because they have no use for them or because they positively want to use something else. For example, the TextBox class overrides the BackColor property so that its background is always the SystemColors.Window color (typically white) by default, regardless of what the ambient background color is. Remember that whenever you read an ambient property on a control, you will get back something, but unless that property was set explicitly on that control, the value you get back will have been retrieved from elsewhere. Visual Studio .NET makes it clear when you have modified a property on a control by showing the value of that property in bold type. This is useful, but it does not tell you how the property obtains its value when it has not been set explicitly—the Properties window always shows the effective value, without telling you where that value came from. In some cases, you may need to examine the source code to see exactly what it has done: if the property has not been set explicitly in the InitializeComponent method, the value shown will be the ambient one. 3.3.4 MDI ApplicationsMany Windows applications use the Multiple Document Interface (MDI). This defines a user interface structure for programs that can display multiple files. The application has a main window, and each document being edited is displayed inside a child window. Windows Forms provides special support for this. We could just create our document windows as children of the main application window. However, this still leaves us with a certain amount of work to do to manage menus correctly—MDI applications usually present their menus in the main application window, but modify which items are present according to whether a document window is active. Windows Forms is able to manage MDI menus correctly for us, including automatically merging a child window's menu into the main application window. The details of menu merging are discussed in Chapter 4, but to make this happen automatically, we must tell Windows Forms that we are building an MDI-style application. First of all, we must set the parent window's IsMdiContainer property to true. Second, when we display a child window, we must let Windows Forms know that is should behave as an MDI child, as in the following C# code fragment: ChildForm cf = new ChildForm(); cf.MdiParent = this; cf.Show(); or in its equivalent VB code fragment: Dim cf As New ChildForm() cf.MdiParent = Me cf.Show() By establishing the parent/child relationship with the MdiParent property instead of the normal Parent property, we enable automatic menu merging. |
[ Team LiB ] |