NET User Interfaces in Csharp Windows Forms and Custom Controls [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

NET User Interfaces in Csharp Windows Forms and Custom Controls [Electronic resources] - نسخه متنی

Matthew MacDonald

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید




































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 <System.Diagnostics.DebuggerStepThrough> attribute preceding it, which indicates that this code will be ignored for the purposes of debugging.







/ 142