Designing with Classes and Tiers

Overview

Some developers resent user interface programming because they believe it's all about painting icons, rewording text, and endlessly tweaking dialog boxes until an entire company agrees that an application looks attractive. Certainly, making a program usable, elegant, and even impressive (or cool) is no trivial task. It can even make the difference between indifferent customers and an enthusiastic audience that's willing to promote your product tirelessly. This kind of excitement about a program's look and feel has driven obscure products like Kai's Power Tools and even Microsoft Windows to great success, and it can't be overlooked. However, developers who are involved in creating and maintaining sophisticated enterprise-level applications realize that there is another set of design considerations for user interface programming. These are considerations about application architecture.

Application architecture determines how a user interface "plugs in" to the rest of an application. Today's development software (like MFC, Visual Basic, and .NET) makes this interaction relatively straightforward and, as a result, developers usually spend little or no time thinking about it. User interface code is usually inserted wherever is most immediately convenient at the time the program is written. This almost always leads to interface code that's tightly bound to a particular problem, scenario, or data source, and heavily interwoven with the rest of the application logic. The interface code might look good on the outside, but it's almost impossible to enhance, reuse, or alter with anything more than trivial changes. To make the jump from this type of scattered user interface coding to a more modern style you have to stop thinking in terms of windows and controls, and start looking at user interface as an entire interrelated framework.

This chapter explains how the principles of object-oriented programming and three-tier architecture apply to user interface design. It identifies the overall concepts that you'll return to again and again throughout the book, including:

The emphasis in this chapter is on general concepts. You'll see some code, but you won't learn about the intricate details like the properties and methods that controls provide. All these details are explored as we delve deeper into controls and user interface coding in the chapters that follow.

 
Chapter 2 - Designing with Classes and Tiers
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Classes and Objects

Today, it's generally accepted that the best way to design applications is by using discrete, reusable components called objects.

A typical .NET program is little more than a large collection of class definitions. When you start the program, your code creates the objects it needs using these class definitions. Your code can also make use of the classes that are defined in other referenced assemblies and in the .NET class library (which is itself just a collection of useful assemblies).

The Roles of Classes

It's important to remember that although all classes are created in the same way in your code, they can serve different logical roles:

  • Classes can model real-world entities. For example, many introductory books teach object-oriented programming using a Customer object or an Invoice object. These objects allow you to manipulate data, and they directly correspond to an actual thing in the real world.
  • Classes can serve as useful programming abstractions. For example, you might use a Rectangle class to store width and height information, a FileBuffer class to represent a line of binary information from a file, or a WinMessage class to hold information about a Windows message. These classes don't need to correspond to tangible objects; they are just a useful way to shuffle around related bits of information and functionality in your code. Arguably, this is the most common type of class.
  • Classes can collect related functions. Some classes are just a collection of static methods that you can use without needing to create an object instance. These helper classes are the equivalent of a library of related functions, and might have names like GraphicsManipulator or FileManagement. In some cases, a helper class is just a sloppy way to organize code and represents a problem that should really be broken down into related objects. In other cases it's a useful way to create a repository of simple routines that can be used in a variety of ways.

Understanding the different roles of classes is crucial to being able to master object-oriented development. When you create a class, you should decide how it fits into your grand development plan, and make sure that you aren't giving it more than one type of role. The more vague a class is, the more it resembles a traditional block of code from a non–object-oriented program.

Classes and Types

The discussion so far has reviewed object-oriented development using two words: classes and objects. Classes are the definitions, or object templates. Objects are "live" classes in action. The basic principle of object-oriented design is that you can put any class to work, and use it to create as many objects as you need.

In the .NET world, however, these terms are a little blurred. The .NET class library is really built out of types, and classes are just one kind of type. To get the most out of this book, you should already know the basics about .NET types and how they can be used. If you need to refresh your memory and get reacquainted with .NET's object family, use the following sections.

Structures

Structures are like classes, but are generally simpler. They tend to have only a few properties (and even fewer important methods). A more important distinction is that structures are value types, while classes are reference types. This means that structures act differently in comparison and assignment operations. If you assign one structure variable to another, .NET copies the contents of the entire structure, not just the reference. Similarly, when you compare structures, you are comparing their contents, not the reference.

structureA = structureB; // structureA has a copy of the contents of structureB. // There are two duplicate structures in memory. if (structureA == structureB) { // This is true as long as the structures have the same content. // Using full binary comparisons like this can slow down performance. }

Some of the structures in the class library include Int32, DateTime, and graphics ingredients like Point, Size, and Rectangle.

Classes

This is the most common type in the class library. All the Windows and Internet controls are full-fledged classes. The word "classes" is sometimes used interchangeably with "types" (or even "objects") because classes are the central ingredients of any object-oriented framework like .NET. Many traditional programming constructs (like collections and arrays) are classes in .NET.

Unlike structures, classes use reference type equality:

objectA = objectB; // objectA and objectB now both point to the same thing. // There is one object, and two ways to access it. if (objectA == objectB) { // This is true if both objectA and objectB point to the same thing. // This is false if they are separate, yet identical objects. }

Occasionally, a class can override this behavior. For example, the String class is a full-featured class in every way, but it overrides equality and assignment operations to work like a simple value type. This tends to be more useful (and intuitive). For example, if a string acted like a reference type it would be harder to validate a password. You would need a special method to iterate through all the characters in the user-supplied text, and compare each one separately. Arrays, on the other hand, behave like traditional objects. If you want to perform a sophisticated comparison or copy operation on an array, you need to iterate through every item in the array and copy or compare it manually.

Delegates

Delegates define the signature of a method. For example, they might indicate that a function has a string return value, and accepts two integer parameters. Using a delegate, you can create a variable that points to a specific method and invoke the method through the delegate.

Here's a sample delegate definition:

// To define a delegate, identify the method's parameters and return type. public delegate string StringProcessFunction(string input);

You can then create a delegate variable based on this definition, and use it to hold a reference to a method:

StringProcessFunction stringProcessor; // This variable can hold a reference to any method with the right signature. // It can be a static method or an instance method. You can then invoke it later. // Here we assume that our code contains a function named CapitalizeString. stringProcessor = new StringProcessFunction(CaptitalizeString); // This invokes the CaptializeString function. string returnValue = stringProcessor("input text");

Besides being an interesting way to implement type-safe function pointers, delegates are also the foundation of .NET's event handling.

Enumerations

Enumerations are simple, static types that allow developers to choose from a list of constants. Behind the scenes, an enumeration is just an ordinary integer where every value has a special meaning as a constant. However, because you can refer to enumerations by name, you don't need to worry about forgetting a hard-coded number, or using an invalid value.

// You define enumerations in a block. public enum SqlQuery { SelectAllOrders, SelectAllCustomers }

Once you have defined an enumeration, you can create enumerations like any other variable. When you assign a value to an enumeration, you use one of the predefined named constants.

// You create an enumeration like an ordinary variable. SqlQuery dBQuery; // You assign and inspect enumerations using an object-like syntax. dbQuery = SqlQuery.SelectAllOrders;

Enumerations are particularly important in user interface programming, which often has specific constants and other information you need to use but shouldn't hard-code.

Interfaces

Interfaces are special contracts that define properties and methods that a class must implement. Interfaces have two main purposes in life. First, they allow polymorphism, which means many different objects that use the same interface can be treated the same way. For example, if you implement an interface called IFileAccess, a client program may not know the specific details about your class, but if it understands IFileAccess it knows enough to use a set of basic file access functionality.

// You can access a supported interface by casting. IFileAccess recognizedObject; recognizedObject = (IFileAccess)MysteryObject; // If IFileAccess supports a method, the object that implements it will too. recognizedObject.OpenFile();

Interfaces are also useful in versioning situations, because they allow you to enhance a component without breaking existing clients. You simply need to add a new interface.

More About Objects

If you haven't had much experience with this type of object-oriented or interface-based programming, I encourage you to start with a book about .NET fundamentals, like A Programmer's Introduction to C# or Programming VB.NET: A Guide for Experienced Programmers, both from Apress. Classes and other types are the basic tools of the trade, and you need to become comfortable with them before you can start to weave them into full-fledged object models and architectures.

User Interface Classes in NET

The first step when considering class design is to examine what rules are hardwired into the .NET framework. Your goal should be to understand how the assumptions and conventions of the .NET platform shape user interface programming. Once you understand the extent of these rules, you will have a better idea about where the rules begin and end and your object designs must take over.

  Tip 

The next section wades through a number of examples, and it uses code with properties and classes you may not have seen before. Remember, all these details reappear and are expanded on in later chapters. The task at hand now is to understand how .NET thinks about control objects, and how these objects interact together.

Controls Are Classes

In the .NET framework, every control is a class. Windows controls are clustered in the System.Windows.Forms namespace. Web controls are divided into three core namespaces, including System.Web.UI, System.Web.UI.HtmlControls, and System.Web.UI.WebControls.

In your code, a control class acts the same as any other class. You can create it, set its properties, and use its methods. The difference is in the lineage. Every Windows control inherits from System.Windows.Forms.Control, and acquires some basic functionality that allows it to paint itself on a window. In fact, even the containing window inherits from the Control base class.

On its own, a control object doesn't do much. The magic happens when it interacts with the Windows Forms engine. The Windows Forms engine handles the Windows operating system messages that change focus or activate a window, and tells controls to paint themselves by calling methods and setting properties. The interesting thing is that although these tasks are performed automatically, they aren't really hidden from you. If you want, you can override methods and fiddle with the low-level details of the controls. You can even tell them to output entirely different content.

So it's clear how you create a control—you just create an instance of a control class, as you would do with any other object. Here's an example that defines a text box:

System.Windows.Forms.TextBox txtUserName = new System.Windows.Forms.TextBox();

You can then set various properties for your control:

txtUserName.Name = "txtUserName"; txtUserName.Location = New System.Drawing.Point(64, 88); txtUserName.Size = New System.Drawing.Size(200, 20); txtUserName.TabIndex = 0; txtUserName.Text = "Enter text here!";

These properties configure some important information, like the size and position of the text box. But none of this actually creates a visible control in a window. How does the Common Language Runtime know whether you are just creating a control to use internally (perhaps to pass to another method) or if you want it to be painted on a specific form and able to receive input from the user?

The answer is in class relations.

Controls Contain Other Controls

The System.Windows.Forms.Control class provides a property called Controls, which exposes a collection of child controls. For example, a Windows Form has a Controls property that contains the first level of contained controls that appear in the window. If you have other container controls on the form, like group boxes, they may have their own child controls.

In other words, controls are linked together by class relations using the Controls collection. Because every control is a special class that derives from System.Windows.Forms.Control, every control supports the ability to contain other controls. (In fact, in Chapter 10 you'll see an example of docking windows that works by placing a form control inside a panel control.)

  Tip 

To be technically accurate, this collection is actually the special .NET type System.Windows.Forms.Control.ControlCollection. This collection is customized to make sure that it can only contain controls, not other types of objects. However, you don't really need to know that to use the collection successfully.

Figure 2-1 shows a sample window, and Figure 2-2 diagrams the relationship of the controls it contains.

Figure 2-1: A sample form

Figure 2-2: Control containment for a sample form

To associate a control with a window, you just need to add it to the form's Controls collection. Like most collection classes, the Controls collection provides some standard methods like Add() and Remove().

For example, the following line of code takes the text box control object and registers it with the form. The text box immediately appears in the window:

frmMain.Controls.Add(txtUserName);

Or, if it you want the text box to be located inside a group box or panel:

// Add the panel to the form. frmMain.Controls.Add(pnlUserInfo); // Add the textbox to the panel. pnlUserInfo.Controls.Add(txtUserName);

The control's location property is automatically interpreted in terms of the parent control. For example, (0, 0) is the top left corner of the container, and (100, 100) is 100 pixels from both the top and left edges.

If you add a control to a form window that already exists, it appears immediately. If, however, the form hasn't been displayed yet, you need to use the form's Show() or ShowDialog() method. Forms are the only controls that know how to display themselves, and they automatically handle the responsibility of coordinating the display of all their contained controls.

frmMain.Show();

A control can be removed from a window by using the Remove() method of the Controls collection. In this case, you need to supply a variable that references the control you want to remove.

// Remove the textbox control. frmMain.pnlUserInfo.Controls.Remove(txtUserName);

All controls, whether they are text boxes, buttons, labels, or something more sophisticated, are added to (and removed from) container controls in the same way. In the next section you see how you use this to your advantage by defining and displaying your custom controls.

  Note 

With Web Forms, the process is similar, but there is never a need to explicitly show a page. HTML isn't a genuine windowing system, and the user is always restricted to seeing one page at a time.You can add a Web control to the current page using the Controls collection of the System.Web.UI.Page class, which is analogous to that of the System.Windows.Forms.Form class. If you want to show another page, however, you need to redirect the user, and let the destination page take over the processing. I won't talk about web interfaces any further in this book, because they have dramatically different needs than Windows applications.

Controls Derive from Other Controls

In Moving to VB .NET, Dan Appleman suggests that inheritance is an over-hyped feature with a few specific uses, but a host of potential problems and considerations. In his words, inheritance is the "coolest feature you'll never use," and many object-oriented gurus would be quick to agree. While inheritance can be useful when creating your business and data objects, it's generally not the best approach, and never the only one.

In the world of controls, however, inheritance just might be the single most useful feature you'll discover. Essentially, inheritance allows you to acquire a set of specific functionality for free. You don't need to worry about how to handle the messy infrastructure code for what you want to do. Instead, you simply inherit from a class in the .NET library, add a few business-specific features, and throw it into your program.

This approach can be used to create customized controls quickly and easily. Below is the definition for a custom text box. It has all the powerful features of a text box, manages its appearance automatically, provides sophisticated user editing capability, and takes care of basic details like painting itself and managing focus. In addition, the custom text box adds two new features. It has a property that returns the total number of letters in the text string (NumberOfLetters), and a method that quickly trims off extra dashes (TrimDashes). To provide this functionality, it uses some standard .NET tricks to iterate through a string, and it makes use of the Trim() method that's built into the string object.

public class CustomTextBox : System.Windows.Forms.TextBox { public int NumberOfLetters { get { int letters = 0; IEnumerator enumerator = Text.GetEnumerator(); while (enumerator.MoveNext()) { if (char.IsLetter((char)enumerator.Current)) { letters += 1; } } return letters; } } public void TrimDashes() { this.Text.Trim(char.Parse("-")); } }

You can use this class in exactly the same way that you would use a class from the .NET library:

CustomControlProject.CustomTextBox txtUserName; txtUserName = new CustomControlProject.CustomTextBox(); txtUserName.Name = "txtUserName"; txtUserName.Location = new System.Drawing.Point(64, 88); txtUserName.Size = new System.Drawing.Size(200, 20); txtUserName.TabIndex = 0; txtUserName.Text = "Enter text in the custom textbox here!"; frmMain.Controls.Add(txtUserName);

The interesting part of this example is not what's in the code, but what is left out. Clearly, there are a lot of Windows-specific details that you don't need to worry about when using inheritance to create a custom control. You also don't need to create separate ActiveX components (and countless versioning headaches). Custom controls in .NET are painless and powerful.

8 examine a variety of custom control programming techniques, and show you how to license, distribute, and manage them in the development environment. Custom control examples also reappear throughout the book. You'll use them to:

  • Solve control synchronization problems
  • Automate control validation
  • Rigorously organize code
  • Preinitialize complex controls
  • Tailor controls to specific types of data, even replacing basic members with more useful higher-level events and properties

Creating custom controls is a key way of playing with .NET, and one of the most important themes of this book.

Inheritance and the Form Class

Inheritance isn't just used when you want to extend an existing class with additional features. It's also used to gain access to important parts of functionality in .NET. One of the best examples is the System.Windows.Form class.

In a Windows application, you could create an instance of a System.Windows.Form and manually go about adding controls and attaching events. However, if you are creating your project in Visual Studio .NET, it defaults to a more structured approach.

When you start designing a new window in the IDE, Visual Studio .NET automatically creates a customized class that inherits from the Form class. This class encapsulates all the logic for adding child controls, setting their properties, and responding to their events in one neat package. It also provides you with an easy way to create identical copies of a form, which is particularly useful in documentbased applications.

Below is a simplified example of a custom form class that contains a simple constructor method. When the form is instantiated in a program, it automatically creates and configures a text box, and then adds it to the form.

public class MainForm : System.Windows.Forms.Form { private System.Windows.Forms.TextBox txtUserName; public MainForm { txtUserName = new System.Windows.Forms.TextBox; txtUserName.Name = "txtUserName"; txtUserName.Location = new System.Drawing.Point(64, 88); txtUserName.Size = new System.Drawing.Size(200, 20); txtUserName.TabIndex = 0; txtUserName.Text = "Enter text here!"; this.Controls.Add(txtUserName); } }

  Note 

Note that the keyword this (or Me in VB .NET) is used to access the current form instance. This allows you to write generic code that can be applied to any instance directly inside the form class.

The custom form class automatically gains all the features of a standard Windows.Forms.Form object, including the ability to display itself with the Show() and ShowDialog() methods.

// Create the form (at this point, its constructor code will run and add // the textbox control). MainForm frmCustomForm = new MainForm(); // Show the form. frmCustomForm.Show();

Notice how the only control MainForm contains (a text box) is referenced with a member variable, so that it can be easily accessed in your code. This means that once the form has been created, there are really two different ways to access the text box. The simplest way is to use the form-level member variable:

frmCustomForm.txtUserName.Text = "John";

It's up to you whether you want to make the member variable accessible to other classes in your program. By default, all control variables in C# are private, so they aren't available to other classes. In Visual Basic .NET projects, all controls are declared with the Friend keyword, and any other class can access them as long as it exists in the current project. This is similar to the way that previous versions of VB worked. Either way, the difference is minor. Generally, Visual Studio .NET tries to discourage you from breaking encapsulation and fiddling with the user interface of a form from another class. However, there is always one open back door. No matter what the language, you can always access controls directly through the form's Controls collection.

The Controls Collection

Generally, the member variables allow flat access to the controls on a form. All the controls on the form have corresponding member variables. On the other hand, only the first level of controls appears in the Controls collection. Controls that are inside container controls like group boxes, tab controls, or panels, will appear in the Controls collection of the control that contains them (as diagrammed in Figure 2-2).

Unfortunately, controls are only indexed by number in the Controls collection. That means the best way to find a control that you need is to iterate through the entire collection and examine each control one by one, until you find a match. You can look for a specific type of control, or a specifically-named control. For example, when a control is created in Visual Studio .NET, it is automatically given a Name property that matches the name used for the member variable.

txtUserName.Name = "txtUserName";

This is just a convenience—you are not forced to set the name property. However, it allows you to easily look up the control by iterating through the Control collection:

// Search for and remove a control with a specific name. foreach (Control ctrl in frmCustomForm.Controls) { if (ctrl.Name == "txtUserName") { frmCustomForm.Controls.Remove(ctrl); } }

  Note 

The Controls collection is always accessible to other forms. However, you shouldn't use this as a back door to allow one form to modify another. For one thing, using a string to identify the name of a control is extremely fragile—if the original form is changed, the code may stop working, but it won't raise a helpful design-time or compile-time error. If forms need to interact, they should do so indirectly. For example, one form should call a method in an application class that then calls the appropriate method or sets the appropriate property in another form.You'll see some of these issues in Chapter 10.

Generating Code with Visual Studio NET

So far you've looked at the code to create control objects dynamically (a topic explored in much more detail in Chapter 11). When you use Visual Studio .NET to create code at design-time, the story is a little different—or is it?

When you create a Windows application in Visual Studio .NET, the IDE creates a customized form class. As you add, position, and configure controls in the design-time environment, Visual Studio .NET adds the corresponding code to a special region of the Form class, inside a method called InitializeComponent(). The form's constructor calls the InitializeComponent() method—meaning that the generated code is automatically executed every time you create an instance of your Form class (even before the form is displayed). A sample (commented and slightly shortened) Form class with an InitializeComponent() method is shown below. It configures the window shown in Figure 2-1.

public class TestForm : System.Windows.Forms.Form { // Form level control variables. // They provide the easiest way to access a control on the window. System.Windows.Forms.GroupBox groupBox1; System.Windows.Forms.Button button1; System.Windows.Forms.RadioButton radioButton1; System.Windows.Forms.RadioButton radioButton2; public TestForm { // Add and configure the controls. InitializeComponent(); } private void InitializeComponent() { // Create all the controls. groupBox1 = new System.Windows.Forms.GroupBox(); button1 = new System.Windows.Forms.Button(); radioButton1 = new System.Windows.Forms.RadioButton(); radioButton2 = new System.Windows.Forms.RadioButton(); // This is our way of telling the controls not to update their layout // because a batch of changes are being made at once. this.groupBox1.SuspendLayout(); this.SuspendLayout(); // (Set all the properties for all our controls here.) // (Configure the form properties here.) // Add the radio buttons to the GroupBox. this.groupBox1.Controls.Add(this.radioButton1); this.groupBox1.Controls.Add(this.radioButton2); // Add the button and group box controls to the form. this.Controls.Add(this.button1); this.Controls.Add(this.groupBox1); // Now it's back to life as usual. this.groupBox1.ResumeLayout(false); this.ResumeLayout(false); } }

The upshot is that a form and its controls are always created and configured through code, even when you design it with the IDE. The only difference between the theoretical code examples in this chapter and designed code is that the latter uses a dedicated InitializeComponent() method for better organization.

  Tip 

If you look at this code in a form you've created in Visual Studio .NET, you'll notice a couple of changes from the code listing I've shown. First, controls are defined and then created in two separate steps (and the creation takes place in the InitializeComponent() method). Second, controls are added all at once using the Controls.AddRange() method, which accepts an array of control objects, and saves a few lines of code at the expense of readability. Finally, the InitializeComponent() method has a special attribute preceding it, which indicates that this code will be ignored for the purposes of debugging.

 
Chapter 2 - Designing with Classes and Tiers
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Interacting with a Control

One interesting and little-known fact about .NET controls is that they provide two different forms of interaction. The first, and less common one, is by creating custom classes and overriding methods.

For example, imagine you have a text box that's designed for numeric entry, and you want to examine every key press to make sure that it corresponds to a number, and not a letter. To perform this type of task, you can create a customized text box, and override the OnKeyPress() method to add this extra verification logic.

public class NumericTextBox : System.Windows.Forms.TextBox { protected override void OnKeyPress(KeyPressEventArgs e) { base.OnKeyPress(e); if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar)) { e.Handled = true; } } }

The OnKeyPress() method is invoked automatically by the Windows Forms engine when a key is pressed in a TextBox control. The overridden method in the example above checks to see if the entered character is a number. If it isn't, the Handled flag is set to true, which cancels all further processing, effectively making sure that the character will never end up in the text box.

This design pattern is useful if you use a number of controls with extremely similar behavior. If, on the other hand, you need to fine-tune behavior for distinct, even unique tasks, this approach is extremely poor. For example, consider a button control. You could react to a button click by creating a special class for every button on your application, and giving each button its own overridden OnClick() method. While your program would still be well encapsulated, it would quickly become completely disorganized, swamped by layers of button classes that have little to do with one another.

The View Mediator Pattern

The approach that .NET encourages circumvents this problem by using events. When you create a form in Visual Studio .NET, a custom class is derived for the form, but all the contained classes are used as is. The event handling logic is inserted as a series of special methods, which are added to the custom form class.

In other words, every form acts as a giant switchboard for all the controls it contains. This type of design pattern, which is so natural to .NET and most Windows development that you might not have even noticed it, is called the View-Mediator pattern. It dictates that one central class organizes each individual window.

Here's the same text box example you saw earlier, rewritten as form-level event handler. The event handler is hooked up in the constructor for the form (although Visual Studio .NET would perform the same task with a dedicated InitializeComponent() method).

public class MainForm : System.Windows.Forms.Form { System.Windows.Forms.TextBox txtUserName; public MainForm() { txtUserName = new System.Windows.Forms.TextBox(); txtUserName.Name = "txtUserName"; txtUserName.Location = New System.Drawing.Point(64, 88); txtUserName.Size = New System.Drawing.Size(200, 20); txtUserName.TabIndex = 1; txtUserName.Text = "Enter text here!"; this.Controls.Add(txtUserName); // Connect event handler. this.textBox1.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.textBox1_KeyPress); } private void textBox1_KeyPress(object sender, System.Windows.Forms.KeyPressEventArgs e) { if (!char.IsControl(e.KeyChar) && !char.IsDigit(e.KeyChar)) { e.Handled = true; } } }

You'll notice that the actual logic for processing the key press is identical, but the way it's integrated into the application is completely different. The form is now responsible for the validation, not the control itself. This is an ideal approach if the form needs to handle the complex validation of multiple different controls. It's a less suitable approach if you need to perform the same type of validation for the same control in different windows, because you'll probably need to copy the code into multiple form-level event handlers.

You should also remember that the point of a switchboard is to route calls to a new destination. In other words, when you create the event handler for a button's Click event, this event handler usually has two purposes:

  • Forward the command to another object that can handle the task.
  • Update the display.

Depending on the button, only one of these tasks may be necessary. But the important concept you should realize is that an event handler is generally part of a user interface object-the form switchboard. It's meant to handle user interface tasks, and delegate more complicated operations to other objects.

Smart Controls

So far you have seen two distinct ways to use controls from the .NET class library:

  • Create an instance of a generic control class "as is." Then, configure its properties.
  • Define a new class that inherits from a generic control class, and customize this class for your needs. Then, create an object based on this specialized class.

The difference is shown in Figure 2-3.

Figure 2-3: Two ways to interact with controls

Visual Studio .NET uses inheritance (the first method) when you create forms. When you configure controls, however, it inserts them as is, and adds the appropriate logic for modifying their properties (the second method). This is the default approach in .NET, but it is not the only approach.

When Visual Studio .NET adds controls and derives a custom form class, it is making a design decision for you. This decision helps clear out the clutter that would result from creating dozens of custom control classes. However, like all design decisions, it's not always right for all people and in all situations. For example, if you use numerous similar controls (like text boxes that refuse numeric input), you may find yourself duplicating the same code in event handlers all over your program. In this case, you might be better off to step beyond Visual Studio .NET's default behavior, and create customized controls with some additional intelligence.

When you are creating a new application and planning how to program its user interface, one of the most important tasks is deciding where to draw the line between smart controls (custom control classes) and smart switchboards (custom forms with event handling logic). A good decision can save a lot of repetitive work. 9 consider some examples that show how you can add a bit of sense to important controls like the TreeView and ListView. Hopefully, you'll realize that custom controls are not just for redistributing neat user interface elements, but also for building intelligence into parts of a large application, and helping to reduce repetition and enforce consistency across different modules.

  Note 

This book includes a special chapter about custom controls, which describes how to package them for redistribution, and covers some advanced options. However, unlike most .NET books, I also explore custom control classes throughout. Creating these smart controls is a crucial ingredient in designing a user interface framework, not just an additional topic.

Smart Forms

As explained earlier, every form class in your application is a custom class that derives from System.Windows.Forms. However, you can use multiple layers of form inheritance to centralize and reuse important form functionality. This topic is commonly referred to as visual inheritance (although it's no different than any other type of control class inheritance), and it's described in detail in Figure 2-4 diagrams this relationship.

Figure 2-4: Ordinary forms and visual inheritance

So far, you've explored the basic object-oriented foundation of .NET user interface programming. The remainder of the chapter considers some higher-level architectural topics like encapsulation and three-tier design. These are the best practices you'll want to keep in mind when planning your user interface classes.

 
Chapter 2 - Designing with Classes and Tiers
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

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.

 
Chapter 2 - Designing with Classes and Tiers
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Developing in Tiers

The basic principle of three-tier design is simple. An application is divided into three distinct subsystems. Every class belongs to only one of these three partitions, and performs just one kind of task. The three tiers are usually identified as:

  • A presentation layer, which converts a user's actions into tasks and outputs data using the appropriate controls.
  • A business layer, where all the calculations and processing specific to the individual business are carried.
  • An underlying data layer, which shuttles information back and forth from the database to the business objects.

An object in one tier can only interact with the adjacent tiers, as shown in Figure 2-6.

Figure 2-6: Three-tier design

Almost everyone agrees that this sort of structure is the best way to organize an application, but it's not always easy to implement this design. Though the schematic looks simple, modern user interfaces are usually quite complicated, and sometimes make assumptions or have expectations about the way they will receive information. The result is that everyone recommends this model, but very few developers successfully follow it.

Problems with Three Tier Design

Before you can solve the problems of three-tier design, you have to understand what these problems are. The problems, although not insurmountable, are found in every tier.

The presentation tier

Though it doesn't explicitly state it, three-tier design really requires a fair degree of consistency among user interface controls. In the real world, this consistency doesn't exist. For example, making what is conceptually a minor change-like substituting a ListView control for a DataGrid-requires a totally different access model. DataGrids are filled exclusively by data binding. ListViews, on the other hand, act like a collection of items. To get information into other ListView columns, you have to add a collection of fields to each individual item. These quirks are easy enough to master (and you sort through them in Chapter 6), but they don't make it possible to create business objects that can quickly and efficiently fill common controls.

The business tier

In three-tier design, it's assumed that the user interface is isolated from the underlying data source. Information for a control is requested through a layer of business objects. This can be a problem if you create business objects that try to model real-world entities. For example, if you need to retrieve a list of customers and some summary information based on all customer orders, you would be crazy to try and work with individual Customer and Order objects. Consider the process:

  1. The user interface layer requests some information from the business layer.
  2. The business layer creates hundreds of Customer and Order objects by retrieving information from the data layer. These objects are organized into large collections. Each property for each object is set according to the corresponding field in the corresponding row.
  3. The user interface layer iterates through all these objects, withdrawing and calculating the relatively simple information it needs to actually display the required rows. Each field is extracted separately as an individual property, which requires a separate line of code. The code is very readable, but far from elegant or efficient.

Some three-tier designs get around this by creating a very thin business layer that might just consist of a pile of utility functions for retrieving rows as arrays. But is this really a good object-oriented design?

The data tier

Keeping the data tier separate from the business tier is another battle. To optimize performance, databases in enterprise applications usually rely on stored procedures, views, and other optimized ways to retrieve and update data. However, the user interface tier can't be built in a database-friendly way, because it is designed to be completely generic. It also can't rely on tricks that programmers' love, like dynamically generated SQL statements, because it is supposed to be completely isolated from the data tier. The result is a tradeoff, where you can favor any one of the following approaches:

  • Create a "thin" business layer that uses methods that correspond very closely to stored procedures and other database-specific parameters. In fact, some programs use business objects that are just thin wrappers on top of a live cursor-based database connection (like an ADO recordset). Unfortunately, this business layer requires significant rework if the database changes.
  • Create an average business layer that lets the user interface retrieve whatever data it wants. The business tier relies on accessing the database using generic SQL statements. It's very expandable and generic, but database performance will be terrible.
  • Create a "thick" business layer that tries to match requests from the user interface with an optimized execution path for a specific database. With a little luck and careful coding, performance could be as good as the first option, and the layer could be nearly as generic as the second. However, writing this tier is a major programming undertaking that takes exponentially more time.

Three Tier Design Consistency

There's nothing explicitly wrong with three-tier design, but it isn't a magic solution. For one thing, it's a rather vague recommendation that has more guidelines than requirements. Developers can interpret it any way they want.

For example, consider an application that reads customer information from a database and displays it in an attractive list control. At first glance, it seems like a straightforward task. But consider the number of different ways it could be modeled with objects:

  • A CustomerData class fetches information from the database, and returns it as a DataSet. Your code then manually reads the DataSet and adds the information to a list control.
  • A CustomerData class fetches information from the database. You also create a customized CustomerList control class that knows how to fill itself using the DataSet it receives from CustomerData.
  • A CustomerData class fetches information from the database. However, the CustomerData class also receives a reference to the list control that needs to be filled. The CustomerData class has the built-in smarts to know how to fill the list control's collection of items.
  • A CustomerData class fetches information from the database. A special helper class, FillListFromDataSet handles the conversion of the information in the DataSet to information in the generic list control.

Which approach is the best one? It's difficult to say. The third option is somewhat suspicious, because it seems that the CustomerData class is being given additional responsibilities beyond the scope it was designed for. Some of the other scenarios make assumptions about the appropriate way to exchange data. But the greatest problem with all of these examples is that there is no guarantee that the other classes in the application will follow this pattern.

  Tip 

An object-oriented framework sets out rules that determine how objects will interact and communicate.When creating a user interface, you have to develop your framework at the same time that you plan your individual objects.

Fixing Three Tier Design

By now you have probably realized that the most important decision you can make is to define how your user interface objects should interact. This is the simplest way to improve your designs without adopting a single specific solution.

To get a better idea about how you might apply three-tier design in the .NET world, consider the simple application below. It uses a TreeView that displays a list of product categories drawn from a database. When you expand a category node, you see all the relevant products. When a record is selected, the corresponding information appears on a list on the left (see Figure 2-7).

Figure 2-7: An intelligent TreeView database browser

There are several important considerations:

  • The whole tree can't necessarily be filled at once. (Whether it actually should depends on the size of the database.) The solution needs the flexibility to be able to fill parts of the trees "just in time," and potentially make multiple trips to the database. An even better solution might make use of threading to asynchronously fetch and add results, or create some kind of paged list.
  • The component that returns the data can't assume that the information is destined for a TreeView-the design may change, or the same information may be reused in multiple locations.
  • The TreeView needs to be able to accept the data in an easily interpretable format. It shouldn't need to recognize specific database tables and fields by their "real" names.

Obviously, there are different ways this application can retrieve information from the database. It could use custom data objects, but that would require category and product objects that use multiple property procedures. The user interface code would then have to retrieve these properties specifically by name, and decide on an appropriate order. For some applications, this approach would work, but in this case it would choke a relatively simple browser with extra classes and code.

Using the DataSet

Instead, information in our example is sent in a neat package called the DataSet. The DataSet is found in the System.Data class, and is often referred to as a part of Microsoft's ADO.NET technology. Unlike the objects in earlier ADO, the DataSet is an ideal solution for information transfer in an application.

  • It's much more generic. All .NET languages support the DataSet.
  • It's inherently portable across language and platform boundaries, because it allows you to quickly swap information into XML (which is another great way to shuffle information around in a program).
  • It's inherently disconnected. Unlike some business objects created in ADO, that are really just Rowset objects with hidden cursors, the DataSet is a discrete copy of information, which makes it easy to transfer and even hold for as long as you want.
  • The DataSet loosely mirrors a database. Your business layer can easily add column mapping and perform other tricks, making sure the user interface layer gets exactly what it expects, regardless of the underlying data source.
  • The DataSet can contain several tables at once. It can contain data relationships, which makes it easy to navigate through the data, but these relationships are independent of whatever relationships exist in the actual data source.
  • It has update possibilities. If you want to hold onto the DataSet, you can make use of built-in methods to withdraw the changed rows and resubmit them to the appropriate business object.

In other words, the DataSet removes some of the headaches of three-tier design. It allows a way to transfer information that's much more lightweight than dedicated classes, but not necessarily tied to a specific database. You can browse through its rows and fields as collections, which makes it easy to add information to list-like controls without needing to look for specific field names. Of course, when you want to start formatting column widths and setting column orders, your user interface may need to know a fair amount of information about the data. As long as you place this information in a central repository (generally a resource class with static members), your user interface will be extensible and easy to change.

  Note 

Where's the code? In this chapter, the emphasis is on concepts. If you really need to see the specific implementation details-which use a combination of data embedding, a dedicated data class, and the ADO.NET data objects-skip ahead to Chapter 9, which shows this code and several other data-enabled user interface controls.

Validation and Business Objects

Before continuing, allow me to debunk one myth about business objects. It's sometimes argued that business objects are an ideal way to store and transfer data because they can provide integrated error checking. In all honesty, the value of this error checking is limited. First, it's duplicating information in the database, which adds extra overhead for writing the code. More important, the error checking happens when the value is assigned, which is too late for it to be useful in the actual user interface. As you learned in the last chapter, the best thing you can do for users is to restrict their ability to make mistakes. That means you have to act on an error as soon as it happens, or better yet, forbid it entirely.

No matter what, your user interface has to be designed with some business rules built-in (for example, forbidding letters in a text box that represents an invoice amount). Your data source performs the final error checking. There's not much use in adding new layers of error checking between these two (except to verify that your client code is behaving correctly). Even the best three-tier design can't escape the need to import business rules into the interface.

Other Types of Application

The TreeView DataSet example above is a viable solution for many traditional database applications, which allow information to be reviewed, analyzed, and modified. Other types of applications may need other allowances.

  • For example, a three-dimensional action game breaks down the barrier between the user interface and the "business" layer. Every object in the game is tightly bound to its on-screen appearance, and also makes use of a slew of programming algorithms to calculate how it moves and what it does. Object communication for this type of application is completely different.
  • As another example, consider a live running tutorial. This kind of application might lend itself most easily to three-tier design. The information in a file is completely processed and delivered in a business object. The user interface is consistent, probably using a single window with several custom classes and a main controller. The controller retrieves information from the business objects, and delivers it to the appropriate user interface class, which handles the display.
  • Or consider document-based applications. These applications are also easier to implement as three-tier designs, because they are based around a well-defined view that allows you to modify a single underlying data object. There just isn't the variety of information and ways of interacting with it that makes many database or hybrid applications more difficult to program and organize.
 
Chapter 2 - Designing with Classes and Tiers
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

The Last Word

Three-tier design is an abstraction. No successful application will implement it exactly. However, it's also a powerful guideline that helps you shape how classes interact in your application. It may seem strange to start talking about DataSets and business objects in a book on user interface design. (In fact, there are other excellent .NET books written entirely about data access.) But as you'll see, when you set specific rules about how the user interface tier can communicate with other parts of your program, you start to make the transition from a simple collection of objects to a true user interface framework.

The following chapters of this book focus on mastering the tools of .NET user interfaces. Considerations for design and user interface architecture crop up periodically, but they take a back seat to the amazing new programming model of .NET. This chapter was designed to give you the background in application architecture that you need to understand this "other layer." If you keep the considerations of this chapter in mind as you continue reading, you'll discover an entirely new and richer way to program user interfaces.

This chapter also introduced you to the broad picture of user interface in the .NET world, and the basic design assumptions that Visual Studio .NET makes automatically. These decisions can be altered, and the .NET framework itself allows you a considerable amount of freedom to create the exact framework that you want. In later chapters you'll learn how to exploit this freedom to create all types of custom controls.

 
Chapter 3 - Control Class Basics
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Control Class Basics

Категории