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

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

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

debugger is also one of the most misunderstood parts of the IDE. The following sections provide a quick tutorial on using the debugger to your advantage while working on the examples in this book. We'll also discuss other debugging tips as the book progresses.

Using the Debugger

The most common way to use the debugger is to set a breakpoint on your code and start the debugger using the Start Debug command. Of course, you can also start the debugger and simply wait for your code to fail the first time.

Breakpoints come in two forms. The first is an unconditional breakpoint you set by clicking the left margin of the line of code. You can also right-click the line of code and choose Insert Breakpoint from the Context menu. An unconditional breakpoint always stops execution.

The second breakpoint is conditional. It relies on a specific set of circumstances to stop execution. To set a conditional breakpoint, right-click the line of code and choose New Breakpoint from the Context menu. You'll see a New Breakpoint dialog box similar to the one shown in Figure 5.9.

Figure 5.9: You can set conditional breakpoints using the New Breakpoint dialog box.

As you can see, this dialog box enables you to set a variety of conditional breakpoints, including breakpoints based on the value of a variable or even the breakpoint hit count. The hit count is the number of times the application has reached the breakpoint during normal execution. Notice the Address tab of the New Breakpoint dialog box. You can use this tab to enter the address where an application failed. The Address field will access a value in decimal or hexadecimal. Preface any hexadecimal numbers with a "0x" to ensure the debugger sees them correctly.

Tip The Debug Start Without Debugging command enables you to run an application from the Visual Studio .NET IDE without using the debugger. This is a useful feature if you want to check for timing errors or problems that could occur when the application runs outside the debugger. Not every problem shows up when you run the application with the debugger. In fact, some resource problems only show up when you create a release version of the application. Consequently, using the Start Without Debugging command is

an essential part of checking your application execution.

Once the application is running under the debugger, you have a number of choices for following the execution path. All of these actions appear on the Debug toolbar shown in Figure 5.10. These buttons (in order of appearance) are: Continue, Break All, Stop Debugging, Restart, Show Next Statement, Step Into, Step Over, and Step Out (we'll cover the other two buttons later).

Figure 5.10: The Debug toolbar contains all of the buttons you'll need to perform most debugging tasks.

The three buttons Step Into, Step Over, and Step Out enable you to follow the execution path one step at a time. If you step into a command, you'll follow the execution path one level lower. For example, if the instruction pointer rests on a function call, you'll step into that function. Stepping over a command means that you remain at the same level. The code still executes the function call, but you only see the results of the function call, not the intermediate steps. Finally, stepping out of a function means rising one level higher. The debugger will finish executing the code at the current level and stop at the next statement of the previous (higher) level. The problem is that stepping out doesn't always work if an errant instruction at the current level prevents the code from executing.

After you've learned all you can at the current breakpoint, you can choose one of three actions. Click Continue if you want the application to execute normally until it hits the next breakpoint. Click Stop Debugging if you've finished the current debugging session. Finally, click Restart if you want to start the debugging session over again. Clicking Break All will stop application execution at the current instruction, no matter what the application might be doing at the time.

Performing Remote Debugging

Not every debugging session happens with an application loaded in the debugger or even with a local application. Sometimes you'll want to debug a running process. In some cases, this process might appear on another machine. For example, you need to perform remote debugging to check the operation of a component on a server that a local client is using. If you're using a distributed application technology such as SOAP, there's no direct connection between client and server, so you need to create a separate debugging session for the remote component.

The Debug Processes command displays the Processes dialog box shown in Figure 5.11. This dialog box enables you to perform remote debugging of an existing process such as a server-side component. All you need to do is attach to the server by supplying a Transport

(Default normally works fine) and a Name value. When the debugger attaches to the server, you'll see a list of the processes on that server.

Figure 5.11: Use the Processes dialog box to attach to a remote server in order to debug existing processes.

Note You must be part of the Debugger Users group on the remote machine in order to use the remote debugging features of Visual Studio .NET. In most cases, being a member of the Administrators group doesn't automatically extend rights to the Debugger Users group. Microsoft chose to make this security election specific in an effort to reduce the risks of security breaches on the remote machine.

Notice that the Processes dialog box includes check boxes that show the processes in all sessions and the system processes. Choosing the correct set of viewing options makes it easier to locate the process you want to view. When you find the process you want to view, click Attach. You'll see an Attach to Process dialog box that contains application debug types. This dialog box will always have the Common Language Runtime and Native options checked. If you want to debug another application type, you'll need to check the correct option. Click OK and you'll attach to the process.

At this point, you can click Break, in the Processes dialog box, to start the debugging session. You can use the same controls as you would with any application to start and stop application execution. In short, except for the attachment process, debugging a remote application is much the same as debugging a local application. Of course, you can't see any visual elements of the application unless they appear through the client on the local machine. Remote debugging is limited to work with the remote code.

Performing Standard Debugging Tasks

When you start your application, the debugger normally creates a series of seven windows along the bottom of the display area. Each of these windows serves a different purpose in helping you work with your application. The following list tells you about each window. We won't visit each window in detail in this chapter, but we'll discuss them as the book progresses.

Autos Some applications can get quite complex and generate more than a few variables, which can make the windows become cluttered. The Autos window contains a list of variables

associated with the current and the preceding statement. This view enables you to focus on just the current task and clears up the clutter you might otherwise have to view. The window shows the variable name, value, and type.

Locals This window contains a list of all the variables associated with the current function. It doesn't show variables outside the current function. For example, if you have a global variable, it won't appear in this window, even if the current function performs some task with the variable. You'll see the values of any arguments used to call the function. The Locals window shows the variable name, value, and type.

Watch You'll place variables and functions that you want to watch in this window. The easiest way to do this is to highlight the code in question and drag it to the window. The Watch window also allows you to type variables and functions by hand. You can create multiple Watch windows for a given debugging session. The Watch window shows the variable name, value, and type.

Call Stack This window answers the question of how the application got to the current point of execution. Every time the application makes a call, the call gets added to the call stack. When the call returns, Visual Studio removes it from the call stack. In short, the Call Stack window shows you a list of pending calls, which tells you how the application accessed the current function.

Breakpoints The Breakpoints window contains a list of breakpoints for your application. It also tells you the break conditions. You can use this window to manage breakpoints, which includes adding new breakpoints and deleting existing breakpoints. The Clear All Breakpoints button enables you to remove all of the breakpoints from your application without looking for them individually. You can also disable the breakpoints by clearing the check next to the entry. The Disable All Breakpoints button will disable all of the breakpoints in an application at once, which means the application will run as if you hadn't set any breakpoints.

Command Window You'll use the Command Window to perform queries on your application as you debug it. For example, if you typed ? <Variable Name> in the Command Window, Visual Studio would return the current value of the Command Window. You can also perform limited "what if" analysis by trying out various permutations of function calls within the window. The reason this option is limited is that you have to avoid certain types of calls. For example, you wouldn't want to call an event handler from within the Command Window.

Output The Output window contains a list of the current debugger actions and any debug statements you place within the code. For example, this window tells you the names of all the DLLs loaded to service your application when it starts. The Output window also reports any return code from your application.

Generally, you'll find that the windows in this list provide everything you need in the way of information. You can learn the values of any variables in your application, interact with the application in various ways, and perform certain types of management tasks. However, there are times when you need to view an application in intense detail, and the debugger displays can help you there as well. For example, you can view the IL code for your application by selecting Disassembly from the View drop-down list box on the Debug menu (the last button in Figure 5.10). Figure 5.12 shows a typical disassembly of the Typing Buddy application we

discussed earlier. The C# statements appear in bold type, while the disassembly appears in a lighter type.

Figure 5.12: The Disassembly window shows you the internal workings of your application.

Notice the if (TimeLeft == 0) statement in Figure 5.12. The disassembly of this code is cmp dword ptr [esi+00000114h],0. It's clear that the code is performing a DWORD (double word or 16-bit) comparison of the value pointed to by the ESI register offset by 0x00000114 with 0. However, you don't believe that the code is correct. How do you validate it? The first thing you need to know is that this value supposedly points to TimeLeft, which has a value of 3,600 according to the Autos window. So, if [esi+00000114h] actually points to a value of 3,600, there's a good chance that this code is correct.

The View drop-down list box also contains a Registers entry. When you open this window, you'll see all of the registers for your machine. According to my Registers window (your window will differ), ESI contains a value of 0x00c45638. Adding 0x114 to this value means that the memory location should point to 0x00c4574c. Open the Memory 1 window using the options in the View drop-down, enter the address, and you'll see a display similar to the one in Figure 5.13. If you look at the value in the Memory window, you'll see that a hex value of 0x0e10 is equal to 3,600 decimal (remember that Intel processors use a little endian storage technique).

Figure 5.13: You can even view the contents of memory within the debugger.

The point of this section isn't to demonstrate that you can become hopelessly mired in debugger functionality in Visual Studio .NET; it demonstrates that you can get as much detail about your application as you need. Application development boils down to knowing the details. Knowing how to use the debugger enables you to check the details, even the details of library code. In short, you might not look at the IL code for your applications every day, but it's good to know what to do if you need that level of detail somewhere along the way.

Where Do You Go From Here?

This chapter has answered the important question of what type of applications you can write using C#. The answer is that you can write all of the common Windows application types you've used in the past. If you're a Visual Basic developer, using a form to create all of your Windows applications isn't anything new, but the level of flexibility that C# provides is a welcome addition. On the other hand, Visual C++ developers will need to get past the document/view architecture of the past and realize there's only one basic Windows application project type to work with now.

The one thing this chapter presents is possibilities. You've learned that a console application commonly writes to the command prompt screen, a file, or an event log. Now is a good time to try out some alternative forms of output that you could use on remote machines. It's also good to know how to work with standard C# resources so you can create feature-rich applications with a minimum of code in a minimum of time. Spend time learning the different Windows applications types so you can create them as needed. Finally, learning to debug applications is a skill that's important for every kind of application. If you gain nothing else from this chapter, learning how to use the debugger is critical.

Chapter 6 presents you with some more opportunities to learn about the kinds of applications you can create with C#. In this chapter, you'll learn how to create both components and controls. This is an essential skills chapter because you need to know how a component works in order to create distributed applications. Controls are important because they're the building blocks of modern applications.

Chapter 6: Creating Controls and

Components

Overview

Components and controls form the building blocks of most modern applications. In an effort to reuse as much code as possible, developers create components or controls that can fulfill more than one need. For example, even though the function of each pushbutton on a form performs a different task, the underlying code required to create the pushbutton doesn't vary. Consequently, even though developers once had to code the individual pushbuttons at one time, they don't any longer because the code appears within an easy-to-use control and encapsulates the control functionality.

C# makes it easy to create both components and controls containing reusable code. It combines the low-level functionality of Visual C++ with the high-level resource management of Visual Basic. This combination of features actually works to the developer's good, because the developer still has the flexibility required to create useful components and controls but doesn't have to spend as much time coding them.

This chapter will show you how to build and use components and controls within your applications. It assumes you have at least some knowledge of component and control construction, so we won't discuss every detail. For example, you should already know the basics such as the use of properties, methods, and events.

.NET and Components and Controls

Components and controls are the basic building blocks of every application today. A developer uses these building blocks to create application elements without coding the generic elements of the component or control each time. Components offer code that defines business elements or generic functionality. Controls provide visual elements (such as pushbuttons) and nonvisual elements (such as timers) that developers use to create the user interface for an application. Both technologies appear as part of Visual Studio .NET as well as offerings from third parties.

The following sections discuss some issues regarding the use of components and controls under Visual Studio .NET in general and in C# specifically. One of the more important issues is how many compatibility problems you'll face when using your current collection of controls and components with .NET. We'll discuss the issue of working with metadata and how you read the metadata for the application.

Compatibility Issues

For many developers, .NET represents yet another way to create applications that might not provide full compatibility with existing technology. After all, .NET relies on CLR, rather than on the Win32 API. You probably have a hard drive full of components and controls that do rely on the Win32 API that you use to build applications. The existence of such a large base of unmanaged components and controls might have some developers worried. However,

.NET in general works with both managed and unmanaged components and controls. The managed components and controls rely on the .NET Framework and the Common Language Runtime (CLR) to operate. Unmanaged components and controls run under the Win32 API. C# automatically handles the different environments for you in most cases.

The reason you need to think about general support is that some older components and controls might not work in the new environment. For example, many developers have reported problems using Microsoft's older database controls, such as the Microsoft ADO Data Control (MSADODC.OCX), within the Visual Studio .NET IDE, because these controls tend to produce less than acceptable results in the application. For example, the Microsoft ADO Data Control will accept your input parameters, but will refuse to access the database or act as a data source to data award controls. In fact, Microsoft doesn't supply these older controls as part of Visual Studio .NET. You need to install the components from a Visual Studio 6 installation, and then follow the instructions found in the \Extras\VB6 Controls folder of Disk 4 of the Visual Studio .NET Enterprise Architect CD set to enable them. While it might appear that the instructions would allow you to use the older controls as before, they'll fail on most systems.

Not every unmanaged component or control has problems working in the new environment. In fact, many developers have found their unmanaged controls work just as they did before. You'll find a custom pushbutton control (PButton1.OCX) in the \Chapter 06\Unmanaged Pushbutton Control folder on the CD. Register this control by typing RegSvr32 PButton1.OCX at the command line and pressing Enter. To use the control, right-click the Toolbox in the Visual Studio .NET IDE and select Customize Toolbox. You'll find the PButton1.OCX entry in the Customize Toolbox dialog box, as shown in Figure 6.1. Click OK, and the control will appear in your Toolbox. You'll find a working example of how to

use this control in the \Chapter 06\PButtonTest folder on the CD. The point is that although many of your old controls will work, some won't under .NET when using C#.

Figure 6.1: You can find all of your old controls on the COM tab of the Customize Toolbox dialog box.

Tip It pays to keep your managed and unmanaged controls separated in the Toolbox. The separation makes it easier to determine when a control might not work due to compatibility problems. In addition, the separation acts as a visual cue that helps the developer keep the control types separate. You can add a new tab to the Toolbox by rightclicking the Toolbox and selecting Add Tab from the context menu. Use a distinct name for the tab, such as "Unmanaged Controls."

Even when a control works as anticipated, you might find some unexpected side effects from the transition from unmanaged to managed environments. Here, the example control supports an About dialog, but you can't access it from C#. This might present a problem if you place your support information in the About dialog for easy access by the developer. If you plan to use an unmanaged control in both managed and unmanaged environments, make sure the developer can access every critical feature from the control property pages.

Another oddity that you need to work with is that you won't see the unmanaged form of the control anywhere in the code. That's because C# has to build a bridge between your unmanaged control and the .NET environment. You'll likely see a reference such as this one for the control:

private AxPButton1Lib.AxPButton1 btnTest;

AxPButton1Lib.AxPButton1 is a reference to the managed wrapper for the control. Unfortunately, your search for control information still isn't over. The code accesses the control using the AxPButton1Lib.AxPButton1, but if you want to view the control in Object Browser, you'll need to find the interop.pbutton1lib assembly, which is automatically added to your project. The use of a different nomenclature for the button will affect the method used to access elements, such as enumerations. For example, if you want to determine the current ModalResult value, you must write a comparison similar to PButton1Lib.ModalType.mrOff. Figure 6.2 shows an Object Browser view of the unmanaged PButton1.OCX control within the managed C# environment. Notice the AboutBox() method is still present, even if it isn't displayed as you might think.

Figure 6.2: Using unmanaged controls within the C# environment might mean spending time in the Object Browser.

Does C# provide full compatibility with your unmanaged components and control? No, it doesn't—you'll find some situations where the Visual Studio .NET environment doesn't provide an acceptable interoperable environment. However, the vast majority of your components and controls will work with some changes in functionality due to .NET environment interoperability concerns.

Moving Unmanaged Controls to .NET

Adding an unmanaged control during design time is relatively easy—just add it to the Toolbox and use it as you would any other control. However, sometimes you need to add a control after the application is already running. The problem with unmanaged controls is that you can't access them the way you would a managed control within the .NET Framework. There's no direct access to the unmanaged environment, and coding around the various constraints is both unnecessary and problematic.

You can manually create a wrapper for your unmanaged control using several .NET utilities. The advantage of this technique is that the control ends up in the Global Assembly Cache (GAC) where it's accessible to all .NET programs. The first technique we discussed in the "Compatibility Issues" section provides a local copy of the control that only one application can use. Creating the wrapper also enables you to access the control as needed without using the Designer. The following steps show how to create the wrapper. (I'll use the PButton1.OCX file from the previous section as an example.)

1.Open the folder containing the control.

2.Type SN -k MyKey and press Enter. This step creates a key pair that we'll use later to create a strong name for the control. The control must have a strong name to appear in the GAC.

3.Type AxImp PButton1.OCX /Keyfile:MyKey. This step generates the two DLL files that the Designer created for you in the previous example. However, these files have a strong name associated with them.

4.Type GACUtil -i AxPButton1Lib.DLL and press Enter. This step adds the control's AxHost class to the GAC so you can create an instance of the control using standard C# techniques. This DLL file doesn't contain the control's definition—just the AxHost part of the picture (as it did for the previous example).

5.Type GACUtil -i PButton1Lib.DLL and press Enter. This step adds the control definition to the GAC. The control definition enables you to access control features, such as properties.

You can easily verify that the two parts of your control now appear in the GAC. Open the \WINDOWS\assembly folder on your system. You'll see a list of assemblies, such as the ones shown in Figure 6.3. Notice the highlighted entry is AxPButton1Lib; immediately below it is PButton1Lib.

Figure 6.3: The \WINDOWS\assembly folder holds all of the assemblies for the GAC.

At this point, you can access the control from an application. The PButtonTest2 application in the \Chapter 06\PButtonTest2 folder on the CD contains the example for this section. Let's look at a few details of that example. The first task you'll need to perform is adding a reference for both DLLs to your application. Use the Project Add Reference command to display the Add Reference dialog box. Highlight and select the two DLLs, as shown in Figure 6.4.

Figure 6.4: Be sure to add references to the two DLL files for the control.

Everything's in place for creating the control dynamically. The sample application has a Test button with a Click event handler named btnTest_Click(). The dynamically created PButton1 pushbutton will also have an event handler, btnNew_Click(). Listing 6.1 shows the source code for both event handlers (the btnNew_Click() event handler is shortened for the sake of clarity).