DekGenius.com
[ Team LiB ] Previous Section Next Section

5.1 Composite Controls

The built-in controls are undeniably very useful—almost every Windows application uses them. Not only does this avoid reinventing the wheel, it also enhances usability: consistency is a desirable property in interactive applications. By using the standard controls, you guarantee consistency, both within your program and also with other Windows applications. Arguably one of Windows' greatest strengths is that most applications have a great deal in common—experienced users are familiar with all the standard control types, such as buttons and comboboxes. Using controls that users know and understand reduces the amount of learning required to use your application.

So it makes sense to use the standard Windows controls wherever possible. But your application may also be able to reap the usability benefits of consistent design on a larger scale—it may be possible to reuse whole chunks of the user interface, not just individual controls. For example, consider an email application. Many email clients allow items to be viewed through a preview pane in the main window as well as in a standalone window. These two views are likely to have a great deal in common—the main area showing the contents of the email will need to do the same thing in both cases. The area showing parts of the email header (fields such as From, To, and Subject) will be either the same or similar. And in a high-quality application, these parts of the UI are likely to be fairly sophisticated. You might want the From and To fields to provide pop-up menus allowing the user to send mail to individual recipients, or add them to an address list.

It would be irritating if the header fields behaved inconsistently, because they are supposed to represent the same things in both locations. If these fields presented a pop-up menu when examining an email in its own window, but failed to do so in the preview pane, it would likely frustrate the user. Not only is it annoying for the user, it is clearly counterproductive for the developer: if a section of the UI that has the same function must appear in two different places, you won't want to write the same code twice—not only do you risk inconsistent behavior, you are wasting your time.

What we want is some way of taking such sections of the user interface and turning them into reusable components. This is exactly what composite controls are all about. In Windows Forms, we build composite controls with the UserControl class.

5.1.1 The UserControl Class

The UserControl class is the base class for all composite controls, which are reusable portions of user interface. Any class based on UserControl consists of one or more child controls and some code that manages their behavior. Once you have created such a composite user interface element, any Windows Forms application can use it just like any other control.

The UserControl class is surprisingly similar to the Form class. Both derive from ContainerControl, enabling them to manage the focus as it moves around the child controls. Both can be scrollable, because ContainerControl derives from ScrollableControl. Both let you assemble child controls into a useful chunk of user interface. Both are even edited in the same way in Visual Studio .NET—the Forms Designer can design a UserControl as well as a Form.

The main difference between a Form and a UserControl is that the UserControl is designed to be dropped into a container—either a Form or another UserControl.[1] Consequently, a UserControl has no border or titlebar, and cannot be a top-level window.

[1] In fact, there is nothing preventing a form from being a child of another window—this is how MDI applications work. It is just that there is no support for this in the designer.

Figure 5-1 shows an example of a composite control displayed in the Visual Studio .NET Designer. As with a form, the selection outline and grid points are visible, you can drag and drop controls into here from the Toolbox, and you can use any control, including ones you have designed yourself. Any control that works in a form also works in a composite control. (Recursion is not allowed though—it would make no sense for a control to contain a copy of itself.)

Figure 5-1. A UserControl in the Designer
figs/winf_0501.gif

The control in Figure 5-1 consists of several label controls, some of which are empty because their values are determined at runtime, and displays information from an email header. (The empty controls have been indicated with a dotted outline above so that you can see them.) Although it just looks like a few labels on a form, such a control could easily be fairly complex. For example, it would probably have custom layout logic to deal with different lengths of email address. And emails often go to many recipients, so this layout might be nontrivial, maybe even needing scrollbars on the recipient list. The email addresses would most likely have context menus associated with them for the reasons discussed earlier. The control might optionally support displaying other header items. For a professional quality application, this kind of feature list can run on and on.

In other words, a simple-looking part of the user interface can turn out to have a surprising amount of code associated with it. So if that user interface fragment is likely to be used in multiple contexts (e.g., in the standalone and preview views shown in Figures Figure 5-2 and Figure 5-3[2]), you will want to be able to reuse the code rather than copying it into two different places. By encapsulating this piece of the user interface as a UserControl, you make such reuse simple. You also guarantee consistency by making sure that just one component is used in both contexts.

[2] If you are wondering why some of the labels appear to be in a different font from that in Figure 5-1, this particular control uses an emboldened version of the font for these labels (as specified by the Font property). This font selection is done at runtime, which is why it doesn't show in the Designer.

Figure 5-2. A composite control in a preview panel
figs/winf_0502.gif
Figure 5-3. A composite control in a standalone window
figs/winf_0503.gif

From the point of view of a form that uses a composite control (such as those in Figure Figure 5-2 and Figure 5-3), the control is no different from any other. It derives (indirectly) from Control, and so it supports all the normal properties and behavior. For example, in these two cases, docking has been used to position the controls. Developers using your component would not necessarily be aware that it was based on UserControl—as far as they are concerned, it's just another control.

In fact, Visual Studio .NET can even put your class into the Toolbox automatically. If your solution contains a UserControl, it will appear in the Toolbox when you edit any other form or user control in the designer. (If you are using Visual Studio .NET 2002, the control will appear at the bottom of the Windows Forms tab in the Toolbox. If you are using Visual Studio .NET 2003, it will appear in the My User Controls tab.) Note that you must do two things for this to work: first, your project must have been built (without errors) for it to appear; second, you may need to close the Designer window and reopen it after building before the control will appear in the Toolbox. (This only works when using a control defined in the same solution. If you want to use a control defined elsewhere, you will need to add the component to the Toolbox manually.)

Because the UserControl class is so similar to the Form class, it should come as no surprise that creating a composite control is essentially the same as designing a form—you arrange the constituent controls and handle events in the same way for both. The Visual Studio .NET Designer generates almost exactly the same code for forms and composite controls. Apart from the absence of a window border or support for a main menu, the only difference between writing a form and a composite control is that with a control, you are not only designing a user interface, you are also providing a programming interface—your control will be used by other developers, and you must bear their requirements in mind as well as the needs of the end user. The issues here are the same for all user-defined controls, so we will discuss them towards the end of the chapter.

5.1.2 Reusing Without Inheritance

The purpose of composite controls is to enable reuse—a section of user interface can be encapsulated in a control and reused in any number of different contexts. Code that reuses a composite control is not required to inherit from it. This shouldn't be surprising, but it is worth stressing because many people often equate reuse in an object-oriented system with inheritance. The most important form of reuse is containment; for example, a form can contain one or more instances of a composite control. Inheritance is a much more specialized technique.

When designing a new composite control, few developers would make the mistake of attempting to derive from another user control when it would be inappropriate.[3] But there is a fairly common scenario where it is much easier to fall into the trap of misuse of inheritance.

[3] Inheritance is not always the wrong thing to do—see the next chapter. It is just a less widely applicable mechanism than is commonly supposed.

You may want to create a reusable control that is very similar to one of the built-in controls, but that adds to its behavior in some way. Inheritance seems like an attractive option here—after all, the whole point of inheritance may appear to be that you can take a class and extend it in some way. But while inheritance can be the right way to go in these circumstances (and the next chapter explains how to do this), it is more often the wrong approach. To understand why, we must bear in mind that inheritance always implies an "is a" relationship. If you make your control derive from some other control, you are making a strong statement: you are saying that your control is compatible in every respect with the control from which it derives.

Consider a control whose purpose is to present an XML document through a tree-like view. We already have a built-in control for showing tree-like structures: the TreeView. Basing our control on this will be the right thing to do, because there is no sense in writing our own tree control from scratch. However, it would be a mistake to inherit from TreeView—our control merely uses a TreeView to provide the display we require, it is not accurate to say that our control "is a" TreeView.

Suppose we did simply inherit from TreeView. Our control's programming interface would be an extension of TreeView. Every method and property provided by TreeView would also be available on our class—that's what inheritance does. So in the case of our control, what would it mean if a form that uses our hypothetical XmlTreeView were to call Nodes.Add("Another node")? This is allowable on a normal TreeView—it will add a new root node to the tree. And if it is allowable on a TreeView, then by definition it is also allowable on anything that derives from TreeView. But our XmlTreeView is supposed to provide a view of an XML document, and they're not allowed to have more than one root element.

In theory, we could work around this particular semantic mismatch; we could, for instance, override the Nodes property and throw an exception for this specific case, or maybe we could make the root of the document implicit, in which case adding a new node would not be invalid (assuming we aren't trying to enforce conformance with a schema or DTD). But the fact is we probably didn't want to go this route at all. Building a visual tree that represents the structure of an existing document is one thing. Trying to apply any changes made to that tree back to the document is something else entirely. It may not even be possible—we might not be able to save the XML document after having modified it, because not all document sources are writable. In any case, by inheriting from TreeView, we have committed ourselves to providing its public programming interface while preserving whatever our control's semantics are. In doing so, we have almost certainly bought into much more complexity than we really wanted.

So what is the alternative? Reuse through containment is a much better approach here. You can use the TreeView as part of your control's implementation details, but you won't be committing yourself to exposing its API. You can present whatever methods and properties you like, providing just the functionality you require, rather than all the functionality implied by inheriting from some control. The part that often trips people up is that although the TreeView is then a private implementation detail that is invisible to code that uses your control, it remains visible to the end user. As far as the control's visible behavior at runtime goes, it will look exactly like a normal TreeView.

Fortunately, it turns out to be remarkably easy to reuse controls in this way. If you want to create a control that looks and feels to the end user just like one of the built-in controls, but that you wish to wrap in some extra code, UserControl provides an easy way of doing this without using inheritance. Simply create a new composite control in the normal way, use the designer to add an instance of the control you wish to reuse, and set the added control's Dock property to Dock.Fill. This will cause the contained control to be the same size as your composite control (i.e., whatever size the code that uses your control decides to make it). From the end user's point of view, the control will look and behave exactly like the control on which yours is based. But from a software design point of view, the contained control is an internal implementation detail. It will be inaccessible to code that uses your component, which means that you will remain in charge of its behavior.

The UserControl class provides a great way to build reusable pieces of user interface out of other controls. But sometimes the functionality you require just isn't supplied by any of the built-in types and cannot easily be created by combining them. For example, if you are writing a vector drawing program, it would be heroically foolhardy to attempt to build an interactive picture editor from a combination of edit boxes and picture box controls. It would be much better to build a control from scratch, so that you can make it behave exactly as you require.

    [ Team LiB ] Previous Section Next Section