Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Visual CSharp .NET Developer's Handbook (2002) [eng]

.pdf
Скачиваний:
35
Добавлен:
16.08.2013
Размер:
4.94 Mб
Скачать

Figure 3.7: Use namespaces to organize your code.

Understanding Methods, Properties, and Events

Methods, properties, and events are the three essential constructs of COM classes. All three elements still exist in .NET, but you'll find that some of the rules for creating and using them have changed. In addition, .NET introduces an event substitute called the delegate that we'll discuss in the "Delegates" section of the chapter. In short, you need to be aware of some changes in the way that you work with classes.

You've already seen several examples of methods in the chapter. Methods have changed the least from the days of COM. The only rule you need to remember beyond those used in the past is that a method must exist within a class. You can't create a global method as you could in previous versions of Visual Studio.

The placement of methods within the class affects the visibility of the method. The visibility of the class also affects the external visibility of the method. However, the class still has a visibility indicator that tells its internal visibility. Table 3.1 shows how internal and external visibility interact to create specific method visibility conditions. The table also tells which combinations the Common Language Specification (CLS) supports directly.

 

Table 3.1: Member Access Specifiers for Managed Code

 

 

 

 

 

 

 

 

External Access

 

 

Internal Access

 

CLS

 

Result

 

 

 

 

 

 

 

 

public

 

 

public

 

public

 

Accessible by any code that

 

 

 

 

 

 

 

accesses the class.

 

 

 

 

 

 

 

 

public

 

 

protected

 

famorassem

 

Limits accessibility to types in

 

 

 

 

 

 

 

the same assembly or types

 

 

 

 

 

 

 

derived from the containing

 

 

 

 

 

 

 

type. A derived type doesn't

 

 

 

 

 

 

 

have to appear in the same

 

 

 

 

 

 

 

assembly.

 

 

 

 

 

 

 

 

public

 

 

private

 

assem

 

Only types within the same

 

 

 

 

 

 

 

assembly have access.

 

 

 

 

 

 

 

 

protected

 

 

public

 

N/A

 

Accessible by code in any

 

 

 

 

 

 

 

derived type and within the

 

 

 

 

 

 

 

containing type. A derived type

 

 

 

 

 

 

 

doesn't have to appear in the

 

 

 

 

 

 

 

same assembly.

 

 

 

 

 

 

 

 

 

Table 3.1: Member Access Specifiers for Managed Code

 

 

 

 

 

 

 

 

External Access

 

 

Internal Access

 

CLS

 

Result

 

 

 

 

 

 

 

 

protected

 

 

protected

 

family

 

Accessible by code in any

 

 

 

 

 

 

 

derived type. A derived type

 

 

 

 

 

 

 

doesn't have to appear in the

 

 

 

 

 

 

 

same assembly.

 

 

 

 

 

 

 

 

protected

 

 

private

 

famandassem

 

Only accessible by code in a

 

 

 

 

 

 

 

derived type if the code appears

 

 

 

 

 

 

 

in the same assembly.

 

 

 

 

 

 

 

 

private

 

 

public

 

N/A

 

Fully accessible from code

 

 

 

 

 

 

 

within the containing type and

 

 

 

 

 

 

 

subclasses.

 

 

 

 

 

 

 

 

private

 

 

protected

 

N/A

 

Fully accessible from code

 

 

 

 

 

 

 

within the containing type and

 

 

 

 

 

 

 

visible to subclasses.

 

 

 

 

 

 

 

 

private

 

 

private

 

private or

 

When used with a method, only

 

 

 

 

 

privatescope

 

accessible from code within the

 

 

 

 

 

 

 

containing type. When used with

 

 

 

 

 

 

 

a static variable, the member

 

 

 

 

 

 

 

isn't accessible at all

 

 

 

 

 

 

 

 

Creating a property is easy in C#. Like methods, properties have a visibility indicator that defines how other objects interact with them. You use the Property keyword to create a property. In addition, you can make properties read-only, write-only, or read/write. Here's a short example of a property (the full component code appears in the \Chapter 03\Example folder on the CD, while the test application appears in the \Chapter 03\Property folder).

//Create the private field used to hold the data. private string _Hello;

//Create the public property used to manipulate

//the data.

public string Hello

{

get

{

return _Hello;

}

set

{

_Hello = value;

}

}

Notice that the class hides the variable used to hold the data. Someone who wants to work with the property must use the public property. The get method enables read access, while the set method enables write access. The read/write state of a property depends on whether you implement either or both get and set. Note, also, that this is all the code you need to make the property persistent. Anyone who's spent hours trying to get COM to cooperate in storing values will appreciate the way .NET handles this issue.

When you add a property to your class, you'll see a special icon for it in Class View, like the one shown in Figure 3.8. Notice that Class View shows both the private field and the public property. A lock on the _Hello field indicates the private state of this variable.

Figure 3.8: Look for the special symbol for a property and its associated field in Class View.

Creating Interfaces

An interface is a contract between the client and server that determines how the two will interact. The interface specifies the methods, properties, and events used for communication, without actually providing an implementation of any of these elements. In short, you should look at an interface as a blueprint for communication. In some respects, an interface is similar to an abstract class. The difference is that an abstract class is used as a base class to create other classes, while an interface is mixed in an inheritance tree that determines the behavior of a new class.

Creating interfaces is important when a developer wants to provide more than one implementation but ensure the contract between client and server always remains the same. The best example of interface use is in components and controls. Both rely on the use of interfaces to define their behavior. In fact, the interface is essential in determining how the client will react to the component or control. While the implementation of the interface varies by component or control, the usage of the interface remains the same, which allows client and server to interact using a standardized method.

Note It's traditional to start an interface name with the letter I, as shown in the example code in this section. Using an I also provides a visual cue to anyone working with your code. However, the I is a convention and the interface will work just fine if you don't include it.

Defining an interface is relatively easy. You create a class-like structure that includes the Interface keyword and a description of the events, methods, and properties the interface supports (without any implementation). Here's a quick example of an interface.

namespace Sample

{

// Create a simple interface.

public interface ISimple

{

//Methods only need to show an argument list. void MyMethod(int Variable);

//Add properties without implementation,

//but always include get and/or set.

int MyProperty

{

get;

set;

}

}

}

As you can see, an interface contains a complete description of the implementation without the implementation details. A server implementing the interface will provide the implementation details. An interface also has a special appearance in Class View, making it easy to see. Figure 3.9 shows the interface created by the example code. You could also view the interface within Object Viewer. Because interfaces are so important to components and controls, we'll discuss this topic in detail in Chapter 6.

Figure 3.9: The Visual Studio .NET IDE provides a special presentation of interfaces.

Class Life Cycle

Every class has a life cycle. An application creates an object based on the class using a constructor, uses it for a while, and then discards it. At some point, the garbage collection system will locate the object created from the class and free the resources that it used. In a few instances, the Garbage Collector will also call upon a destructor to free unmanaged resources, such as those needed for COM interoperability.

Understanding the class life cycle is important because there are some problems that you'll need to diagnose based on this understanding. For example, your application might experience memory problems if it creates a large number of short-lived objects. While Microsoft has tuned the Garbage Collector to prevent problems of this sort, you need to know when the Garbage Collector is to blame for a memory problem (and what you can do to fix it). Of course, the most basic fix is to dispose of your objects (a procedure that isn't recommended, in most cases, because using dispose indiscriminately can cause performance problems).

The following sections contain a description of the life cycle of a class. They help you understand some of the requirements for working with classes under .NET. The reason this section is so important is because .NET has a class life cycle that's essentially different from the process used in the past.

Working with Constructors

Any class you create should include a constructor, even if the constructor is empty. A constructor enables a client to create an instance of the object that the class defines. The constructor is the starting point for your class. We've already looked at several constructors in the chapter. As you've seen, constructors never provide a return value, but they can accept one or more input parameters.

One of the issues we haven't discussed is the use of multiple constructors. Classes with multiple constructors are quite common because the multiple constructors enable a client to create objects in more than one way. In addition, many classes use overrides as a means for handling optional parameters. One of the better examples of this second form of constructor is the MessageBox.Show() method, which has 12 overrides that enable you to use different types of optional parameters. Here's an example of an application that relies on a class with multiple constructors. (You'll find the source code in the \Chapter 03\Constructor folder on the CD.)

public TestForm()

{

// Call the Form Designer code. InitializeComponent();

}

public TestForm(String FormName)

{

//Call the Form Designer code. InitializeComponent();

//Set the form name.

Text = FormName;

}

public TestForm(String FormName, String WelcomeLabel)

{

//Call the Form Designer code. InitializeComponent();

//Set the form name.

Text = FormName;

// Set the welcomd text. lblWelcome.Text = WelcomeLabel;

}

The example application uses these three constructors to open three versions of a secondary form. As you can see, each constructor adds another optional parameter. The example works equally well with any of the constructors. However, adding more information results in a more functional display. The three buttons (Plain, Name, and Welcome) on the first example form exercise the three constructors, as shown here.

private void btnPlain_Click(object sender, System.EventArgs e)

{

//Create the form using the first constructor,

//then display it.

TestForm Dlg = new TestForm(); Dlg.ShowDialog();

}

private void btnName_Click(object sender, System.EventArgs e)

{

//Create the form using the second constructor,

//then display it.

TestForm Dlg = new TestForm("A Test Form Name"); Dlg.ShowDialog();

}

private void btnWelcome_Click(object sender, System.EventArgs e)

{

//Create the form using the third constructor,

//then display it.

TestForm Dlg = new TestForm("Test Form with Welcome",

"Hello, this is a Welcome label.");

Dlg.ShowDialog();

}

When you click Welcome, you'll see the full output of the secondary dialog, as shown in Figure 3.10. Of course, this is a simple example of the types of constructors we'll create as the book progresses. The idea is to use constructors carefully to ensure the developer has the right combination of flexibility and ease-of-use. Too many constructors would make the decisions of which constructor to use difficult at best. Also note that the constructors are arranged in order from least to most complex, making it easier to find the correct constructor.

Figure 3.10: Using a series of constructors can help you create multiple effects using a single class.

Tip The example in this section uses multiple forms. When working with complex class additions, make sure you use the C# Class Wizard to aid your efforts. You open the C# Class Wizard by right-clicking the Project entry in Class View, then choosing Add Add Class. Type a class name in the Class Name field on the Class Options tab. In the

Base Class tab, choose System.Windows.Forms in the Namespace field and Form in the Base Class field. Select other options as needed for the dialog that you want to create, then click Finish. The new class will contain rudimentary support for essential form elements, saving you some typing time.

Working with Destructors

Given the way that C# applications work, you'll almost never need to add a destructor to any code. A destructor works differently in the .NET environment than in previous Windows environments in that you can't be sure when the destructor will get called. The Garbage Collector frees class resources, at some point, after the class is no longer needed. See the "Understanding Non-Deterministic Finalization" section for further details.

The C# specification does allow for a destructor. The destructor can't have any return value, nor can it accept any input values. This means you can't override the destructor, so there's at most one destructor for any given class. In addition, a class can't inherit a destructor, which means you must create a unique destructor for each class you create (if one is desired).

Destructors should avoid making time-critical changes to the user environment, and they should not use resources that might not be available at the time the destructor is called. For example, you wouldn't want to display a completion message from the destructor because the user might be engaged in some other activity. Likewise, you wouldn't want to attempt to close a file handle from the destructor because the file resource might not exist.

Despite problems using destructors in managed applications, you can use them for some types of non-time-critical purposes. For example, services and server-side components often place a message in the Event Log to signal a successful shutdown or to register shut down errors. You could perform this task with a destructor, provided the destructor doesn't rely on any external resources.

The one essential use of destructors is when you include unmanaged resources in your application. For example, you might create a device context for creating a 3D drawing in your application. If the device context is global to the class, you could use the destructor to free it before the application ends. However, a better memory management technique is to keep as many unmanaged resources as possible within the method call in which they're used, so you can free the resource immediately after use. Here's a simple example of a destructor (you'll find the full source in the \Chapter 03\Destructor folder on the CD).

//A simple class that includes both constructor

//and destructor.

public class DoTest

{

public DoTest()

{

// Display a message when the class is destroyed. MessageBox.Show("The class has been created!",

"Destructor Test Message", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);

}

~DoTest()

{

// Display a message when the class is destroyed. MessageBox.Show("The class has been destroyed!",

"Destructor Test Message", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);

}

}

This code will eventually run; however, you might end up waiting quite a while unless your machine is short on resources. (Make sure you click Quit for the first test in the example program.) The Garbage Collector only runs as needed, and that isn't as often as you might think on a well-designed development machine. As part of the testing process for this book, I ended up waiting a full minute for the Garbage Collector to run on at least one machine. The message always appears, but this example serves to point out that relying on a destructor under .NET is an error-prone and time-consuming process at best. Of course, you can speed the process up, if desired, but only if you're willing to give up some of the performance benefits of the Garbage Collector.

Note One of the interesting features of this example is that the second message box (the one signifying the destructor was called) will close by itself because the class that pointed to the dialog box is no longer available. Wait a few seconds after the clicking Quit, and you'll see the dialog box close automatically. However, if you force garbage collection (as described in the paragraph that follows), the dialog box remains in place until you specifically close it because the garbage collection mechanism keeps the class in place.

Every C# program has access to the GC (Garbage Collector) object. You can tell the Garbage Collector to run when it's convenient for you. However, the garbage collection process doesn't just involve your application. If you start the garbage collection process, the Garbage Collector will examine the resources for every running application—a huge performance penalty in some situations. The garbage collection could also occur at an inconvenient time. Time-critical processes could get interrupted when you force a garbage collection at the wrong time. The following code shows how to force a garbage collection, but you should use this feature with extreme care.

private void btnGC_Click(object sender, System.EventArgs e)

{

//Create the test class. DoTest MyTest;

MyTest = new DoTest();

//Free the resource. MyTest = null;

//Force a garbage collection. GC.Collect(); GC.WaitForPendingFinalizers();

//Display a success message. MessageBox.Show("Garbage collection has succeeded.",

"Garbage Collection Test",

MessageBoxButtons.OK,

MessageBoxIcon.Information);

// Exit the application. Close();

}

Understanding Non-Deterministic Finalization

Anyone who's worked with unmanaged code in the past knows that classes always contain a constructor (for creating an instance of the class) and a destructor (for cleaning up after the class is no longer needed). The world of .NET works differently from code you might have used in the past. Classes still contain constructions, as shown in previous examples, but they no longer contain destructors because the Garbage Collector determines when an object is no longer needed. This loss of ability to determine when a class is no longer needed is called non-deterministic finalization.

Behind the fancy term is a simple explanation. The developer loses the ability to determine when an object is going to go away. The object doesn't go away immediately—it may go away after the application terminates. This is a problem if you're using the destructor in an object to perform post-component processing. For example, you may leave a file open the whole time you use a component and close it only when the component no longer needs file access. Using the destructor to close the file is no longer an option in .NET, so you need to use some other method to handle the problem.

Note You can read more about this particular problem at http://www.devx.com/free/press/2000/102500.asp. Fortunately, there are workarounds for this problem, most of which are addressed by following good coding practice. In some cases, however, the Garbage Collector will cost some performance, because you'll need to open and close files more often.

Delegates

Delegates are the new event class for .NET. Using a delegate is somewhat different from an event. In some respects, using delegates is more flexible than using events. However, given the differences between delegates and events, even the most experienced programmers find them confusing to say the least.

Note Delegates can take many forms. This section presents one of the easiest to understand forms that a delegate can take. In addition, delegates often provide support for multiple handlers. This section will look at two forms of handler, but we'll discuss them separately. In a multiple handler scenario, the delegate actually receives a linked list of handlers that it calls in order. We'll discuss more complex forms of delegates as the book progresses because this is such as useful feature.

You begin creating a delegate by declaring the form the delegate must take. The Delegate keyword creates a special type of class that enables you to define delegate prototypes. Here's the code you'll normally use to declare a delegate. (The source code for this section appears in the \Chapter 03\Delegate folder of the CD.)

// Define a delegate for the example.

public delegate void DisplayMessage(String Subject, String Title);

As you can see, the code is relatively simple. You define the return value (normally void), the delegate name, and any arguments the delegate will require. It's important to note that this one

line of code represents an entire class. C# creates the class for you behind the scenes. As a result, the delegate definition normally appears within a namespace, but outside of a class.

Delegates require an external entity that stores data for processing and the list of handlers for the delegate. These entities normally appear in separate classes. The class could provide something as complex as database manipulation or as simple as managing a list in memory. (For that matter, you can keep track of a single data member at a time, but that technique doesn't demonstrate the power of delegates fully.) Here's the ProcessMessageClass for the example.

// Define a message processor. public class ProcessMessageClass

{

//Using this struct makes it easier to store the

//Subject and Title values.

private struct Message

{

public String Subject; public String Title;

public Message(String _Subject, String _Title)

{

Subject = _Subject; Title = _Title;

}

}

// Create an array to hold the message values. private ArrayList Msg = new ArrayList();

public void add_Message(String Subject, String Title)

{

// Add new messages to the list when requested. Msg.Add(new Message(Subject, Title));

}

public void ProcessMessage(DisplayMessage Del)

{

// Process each message in the message list. foreach (Message Item in Msg)

Del(Item.Subject, Item.Title);

}

};

As you can see, the example uses a struct named Message to act as a means for storing the data elements exposed to the handlers. The example will store a message subject (content) and title. The ArrayList data member Msg is the in memory database for the example. It holds every addition the code makes to the list of messages the user wants processed. The final piece of the database is add_Message(). This method adds new messages to Msg.

ProcessMessage() is the active part of the delegate-processing mechanism. It receives an implementation of the delegate we discussed earlier (the handler) and sends the handler the data in Msg to process. ProcessMessage() has no idea at the outset if it will receive one or multiple handlers, nor does it care which handler processes the data. In short, it's completely disconnected from the client portion of the application until the code creates the required connection.