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

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

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

#endif

#endregion

#line default

}

}

}

The #define and #undef directives shown in this example create two symbols that appear as part of the #if and #elif directives later. Notice that the example code contains two regions. The reason regions are such a good idea is that you can see just the parts of your code you want. The collapsed areas contain descriptive text, as shown in Figure 4.4.

Figure 4.4: Use regions to provide collapsible areas in your code.

As shown in Figure 4.4, the #region directive creates collapsible areas within the Main() method. This provides you with better flexibility in viewing your code, especially if a method contains a lot of code.

The #warning and #error directives appear in the output when you compile the code. Figure 4.5 shows the result of compiling the sample code. Notice that the #error entry contains an exclamation mark to make it easier to find. You should also observe that the #warning output contains a line number based on the #line directive entry in the code shown in Listing 4.4. The filename is different as well.

Figure 4.5: The #warning and #error directives provide output you can use as a reminder.

Calling Native DLLs

It's important to use the .NET Framework for as much of your coding as possible because it provides a managed environment. However, sometimes you'll find that the .NET Framework doesn't provide a service that you need for your application. In this case, you need to resort to

the Win32 API to service the application requirement. You'll hear this technique called by a variety of names, but the most common is Platform Invoke, or PInvoke for short.

The main idea behind using PInvoke is to create a connection between your managed code and the unmanaged code of the Win32 API. Microsoft suggests that you create one or more classes to hold the PInvoke calls so that you can use them with multiple applications. Given the importance of some of these calls, expect to see third-party libraries containing the required classes for you. Fortunately, using PInvoke isn't difficult, as shown in Listing 4.5.

Listing 4.5: Use PInvoke to Access Win32 API Functions

// Make sure you include the InteropServices namespace. using System;

using System.Windows.Forms;

using System.Runtime.InteropServices;

namespace PInvokeDemo

{

class Class1

{

// Import the MessageBeep function so we can use it. [DllImport("User32", EntryPoint = "MessageBeep")] static extern bool MessageBeep(int uType);

[STAThread]

static void Main(string[] args)

{

//Play the default beep. MessageBox.Show("Should hear default beep."); MessageBeep(-1);

//Play the asterisk sound. MessageBox.Show("Should hear asterisk sound."); MessageBeep((int)MessageBoxIcon.Asterisk);

}

}

}

Always add the System.Runtime.InteropServices namespace if you plan to use an external DLL. This namespace adds the support you need to work with PInvoke.

The second step is to add the [DllImport] attribute to your code along with a method declaration. The example uses the MessageBeep() function found in the User32.DLL file. Notice that you must declare MessageBeep() as static extern, as shown in the listing, or the application won't compile. The MessageBeep() declaration indicates the function will accept an integer value as input and provide a bool output (true when successful).

When using the MessageBeep() function, you can supply a value for a specific kind of beep based on the message box icons, or you can provide a value of –1 for the default beep. Normally, you'd need to provide a list of constants for the beep values; but, in this case, we can use the .NET Framework–supplied values. Notice the (int)MessageBoxIcon.Asterisk entry for the asterisk sound. You'll find that the int cast works just fine for this example and

many other examples you might want to create. Of course, you can't use the .NET Framework values directly—you must perform the cast in order to prevent problems when making the Win32 API call.

Error Handling

Most applications today contain some form of error handling. Unfortunately, the level and quality of error handling varies greatly. Some applications provide so much error handling that the application constantly raises alarms, even if the user provides the correct input. Other applications provide error handling that only a developer could love—the messages are replete with jargon and the data is useful only if you designed the application. Still other applications provide one-size-fits-all error handling that simply tells the user an error occurred without saying what error it was or how to fix it. The following sections will demonstrate various error-handling techniques. We'll also visit this topic regularly throughout the book.

Using Try and Catch

You'll use the try…catch statement regularly for error handling. In fact, you saw one example of the try…catch statement earlier in the chapter when we discussed using multiple constructors to handle command-line input. The purpose of the try…catch statement is to allow you to handle application errors so the user won't see the usual incomprehensible Windows error message. Even if you can't handle the error, it's helpful to provide a detailed and understandable message. Generally, the try…catch statement takes the form shown here.

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

{

object MyObject = null; // Create an object.

// Create the try...catch... structure. try

{

// The call will fail because of a bad cast. MyBadCall1((int)MyObject);

}

catch (Exception error)

{

// Display an error message. MessageBox.Show("Source:\t\t" + error.Source +

"\r\nMessage:\t\t" + error.Message + "\r\nTarget Site:\t" + error.TargetSite + "\r\nStack Trace:\t" + error.StackTrace, "Application Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

}

}

In this case, the example code will fail because the method we're calling expects an integer. The invalid cast will cause an error even though C# compiles the example without so much as a warning. Notice that this try…catch statement uses the general Exception variable. Using Exception enables the application to catch all errors. We'll see in the next section that you can provide more selective error handling.

The exception object, error, contains a wealth of information. The example shows some of the common items you'll use to diagnose problems with your application. Here's the output you'll see when working with this example.

Figure 4.6: The error object contains a wealth of information about the problem with the application.

As you can see, the output is the usual incomprehensible gibberish that Windows produces by default. We'll see in the next section that you can improve on the Windows messages by catching errors and handling them yourself. Using this technique enables you to create better error messages.

Throwing an Exception

Sometimes you'll want to detect an error in your application and provide custom support for it. However, you might not be able to handle the error at the level at which the error occurs. In this case, you need to throw an exception. The exception will travel to the next level in the application hierarchy. Listing 4.6 shows an example of throwing an exception.

Listing 4.6: Throw an Exception When Necessary

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

{

try

{

// The call will fail because we've thrown an exception. MyBadCall1(12);

}

catch (InvalidOperationException error)

{

// Display an error message. MessageBox.Show("Invalid Exception Error" +

"\r\nSource:\t\t" + error.Source + "\r\nMessage:\t\t" + error.Message + "\r\nTarget Site:\t" + error.TargetSite + "\r\nStack Trace:\t" + error.StackTrace, "Application Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

// Display the inner error message as well. Exception InnerError = error.InnerException; MessageBox.Show("Inner Error Message" +

"\r\nSource:\t\t" + InnerError.Source +

"\r\nMessage:\t\t" + InnerError.Message + "\r\nTarget Site:\t" + InnerError.TargetSite + "\r\nStack Trace:\t" + InnerError.StackTrace, "Application Error",

MessageBoxButtons.OK,

MessageBoxIcon.Error);

}

}

private void MyBadCall1(int AnInt)

{

object MyObject = null;

try

{

// Create an error condition. AnInt = (int)MyObject;

}

catch (Exception error)

{

// Throw an exception. throw(new

InvalidOperationException("This is an error message!", error));

}

}

MyBadCall1() creates an error condition by attempting to cast an object as an int. This is the same error as in the first example. However, MyBadCall1() is one level lower in the hierarchy, so we catch the error and pass it up to the calling method, btnThow2_Click(). Notice the technique used to create a new exception of a different type and to pass the existing exception up to the next highest level.

The catch portion of btnThow2_Click() looks for a specific error. In this case, it looks for an InvalidOperationException object. However, you could use the same techniques shown here for any other specific error. The two MessageBox.Show() calls display the error information. The first call contains the custom message shown in Figure 4.7. The second call contains the same error information you saw in Figure 4.6 (with appropriate changes for error location).

Figure 4.7: A custom error message can provide better input than the Windows default message.

Using the Finally Keyword

The finally keyword is an addition to the try…catch statement. You can use it in addition to, or in place of, the catch keyword. Any code that appears as part of the finally portion of the statement will run even after an error occurs. This keyword comes in handy when you want to attempt to reduce the damage of an error. For example, you might need to close a file to ensure a graceful failure of your application. Here's an example of the finally keyword in use.

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

{

try

{

// The call will fail because we've thrown an exception.

MyBadCall1(12);

}

catch

{

// Display an error message. MessageBox.Show("Invalid Exception Error",

"Application Error",

MessageBoxButtons.OK,

MessageBoxIcon.Error);

// Try to return. return;

}

finally

{

// The example must run this code. MessageBox.Show("Must Run This Code",

"Finally Code", MessageBoxButtons.OK, MessageBoxIcon.Information);

}

//The example will try to run this code, but won't

//because of the exception.

MessageBox.Show("This Code Won't Run", "Code Outside Try//Catch", MessageBoxButtons.OK, MessageBoxIcon.Information);

}

As you can see, we're using MyBadCall1() again to generate an error. In this case, the catch portion of the try…catch statement will generate a simple message telling you of the error. The message in the finally portion of the try…catch statement will also appear, despite the return statement in the catch portion. The finally code will always run. However, because of the return statement, the message box outside the try…catch statement will never appear.

Advanced Class Creation Techniques

By now, you should know that classes are extremely flexible. However, we haven't looked at some of the advanced development techniques for classes. For example, you can nest one class within another. The parent class can see and use the child class, but you can hide this class from the outside world if desired.

The following sections look at several helpful class creation techniques. You might not use these techniques every day, but you'll find them essential when you do. For example, not every class requires variable length parameter lists, but knowing how to create a class that does can keep you from creating an odd assorting of class overrides to fulfill certain needs. We'll also discuss how you can create class versions and implement special class conditions.

Creating Variable Length Parameter Lists

There are some situations when you don't know the precise number of parameters that a user will pass to your method. For example, you might create a method that adds a list of numbers, but you not know how many numbers the user might need to add. C# provides help in this situation in the form of the params keyword. The params keyword can only appear once in a

list of arguments for a method and must appear at the end of the list. Here's an example of the params keyword in use.

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

{

// Test the variable argument list.

MessageBox.Show("The Result Is: " + MyVar(1, 2).ToString()); MessageBox.Show("The Result Is: " + MyVar(1, 2, 3).ToString()); MessageBox.Show("The Result Is: " + MyVar(1, 2, 3, 4).ToString());

}

private int MyVar(int Value1, params int[] Value2)

{

int

Temp;

//

Temporary Value Holder

int

Count;

//

Loop Count

//Transfer Value 1 to Temp. Temp = Value1;

//Keep adding until done.

for (Count = 0; Count < Value2.Length; Count++) Temp = Temp + Value2[Count];

// Return the value. return Temp;

}

In this example, MyVar() requires a minimum of two numbers to add. However, if you want to add more numbers, MyVar() will accept them, as shown in btnTest_Click(). As you can see, you must add the params keyword to the last argument in the list. The argument must also consist of an array told hold the variable number of input values.

The example shows how you could use an int array. However, this technique works even better with strings or objects because you can send data of various types and convert them within the method. The biggest disadvantage of this technique is that Intellisense can't help the user. For example, the user will only receive helps on the first two arguments in the example code.

Creating Class Versions

Most of the classes in the .NET Framework follow a hierarchy. A derived class uses a base class as a means of getting started. In most cases, the base class is perfectly usable as a class in its own right. However, there are exceptions to the rule that we'll discuss in the "Implementing Special Class Conditions" section that follows. Some classes are only usable as base classes and you can't derive from others.

The act of evolving a class over time is called versioning. Almost every class you build will go through several versions, because usage will demonstrate the need for added functionality. The act of creating new versions of existing methods is common practice—many companies will simply append a number to the end of the method name to avoid problems.

You can also apply the term versioning to derived classes. The derived class is a new version of an existing class. In some languages, the act of deriving new classes mixed with the need of the base class to evolve causes problems. For example, whenever a developer adds a new

method to a base class, there's a chance that the new method will conflict with methods in a derived class.

While a well-designed class will retain the same interface for existing methods, adding new methods usually doesn't break applications—that is, unless the addition creates a conflict. Derived classes are an essential part of the object-oriented programming paradigm. A developer writes a base class with some simple functionality, then refines the intended behavior within a derived class. Consequently, developers require access to both new versions of existing classes and derived classes that augment the behavior of the base class. Conflicts seem inherent and unavoidable in such an environment.

To prevent problems with versions, C# forces the derived class to declare its intent using one of several keywords. The first keyword is new. If you declare a method in a derived class with the same name as the base class, but add the word new to the declaration, then C# treats the method as a new addition. The new method doesn't override or block the method in the base class—users can see both methods when they use the derived class.

In some cases, the derived class does need to override the method found in the base class. Perhaps the implementation of the method in the base class won't work with the derived class. In this case, the developer adds the override keyword to the method declaration in the derived class. The user of the derived class will only see the method that appears in the derived class—the method in the base class is blocked from view. Listing 4.7 shows an example of the new and override keywords in use.

Listing 4.7: Using the New and Override Keywords

using System;

namespace ClassVersion

{

class Base

{

// Create the conflicting method. public virtual void Conflict(int Value)

{

Console.WriteLine("This is from Base: {0}", Value);

}

}

class Derived1 : Base

{

// This method won't hide the one in Base. public new virtual void Conflict(int Value)

{

Console.WriteLine("This is from Derived1: {0}", Value);

}

}

class Derived2 : Base

{

// This method does hide the one in Base. public override void Conflict(int Value)

{

Console.WriteLine("This is from Derived2: {0}", Value);

}

}

class Class1

{

[STAThread]

static void Main(string[] args)

{

//Create three objects based on the classes. Base MyBase = new Base();

Derived1 MyDerived1 = new Derived1(); Derived2 MyDerived2 = new Derived2();

//Output values based on the Conflict method. MyBase.Conflict(1);

MyDerived1.Conflict(1);

MyDerived2.Conflict(1);

}

}

}

As you can see, all three classes contain the same method. When you call on them in the Main() function, they all display the results you expect. Each call to Conflict() uses the version specifically designed for that class.

An interesting thing happens if you make the Conflict() method in Derived1 private. The application still compiles, but you'll see the output from the Base class, not Derived1. Because Derived1 doesn't hide the Base class version of Conflict(), the application assumes you want to use the Base class version. On the other hand, if you try to make the version of Conflict() in Derived2 private, the application will register an error telling you that you can't make virtual methods private. The reason for the difference is that Derived2 hides the version of Conflict() in the Base class.

Implementing Special Class Conditions

All classes don't come in the same form. So far, we've dealt with classes that are basically open for any activity. You can inherit from them, instantiate them, and generally use them as needed. However, not every class is a good candidate for such global use. For example, you might want to create a class that serves only as a template for creating other classes. This is the infamous base class that you've heard so much about. To create a base class, you need to know about abstract classes. The following list describes each special class condition in a little more detail.

Sealed Sealed classes always provide a full implementation of all the methods they contain. You can instantiate a sealed class and use the result object as you would any other object. However, other classes can't inherit from a sealed class. In addition, all methods within a sealed class are static, which means that they can't be changed or overridden in any way. Sealed classes are useful because they enable you to create a specific class implementation without concern that someone else will misuse the class in some unforeseen way.

Abstract Abstract classes are very close to interfaces. You can't instantiate an abstract class and the class doesn't need to provide implementations for any of the methods it contains. However, unlike an interface, an abstract class has the option of providing some

implementation details. In addition, while a class can inherit from multiple interfaces, it can inherit from only one class. This means that an abstract class provides the perfect means to create a base class—one that you'd never use, except to create other classes.

Note You might hear the term virtual in conjunction with classes. In C#, the virtual keyword only appears with methods and properties. The keyword enables developers to override the method or property in a derived implementation of the associated class. In short, any interface or class that contains virtual members could be considered virtual, but the keyword has no meaning in this context.

Where Do You Go From Here?

When you complete this chapter, you should have a better idea of how to work with classes with C#. One of the central ideas is that the class is the center of everything in .NET. We've explored classes in many ways, including specialty class types used to create other classes.

One of the best ways to use the information in this chapter is to look at any existing code you might have written in .NET or design some new code using the principles in Chapters 3 and 4. Make sure you consider how best to write the code so that it requires the least effort. For example, you'll want to use attributes and directives to perform some work automatically, rather than write all of the code by hand.

The problem that you hear about the most today is bugs in code. Yes, there's the age-old argument that users are their own worst enemy, but a good developer realizes this and builds the application accordingly. A large part of the solution for errant code is good error handling. This chapter has introduced you to some new solutions for error handling provided by .NET. Now might be a good time to see how your existing code stacks and use the information in this chapter to improve it.

Chapter 5 is going to explore the application that all of us have written at one time or another—the desktop application. The chapter will tell you about the basic application types you can create with C#, how to use various resources, and then how to write the various applications. We'll also discuss an essential topic for developers—debugging techniques. The Visual Studio .NET IDE makes this task easier than ever before.

Part II: Standard Application Development with C#

Chapter List

Chapter 5: Building Desktop Applications

Chapter 6: Creating Controls and Components

Chapter 7: Working with Threads

Chapter 8: Working with Active Directory

Chapter 9: Designing with Security in Mind

Chapter 10: Application Packaging Issues

Chapter 5: Building Desktop Applications