Visual CSharp .NET Developer's Handbook (2002) [eng]
.pdfGDI+
Anyone who's developed graphics applications under Windows knows that it's an error-prone process. You need to gain access to a device context, allocate memory, and remember to give it all up when you're finished. The Graphical Device Interface (GDI) calls are especially troublesome because they seldom follow the same syntax from call to call. GDI+ rids the developer of these problems by enabling access to the graphical environment through managed programming techniques.
Don't get the idea that GDI+ is a complete solution. Microsoft still has a lot of problems to iron out in this area. The biggest problem is the lack of support for anything but 2D graphics primitives at this point. Many developers, even those that work primarily with business software, require 3D graphics today. Business users need presentations with pizzazz to garner the level of interest needed to win new accounts or develop interest in a new idea. The lack of 3D graphics is a major concern.
Another problem is that GDI+ is slow compared to GDI and even worse when compared to DirectX. The reason that Microsoft developed DirectX is that developers complained the current graphics environment was too slow. The need for high-speed graphics doesn't disappear simply because the developer begins to use a new programming environment.
These two problems are just the tip of the iceberg, but they are significant problems that most developers will encounter when using GDI+. The choice you have to make is one of ease-of- use versus capability and execution speed. GDI+ removes the source of many bugs, but the cost is high. If you're working with relatively simple graphics and don't require 3D, the choice is relatively easy to make. GDI+ has a lot to offer to the low-end graphics developer.
An Overview of the Visual Studio.NET Designers
Visual Studio .NET includes a designer feature. A designer is a new name for something that's existed since the first versions of Visual Studio. It's the visual part of the IDE that enables you to create the user interface for your application, using what amounts to dragging and dropping controls onto a form. However, the support for visual development in Visual Studio .NET has changed.
The new designer support uses a componetized approach that enables you to use more than one interface for creating the user interface for your application. The idea is that you can use the set of tools that best match the requirements of the user interface. A web page has different design requirements than a desktop application does, so it makes sense to use an interface with the web environment in mind. In addition, this approach enables third-party developers to create additional designers for specific situations.
The three designers that come with Visual Studio .NET include one for Web (HTML) design, a second for Windows design, and a third for working with XML schemas. All three designers look approximately the same and work the same, but the type of display they create is different. When you work with the Windows designer, you'll create a form for a desktop application. The HTML designer creates a web page. The XML schema designer shows the relationship between XML entries including elements, attributes, and types.
The Windows designer contains a single tab because it doesn't have to hide any details from the viewer. The HTML designer contains two tabs, one labeled Design for the graphic view of the web page and a second labeled HTML that enables you to view the resulting HTML code. Likewise, the XML schema designer contains a Schema tab (graphical representation) and an XML tab (actual output).
You'll also find that you populate the forms for the three designers using the controls on the Toolbox or the data from the Server Explorer. We'll discuss both of these elements more as the book progresses and you create more applications. Visual Studio .NET provides methods for customizing both the Toolbox and Server Explorer, which means that you have a great deal of flexibility in the appearance of these two tabs. The tabs slide out of view when you don't need them so you have better access to the design area.
Note The Web or Windows designer might not appear when you first open an application. To open a designer, highlight the appropriate form in Class View, then use the View → Designer command to display the designer on screen. The same command works for both types of forms. Visual Studio .NET automatically chooses the correct designer for your needs.
The HTML and Windows designers begin in grid view. Most developers find the grid helps them design forms quickly, but some find the grid actually gets in the way and obscures their view of the control layout. You can also turn on the Snap to Grid feature. Whenever you move a control, it snaps to the grid. While the Snap to Grid feature does enable you to create neat forms quickly, it does hinder precise placement of controls on screen. You can override the current designer settings by opening the Options dialog using the Tools → Options command, and then choosing the correct designer entry.
Where Do You Go From Here?
This chapter has helped you discover more about .NET. We've spent time looking at both the
.NET Framework and CLR, both of which affect Windows development in profound ways. You've also learned about the various interface support mechanisms.
One of the things you can do to improve your .NET experience is to spend some time looking at the various .NET Framework namespaces. This is a new technology and you might not always find classes you need in the obvious places. In addition, learning about the .NET Framework helps you design your own namespaces-a requirement for projects of any complexity.
Chapter 3 will begin the process of creating complex applications. We'll discuss classes in detail. Of course, this includes a few class basics as well as programming considerations such as non-deterministic finalization (the fact that you can't determine in advance when C# will release a resource). You'll learn about such programming features as delegates and attributes. We'll create custom attributes, and you'll learn how to use reflection.
Chapter 3: A Quick View of Classes
Overview
C# is a true object-oriented language; we've seen many examples of this orientation in the book so far. As a result, classes are an especially important part of working with C#. You must create classes that the system uses to create objects. A class is simply a description of how to build an object. As shown in Chapter 2, C# treats all entities as values or references, both of which can appear in the form of objects.
This chapter discusses classes from a C# perspective. Experienced developers will find a few changes from Visual C++ or Visual Basic. The lack of global variables makes a difference in the way you design C# classes. You must pass every piece of data between classes through properties, fields, or structures. There's no concept of global data in C#, so you have to design classes to provide robust application support.
We'll also discuss the class life cycle. Garbage collection (along with other .NET Framework features) changes class construction techniques. You'll find that you don't have to implement certain class components such as a destructor. Working with C# also means learning to deal with problems such as nondeterministic finalization (an inability to determine when the system destroys an object).
Class Basics
Classes encapsulate code and the data the code manipulates. It's important to understand that the data and the code are never separate in C#. The C# approach means leaving behind some coding techniques you might have used in the past. However, you'll also find that the C# approach reduces the complexity of many coding scenarios, makes bugs less likely, and makes your code easier to read.
C# extends the idea of modularity and organization by extending it to the application as a whole. A C# application uses hierarchical design to an extreme. In fact, the Visual Studio IDE is designed to accommodate this feature so that you can view your application from several different "levels" using IDE (features we'll discuss as the section progresses). The idea is that you can view your application in an outline mode that makes design flaws easier to see and fix. More importantly, you can move from one area of the application to another with greater ease by viewing the code at a higher level.
Class construction means knowing how to use existing code, providing safe code access, and creating complex classes with the least amount of code. As part of creating classes, you need to know about working with base classes, delegates, and attributes.
Base classes The concept of the base class is universal to all languages with object-oriented design capabilities. In many ways, this is the most generic of the C# class creation features. When creating a new class, you can inherit the capabilities of a base class. Depending on the construction of the base class, you can customize class behavior by overriding base class methods.
Delegates Delegates are simply a new way to handle function pointers. However, the term delegate isn't simply a replacement for function pointer. You'll find that delegates have differences from function pointers that make class creation simultaneously easier and more difficult. The delegate object encapsulates a reference to a method within a class.
Attributes Attributes are a means to augment the capabilities of your class. You can use attributes for a number of purposes and even create your own attributes to meet special needs. We'll discuss attributes throughout the book, but this chapter will introduce the attributes that you'll use on a regular basis.
The following sections will lead you through the process of creating simple (example) classes. We'll begin with the easiest console application class you can create and move onto examples that use additional C# features. The idea of this section is to show you how classes work in the C# world. We won't build complex applications or even delve very far into Windows applications. All of the examples rely on the console or are simple Windows application types.
Application Structure
Let's begin by looking at the structure of a C# application. Every C# application contains certain elements. The application begins with the hierarchical levels, listed here in order of appearance.
1.Namespace
2.Class
3.Method
Every application begins with a namespace that has the same name as the project. Of course, you can change the namespace to anything you like in order to maintain compatibility with other projects. We'll discuss the implications of namespaces in the "Working with Namespaces" section of the chapter.
Below the namespace level is the class level. The name of this class varies. If you create a console application, then C# assigns the class a name of Class1; Windows applications begin with a class name of Form1.
Classes normally contain one or more methods. Every C# application has at least one method in it called Main(). This is the method that C# uses as a starting point for execution. Every C# application has to have an entry point, and some types of C# projects often have more than one. For example, a DLL commonly has more than one entry point. Figure 3.1 shows a basic C# application.
Figure 3.1: Every C# application begins with the same hierarchical structure.
Sometimes it's hard to see the structure of your application. Even the basic Windows application contains a lot of "boilerplate" or default code. Look at Figure 3.1 again and you'll notice some minus signs next to each major code area. Click the minus sign, and you'll see that section contract. Using this technique enables you to see the structure of an application with greater ease. Figure 3.2 shows an example of a typical Windows application.
Figure 3.2: You can use the IDE to help you see the structure of an application.
As you can see from Figure 3.2, a Windows application has the same elements as a console application. It contains a namespace, class, and at least one method. In this case, Main() doesn't accept parameters, but you can change this behavior. Main() does call on the application to create an instance of Form1 as part of the startup process. A Windows application also includes a Form() method to display the graphical interface and a Dispose() method for releasing resources.
Figure 3.2 also shows the effect of using the IDE to display the application hierarchy. The ellipses next to each entry tell you that the application contains more code; you just can't see it now. Clicking the plus sign reveals the code for that area of the application.
Working with Parameters
It's not uncommon for class constructors to accept input parameters that configure the class in some way. In fact, many classes provide several overrides that enable the developer to select the most convenient method for creating the class. For example, a constructor might provide overrides that accept no parameters, a string, and an integer for the same class. Some overrides enable the developer to choose which optional parameters to provide with the constructor call.
We'll explore general class parameters in more detail in several sections of this chapter and throughout the book. For example, the "Working with the Out and Ref Keywords" section describes how you can pass values by reference. You'll also learn some parameter passing techniques in the "Understanding Methods, Properties, and Events" section.
This section of the book examines a special type of parameter passing-the type that occurs when you start an application. Many applications require input parameters (the type entered at the command line). For example, you can start Notepad with input parameters that automatically load a file or print a selected file. The use of parameters enables an application to automate certain tasks and make it user friendly. In some cases, you must include parameter support to enable your application to work with Windows utilities such as the Task Scheduler.
Adding command-line parameter support to your application isn't difficult. All you need to do is change the Main() method to accept the parameters, then pass the parameters to the initial class for your application. Listing 3.1 shows the essential parts of the Parameters example found in the \Chapter 03\Parameters folder on the CD. Note that this is just one of several methods for handling command-line parameters.
Listing 3.1: One Method of Handling Command-Line Parameters
static void Main(string[] args)
{
string []NoArg = {"No Argument Supplied"};
// Determine the correct string to pass to the constructor. if (args.Length > 0)
Application.Run(new Param(args)); else
Application.Run(new Param(NoArg));
}
public Param(string[] InputValue)
{
//Required for Windows Form Designer support InitializeComponent();
//Process the input parameters one at a time. foreach (string Argument in InputValue)
{
this.InputValue.Text += Argument; this.InputValue.Text += "\r\n";
}
}
Notice the addition of the string[] args parameter to the Main() method. This is the entry point for data into the application. However, the user isn't under any obligation to pass any arguments, so the code needs to detect the presence of command-line arguments. You can perform this task in one of two ways. Listing 3.1 shows a technique where you detect the command-line parameters in the Main() method. A second method is to create two constructors. The first accepts parameters, and the second doesn't. C# automatically selects the correct constructor at runtime.
The example performs simple processing of the command-line parameters. It separates each parameter, adds it to a text box on screen, and then adds a carriage return so it's easy to see the
individual command-line parameters. Figure 3.3 shows some typical output from the application.
Figure 3.3: An example of command-line processing
It's convenient to add the command-line parameters directly to the project so you don't have to keep a command prompt open for testing purposes. To add a set of command-line parameters to your test environment, right click Parameters in Class View and choose Properties from the context menu. Select the Configuration Properties → Debugging folder. You can type the command-line options that you want to use in the Command Line Arguments field, as shown in Figure 3.4.
Figure 3.4: Use the Command Line Arguments field to hold any parameters you need for testing.
Working with the Out and Ref Keywords
C# makes a distinction between values and references. Any variable that keeps its value on the stack is a value. Examples of values include int and short. Any variable that maintains a pointer on the stack and its value on the heap is a reference. Examples of references include strings. We've discussed this issue several times in the book so far, but it's important to understand the distinction when it comes to the Out and Ref keywords.
Note You'll find the full source code for the examples in this section in the \Chapter 03\Reference folder on the CD. The listings show just the demonstration code.
When you pass a reference to a method in your class, any changes the class makes will appear in the reference when the call returns. However, when you pass a value to a method in your class, the value remains unchanged when the call returns. The Ref keyword enables you to
change this behavior. If you add the Ref keyword to a method's parameter list, then the value reflects any changes made during the call. Listing 3.2 shows an example of this technique.
Listing 3.2: Using the Ref Keyword to Affect Parameter Return Value
private void DoReference(int Value1, ref int Value2)
{
// Increment the two values. Value1++;
Value2++;
}
private void btnRefTest_Click(object sender, System.EventArgs e)
{
// Create two variables and assign them values.
int |
Value1 |
= |
1; |
int |
Value2 |
= |
1; |
//Call a function using a reference for one and standard
//calling technique for the other.
DoReference(Value1, ref Value2);
// Display the results.
MessageBox.Show("Value1 Results: " + Value1.ToString() + "\r\nValue2 Results: " + Value2.ToString(), "Output of Reference Test", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
Notice that the first value is passed normally, while the second value uses the ref keyword. Both values are int and initialized to 1. However, on return to the call from DoReference(), Value2 contains 2, not 1 as normal. Figure 3.5 shows the output from this example.
Figure 3.5: Using the Ref keyword properly can help you extend the flexibility of your applications.
Note One rule you must observe when using the Out and Ref keywords is that the keyword must appear at both the calling and receiving methods. C# will remind you to add the keywords if you fail to include them on one end of the call.
The Out keyword serves a different purpose from the Ref keyword. In this case, it enables you to pass an uninitialized value to a method and receive it back initialized to some value. You gain three benefits when using this technique.
•Slight performance gain by passing the value once
•Less code
•Slightly lower memory cost
You won't see huge gains by using the Out keyword, but they're real nonetheless. The point of this keyword is that you can create the value locally, but expect the called method to perform all of the work for you. Listing 3.3 shows a quick example of the Out keyword.
Listing 3.3: Using the Out Keyword to Affect Parameter Initialization
private void DoOut(out int Value1)
{
// Initialize the value. Value1 = 3;
}
private void btnOutTest_Click(object sender, System.EventArgs e)
{
//Create a variable, but don't initialize it. int Value1;
//Call a function using the Out keyword. DoOut(out Value1);
//Display the result.
MessageBox.Show("Value1 Results: " + Value1.ToString(),
"Output of Out Test",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
As you can see, the calling routine creates Value1, but doesn't initialize it. DoOut() performs the task of initializing Value1. Of course, this is a very simple example of how the Out keyword works. Figure 3.6 shows the output from this example.
Figure 3.6: The Out keyword allows you to pass uninitialized values to a method.
Working with Namespaces
Nothing in the C# standard says you must use namespaces. You can create classes by themselves and use them as needed. The point behind using namespaces is organization. Using a namespace enables you to gather all of the classes you create in one place, even if they exist in separate files. In addition, using namespaces helps avoid potential name clashes between vendors. If two vendors choose the same name for a class, then C# will constantly flag ambiguous references in your code.
The namespace isn't a one-level phenomenon. You can use several levels of namespaces to organize your code. In fact, Microsoft followed this procedure when creating the .NET Framework. Look at the System namespace and you'll see it contains several levels of other namespaces.
Creating multiple levels of namespaces in your own code is relatively easy. All you need to do is define a namespace within the confines of another namespace as shown here:
namespace Sample
{
public class Class0
{
public Class0()
{
}
}
namespace MyFirstNamespace
{
public class Class1
{
public Class1()
{
}
}
}
namespace MySecondNamespace
{
public class Class2
{
public Class2()
{
}
}
}
}
Notice that the Sample namespace contains a combination of classes and namespaces. You can mix and match classes and namespaces as needed to achieve your organizational goals. The important consideration is keeping the classes organized so they're easy to find.
The namespaces use a dot syntax. If you want to access Class1 in the previous example, you need to access it through Sample.MyFirstNamespace. Figure 3.7 shows the Object Browser view of this set of namespaces and classes. Of course, a read class library would contain more classes within the namespaces than are shown in the example.
