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

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

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

values to the StandardMsg and ErrorMsg properties. Only the btnThrowError_Click() button event handler needs to set the CreateError property so the thread will fail.

The final three lines of code create the actual thread, start it, and assign a null value to the thread and thread object. Starting the thread isn't nearly as much work as setting it up.

Both thread event handlers display messages. The MyThreadExceptionHandler() event handler provides the most complete information based on the contents of the ThreadExceptionEventArgs object. Notice the use of the DialogResult variable, Result, to handle user input. This event handler assumes that the user might want to terminate the application due to the error, but it gives the user other options as well.

Performing a Worker Thread Test

The example application helps you learn about several worker thread features. For one thing, you'd never know the worker thread even existed were it not for the event handler code. Any application can hide threads this way. The threads work in the background without notice unless an error or some other event occurs. Theoretically, the application could perform tasks on the user's behalf without the user's knowledge.

When you start the application, you'll see a dialog similar to the one shown in Figure 7.1. Try using different message and timing values. As you increase the thread sleep time, you'll see a longer interval before the event message appears. Using a long interval also enables you to start several threads at once by clicking either Test or Throw Error several times in succession. Changing the messages before each pushbutton press shows that the threads are truly independent and that no data corruption occurs due to shared data.

Figure 7.1: The Worker Thread Demo enables you to test both success and error conditions.

One other test you'll want to try is viewing the application using the Spy++ utility found in the \Program Files\Microsoft Visual Studio .NET\Common7\Tools folder. You must set the Wait Time field to a high value to make this part of the test process work. I found that 12 seconds (12,000 milliseconds) works best, but your results will vary.

Start Spy++ and look for the WORKERTHREAD entry shown in Figure 7.2. The example begins by creating three threads. The first thread is the main window and it has all of the controls within it. The second and third threads are application-specific and you don't need to worry about them at this point.

Figure 7.2: Use Spy++ to see the threads in your application.

Click the Test button on the application, then quickly press F5 in Spy++ to refresh the window. You'll see another thread added to the list. This thread won't have any display elements because it's the worker thread. In fact, you'll see one new thread generated each time you click Test or Throw Error. If you create multiple threads, you'll see multiple entries added to Spy++. After the time elapses and the event message dialog box appears, press F5 in Spy++ again. This time you'll see a new thread that does include display elements—the thread for our worker thread will be gone. Close the Success or Error Message dialog boxes and the number of threads will return to the same number you started with in Figure 7.2.

Local DLL Example

This example shows how to place the threading logic for your application in a separate file—a DLL. The reason you want to know how to use this technique is that placing the thread in a DLL enables you to use it with more than one application. This principle is at the center of the common dialogs that most developers use to create applications. A single DLL can serve many generic needs, so long as you include enough configuration features to ensure complete customization, if desired.

The example in this section contains two user interface elements. The first is the main thread—a dialog-based test application. The second is the UI thread contained within a DLL. The test application will enable you to create multiple copies of the UI thread, with each UI thread providing independent operation for the user. You'll find the source code for this example in the \Chapter 07\DLLThread folder of the CD.

Configuring the Project

This example requires a little special configuration, due to the use of multiple UI elements. You'll begin creating the DLL portion of this example using the Class Library project. The example uses a project name of DLLThread. Add to this project a Windows Application with a name of DLLThreadTest.

Note Make sure you set DLLThreadTest as your startup project to ensure that the example

runs in debug mode. The Visual Studio .NET IDE will assume you want to use DLLThread as the startup project because it's the first project created. To set DLLThreadTest as the startup project, right-click DLLThreadTest in Solution Explorer and select Set as Startup Project from the context menu.

The Class Library project provides you with an empty shell. It doesn't even include a dialog box as part of the project. You'll need to add a dialog box by right-clicking DLLThread in Solution Explorer and choosing Add Add New Item from the context menu. Select Local Project Items\UI and you'll see a list of items similar to the one shown in Figure 7.3. You'll need to add the Windows Form option to the project. The example uses a name of ThreadForm.CS for the resulting file.

Figure 7.3: Visual Studio .NET allows you to add UI items to DLLs using this dialog box.

As part of the configuration process, you need to create a connection between the DLL and the test application. Right-click DLLThreadTest\References in Solution Explorer and choose Add Reference from the context menu. Select the Projects tab in the Add Reference dialog. Highlight the DLLThread project and click Select. Click OK to add the reference to the DLLThreadTest project.

Creating the DLL

The DLL code falls into two parts. First, you need to design a dialog box. The example uses a simple dialog box with a Quit button. The Quit button closes the form when you're done viewing it. You can find the code for the form in the ThreadForm.CS file. Second, you need to create code within the DLL to manage the form. The DLLThread class code appears in Listing 7.4.

Listing 7.4: The DLLThread Class Manages Access to the ThreadForm

public class DLLThread

{

// Create a dialog name variable. private string _ThreadDlgName;

public DLLThread(string ThreadDlgName)

{

// Store a dialog name value.

if (ThreadDlgName != null) _ThreadDlgName = ThreadDlgName;

else

_ThreadDlgName = "Sample Dialog";

}

public void CreateUIThread()

{

// Create a new form.

ThreadForm Dlg = new ThreadForm();

//Name the dialog. Dlg.Text = _ThreadDlgName;

//Display the form. Dlg.ShowDialog();

}

}

As you can see, the code for the DLL is relatively simple. Notice the private variable _ThreadDlgName. In an updated version of this program in the "Understanding Critical Sections" section of this chapter, you'll see that this variable isn't thread safe. We need a critical section in order to keep this variable safe. For now, with the current program construction, this variable will work as anticipated. However, it's important to think about potential problems in your application variables before you use the code in a multithreaded scenario.

The DLL constructor assigns a value to _ThreadDlgName. The check for a null value is important because you want to be sure the dialog has a name later. Note that you'd probably initialize other dialog construction variables as part of the constructor or use properties as we did in the worker thread example.

The CreateUIThread() method creates a new instance of the ThreadForm class, which contains the Thread dialog box. Notice that this method also assigns the name of the dialog box using the _ThreadDlgName variable. It's the time delay between the constructor and this assignment that causes problems in a multithreaded scenario. CreateUIThread() finishes by calling the ShowDialog() method of the ThreadForm class. It's important to use ShowDialog() rather than Show(), so you can obtain the modal result of the dialog box if necessary.

Creating the Test Program

The test program form consists of three pushbuttons and a textbox. The Quit button allows the user to exit the application. The New Dialog button will demonstrate the DLLThread class as a single-threaded application. The New Thread button will demonstrate the DLLThread class as a multithreaded application. It's important to the understanding of threads to realize that any DLL you create for multithreaded use can also be used in a single-threaded scenario. Listing 7.5 shows the test application code.

Listing 7.5: The Test Application Works in Both Single-threaded and Multithreaded Modes

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

{

//Create a new thread object. DLLThread.DLLThread MyThread =

new DLLThread.DLLThread(txtDlgName.Text);

//Display the dialog. MyThread.CreateUIThread();

}

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

{

//Create a new thread object. DLLThread.DLLThread MyThread =

new DLLThread.DLLThread(txtDlgName.Text);

//Create and start the new thread.

Thread DoThread =

new Thread(new ThreadStart(MyThread.CreateUIThread));

DoThread.Start();

// Get rid of the variables. DoThread = null;

MyThread = null;

}

The btnNewDialog_Click() method begins by creating an instance of the DLLThread class. Notice the inclusion of the text from the application textbox as part of the call to the constructor. The method calls CreateUIThread() to create a standard single-threaded application call to the DLL.

The btnNewThread_Click() method begins essentially the same way as btnNewDialog_Click(). However, in this case, the method creates a separate thread for the dialog box. Notice that there's no difference between this call and the one we used for the worker thread example earlier in the chapter. The only difference is in the implementation of the thread within the thread class.

When you run this application you'll notice that both buttons will produce a copy of the thread form when clicked the first time. However, if you try to click New Dialog a second time while the thread form is still present, the application will beep. That's because the application is waiting for the modal result from the thread form. However, you can create multiple copies of the thread form by clicking the New Thread button because each dialog resides in a separate thread. Of course, you can only continue clicking New Thread so long as you don't click New Dialog. The moment you click New Dialog, the main application thread stops and waits for the thread form to complete.

Server-Based DLL Example

Threading is used for more than a few server needs. For example, when a user accesses a website and requests service from an ISAPI Extension, the DLL must track the user identification as well as operate in a thread-safe manner. The same can be said for many server services. One or more users can send information to the service, either directly or as part of another request.

The following sections show how to create a Windows service. In this case, the service monitors user access, updates an internal variable showing the last service, and provides a command for writing the number of accesses to an event log entry. The client application generates stop, start, access, and access log requests.

Creating the Windows Service

The initial part of creating a Windows service is configuration of the Windows Service project. In fact, the wizard performs a large part of the setup for you. All you really need to worry about is the service functionality—at least if you're creating a basic service that doesn't require low-level system access such as device driver interfaces. The following steps show how to create the service. (You'll find the complete source code for this example in the \Chapter 07\ServerThread folder on the CD.)

1.Create a new Windows Service project. The example uses a name of Server Thread. You'll see an initial Designer where you can place controls for the service to use. This example won't use any special controls.

2.Select the Designer window. Note that the entries in the Properties window (shown in Figure 7.4) help you configure your service. The figure shows the settings for the example service. The main property you need to set is ServiceName. Notice the Add Installer link at the bottom of the Properties window—you must add an installer after completing the service configuration.

Figure 7.4: The Service properties enable you to configure general service features.

3.Perform any required configuration of class, file, and service names. It's better to finish the service configuration before you add an installer. Otherwise, you'll need to configure both the service and the installer manually.

4.Create an installer by clicking the Add Installer link at the bottom of the Properties window shown in Figure 7.4. Notice that the wizard creates another Designer window for you. However, in this case, the Designer window contains two controls used to manage the service installation.

5.Select the serviceProcessInstaller1 control. Modify the Password, Username, and Account properties as needed for your service. In most cases, you'll want to leave the Password and Username properties blank, and set the Account property to

LocalSystem.

6.Select the serviceInstaller1 control. Modify the DisplayName and StartType properties as needed for your service. The example uses a value of Track Number of User Accesses for the DisplayName property. Generally, you'll want to set the StartType to Automatic for services that will run all of the time.

At this point, you should have a generic Windows service project. If you compiled and installed the service, at this point, you could start, stop, and pause it. However, the service wouldn't do much more than waste CPU cycles, because you haven't added any commands to it. Listing 7.6 shows one of several techniques you can use to support commands in a Windows service.

Listing 7.6: The Windows Service Performs Much of the Work

protected override void OnStart(string[] args)

{

// Set the number of accesses to 0. _NumberOfAccesses = 0;

}

public void DoAccess()

{

// Increment the number of accesses. _NumberOfAccesses++;

}

public void GetAccess()

{

// Write an event log entry that shows how many accesses occurred. this.EventLog.WriteEntry("The number of user accesses is: "

+ _NumberOfAccesses.ToString(), EventLogEntryType.Information, 1200, 99);

}

protected override void OnCustomCommand(int Command)

{

//Execute the default command. if (Command < 128)

base.OnCustomCommand(Command);

//Increment the number of accesses. if (Command == 128)

DoAccess();

//Write an event long entry.

if (Command == 129) GetAccess();

}

The two commands, DoAccess() and GetAccess() work with a private variable named _NumberOfAccesses. The only purpose for this variable is to record the number of times someone accesses the service since it was last started. To ensure that the variable always starts at a known value, the OnStart() method sets it to 0. The OnStart() method is one of several methods the wizard assumes you'll need to override, so it provides this method by default.

The user still can't access your service. We'll see later that accessing a service is a bit cryptic in C#, most likely because of the way Windows services work in general. You must override the OnCustomCommand() method if you expect the user to interact with the service directly. Notice that this method transfers any commands below 127 to the base class. It also supports two other commands, 128 and 129, which call the appropriate command. The service ignores any other command input number.

Compile the example and you'll end up with an EXE file. The EXE file won't run from the command prompt and you can't install it in a project. You must install the service using the InstallUtil ServerThread.EXE command. If you want to uninstall the service, simply add the - u switch to the command line. After you install the service, open the Services console found in the Administrative Tools folder of the Control Panel. Figure 7.5 shows the service installed.

Figure 7.5: The InstallUtil utility will install the Windows service on any machine that will support it.

Creating the Test Application

The client application uses a standard dialog form containing buttons to exit the application, start the service, stop the service, register a service access, and display the number of service accesses since the last service start. To access the service, locate it in the Server Explorer dialog shown in Figure 7.6. Drag the service to the form in the Designer. This action will create a new ServiceController entry in the Designer that you can use to access the service.

Figure 7.6: User the Server Explorer to locate services on the local or any remote machine.

Now that you have access to the service, let's look at some code to interact with it. Listing 7.7 shows the code you'll use to work with a ServiceController named TrackAccess. Notice that some of the code enables and disables buttons as needed to ensure that they're accessible only when valid. This is a big concern for the developer because attempting to perform some actions with the service in an unknown state can have unfortunate side effects, such as data loss. At the very least, the application will generate an exception.

Listing 7.7: The Windows Service Client Can Stop, Start, and Access Service Commands

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

{

// Validate current service status.

if (TrackAccess.Status == ServiceControllerStatus.Running)

{

btnStart.Enabled = false;

}

else

{

btnStop.Enabled = false; btnAccess.Enabled = false; btnGetAccess.Enabled = false;

}

}

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

{

//Start the service. TrackAccess.Start();

//Wait for the start to complete. TrackAccess.WaitForStatus(ServiceControllerStatus.Running,

System.TimeSpan.FromMilliseconds(2000));

// Change the button configuration to match service status. btnStart.Enabled = false;

btnStop.Enabled = true; btnAccess.Enabled = true; btnGetAccess.Enabled = true;

}

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

{

// Stop the service.

if (TrackAccess.CanStop) TrackAccess.Stop();

else

{

// We can't stop the service, so exit. MessageBox.Show("Service doesn't support stopping.",

"Service Stop Error",

MessageBoxButtons.OK,

MessageBoxIcon.Error);

return;

}

// Wait for the start to complete. TrackAccess.WaitForStatus(ServiceControllerStatus.Stopped,

System.TimeSpan.FromMilliseconds(2000));

// Change the button configuration to match service status. btnStart.Enabled = true;

btnStop.Enabled = false; btnAccess.Enabled = false; btnGetAccess.Enabled = false;

}

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

{

// Access the service to increment the counter. TrackAccess.ExecuteCommand(128);

}

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

{

EventLog MyEvents; // Event log containing service entries.

//Accesss the service to report the number of accesses. TrackAccess.ExecuteCommand(129);

// Open the event log.

MyEvents = new EventLog("Application");

// Look at each event log entry for the correct message. foreach (EventLogEntry ThisEvent in MyEvents.Entries)

{

//The message will contain a category number of 99

//and an event ID of 1200.

if (ThisEvent.CategoryNumber == 99 && ThisEvent.EventID == 1200)

// Display the message. MessageBox.Show(ThisEvent.Message);

}

}