Encapsulation
Encapsulation is the principle that suggests objects should have separate, carefully outlined responsibilities. Everything they need to fulfill these responsibilities should be wrapped up, hidden from view, and accomplished automatically, where possible. Encapsulation is often identified as a pillar of object-oriented programming, but it has played a part in program design since the invention of software. A properly encapsulated function, for example, performs a discrete well-identified task independently, and has a much better chance of being reused in another application (or even the same program).
The best way to start separating your user interface code is to think more consciously about encapsulation. The custom form class, with its "switchboard" design, is an excellent example of encapsulation at work. However, it also presents a danger. It potentially encourages you to mix in a great amount of additional material through the form's event handlers. A large part of good user interface programming is simply a matter of resisting this urge.
Of course, there are finer points to perfecting encapsulation. Custom controls, which handle some basic user interface operations on their own, are another good example. The following sections present guidelines that can help you keep encapsulation in mind. These techniques are not all immediately obvious. In this book, I'll return to the principle of encapsulation often, and show specific examples with common controls.
Use Enumerations and Resource Classes
User interface controls often require sets of constants, and trying to hard-code them is a tempting trap. Instead, you should create enumerations with meaningful names, and place them in dedicated resource classes. For example, you can define enumerations that help you manage and identify different levels of nodes in a TreeView control (see Chapter 6), distinguish different types of items in a ListView, or just pass information to other methods in your program. Extraneous details like SQL statements should also be strictly confined to distinct resource classes.
Use Collections
Objects are only as good as the way you can access them. On its own, a data object is a group of related information. By using a collection or other classes that contain collections, you can represent the underlying structure of an entire set of complex data, making it easier to share with other parts of your program.
Restrain from Sharing Control References
It's easy to pass control references to helper methods. For example, you can create utility classes that automatically fill common list controls. However, this type of design, where you rely on extraneous classes to perform user interface tasks, can make it extremely difficult to make even simple modifications to the user interface. As a rule of thumb, business code should never rely on the existence of a specific type of user interface control.
Define a Data Transfer Plan
The single greatest challenge when creating a reusable object framework is deciding how to retrieve data and insert it into the corresponding controls, without mingling the business and the presentation logic. To succeed, you may need to tolerate slightly long-winded code. You also need to create a clearly defined plan for transferring data that can be shared by all your forms and controls. In .NET, life just became a lot easier-the DataSet object makes a perfect, nearly universal solution for transferring information.
Use a Central Switchboard
The form acts as a switchboard for all the controls it contains. However, you shouldn't rely on forms for anything more. Instead, you should be able to remove a form, add a new one, or even combine forms without having to rewrite much logic. To accomplish this, forms should always hand off their work to another central switchboard, like an application class. For example, it may be easy to update a record in accordance with a user's selections by creating a new object in the form code and calling a single method. However, if you add another layer of indirection by forcing the form to call a more generic update method in a central application switchboard, your user interface gains a little more independence and gets closer to the ideal of three-tier design. Figure 2-5 shows how this process might work when updating a customer record. The update is triggered in response to a control event. The event handler calls a DoCustomerUpdate() form method, which then calls the required methods in the CustomerDB business object (and creates it if necessary).
Figure 2-5: Using form and application switchboards
Create Data-Driven User Interfaces
As you prepare for a three-tier architecture, it's a good idea to start designing your user interface around the data it manages. This may sound like a slightly old-fashioned concept in today's object-oriented way, but it's actually a good habit to prevent yourself from subconsciously combining user interface and business processing logic.
Think of your user interface as having one "in" and one "out" connection. All the information that flows into your user interface needs to use a single consistent standard. All forms should be able to recognize and process this data. To achieve this, you might want to use data objects that rely on a common interface for providing data. Or, you might want to standardize on .NET's new DataSet object, which can convert information into XML and back seamlessly. The second part of Chapter 9 explores the ways you can tame data in a user interface.
Tip
When is a data-driven interface just another bit of jargon? Probably when you aren't creating an application based on processing, displaying, and managing data. In the business world, the majority of applications deal with databases, and the majority of their work is processing and formatting complex information. For that reason, a great deal of emphasis is placed on how this information is managed and transferred. If, on the other hand, you plan to create the next three-dimensional action game, the rules may change.