Register
Tuesday, March 16, 2010
 
 DBAs And ProgrammersBlog
  
News! Minimize
   
 
 Print   
 
Misc Blog Stuff Minimize
   
 
 Print   
 
The Reluctant DBA Minimize
 
 
 
 Print   
 
Reluctant DBA Minimize
   
 
  
 
Reluctant DBA Minimize
   
 
  
 
The Reluctant DBA Minimize
 
Feb23

Written by:CarpDeus
2/23/2009 12:05 PM 

A couple of weeks ago I talked, briefly, about writing Windows Services. Since this is something we do a lot of for various processing, I decided to create templates I could use rather than having to keep copying and pasting code. So, if you want to follow along, download the templates from svn://finsel.com/public/VS2008/Templates. That's my public SVN site and you can download either the individual templates or the BasicServiceTemplate.zip which contains the other three templates. It's also available here.

You need to put the three zip files in .\My Documents\Visual Studio 2008\Templates\ProjectTemplates and restart VS 2008 but then you should have the following three templates show up in My Templates when you go to create a new Visual C# application:

  • BasicApplicationForService
  • BasicService
  • BasicWorkerClass

Unfortunately, I don't know how to create a solution based template so create a solution and add each of these projects to it. I should note that these templates us Log4Net so you will either need to install that or remove the code that references it.

 

WorkerClass

The first thing I want to look at is the WorkerClass. This is where all of the real work is done. It has a three basic properties:

  • KeepProcessing: This is a bool flag that is used to stop processing mid-stream for threaded processing requests
  • ProcessedCount: This is an int that keeps track of how many items have been processed on the background thread
  • dbConnection: A database connection, since almost all of the services I write need at least one.

There are also some private variables for handling logging and the backgroundThread. This is, as you might guess, a Thread object. During the instantiation of the class, it does nothing and is set to null.

There are also two key methods for this class. The easiest one is ProcessData. In its entirety, the stub looks like this:

public void ProcessData()
{
ProcessedCount = 0;
KeepProcessing = true;
while (KeepProcessing)
{
  // Do something
  ProcessedCount++;
  if(true==false)
    break;
}
}

All this stub does is keep looping and incrementing ProcessedCount until something causes it to stop. In some cases, such as processing data queued up in a database table, it might be that the request for a new record returns null. In a Message Queue listener, you might never exit the loop. In either case, calling ProcessData will start the real work of the class. Depending on how intensive this is, you may want to just have your timer call this method directly. But you can always call StartProcessing instead.

StartProcessing basically calls ProcessData on a thread. This is done with four simple lines of code encapsulated in a method:

public void StartProcessing()
{
if (backgroundThread != null)
  backgroundThread.Abort();
backgroundThread = new Thread(new ThreadStart(ProcessData));
backgroundThread.Start();
}

It spins up thread assigned to backgroundThread to execute ProcessData and then starts the thread processing. Any call to StartProcessing a second time will basically kill the process running on backgroundThread and start it up again.

And, if you look at ProcessData, you'll notice that there are two ways for it to end: running out of items to process (which should exit the while loop) or having KeepProcessing set to false. In either case, it will end and backgroundThread will be released.

BasicApplicationForService

The second template, BasicApplicationForService, is a simple WinForms application that we can use to call and test the WorkerClass. It's a simple form:

BasicServiceApplication

It has a checkbox that can be used to turn the timer on and off, a button that can be used to call ProcessData to run through the data once and StopThread which will change the WorkerClass KeepProcessing to false to stop the processing mid stream.

I'm not using a Timer object on the form, which actually makes the code a bit more complicated because the Timer object exists on a thread different from the form. What that really means is that I need to add a couple of delegates to handle details like changing the cursor to show that the form is busy, but that's part of the main reason I planned to write this post so let's dig into delegates and threading and such.

Delegates

Delegates in C# are very similar to function pointers. Most of the time, we call our methods directly. So we can define a method like this:

private void Notify(string Message)
{
this.textBox1.Text = Message;
Application.DoEvents();
}

and then call it like this: Notify("This is an example"); Very straight forward. But sometimes you can't do that, perhaps because you don't know about the function you actually want to call. If you look at the code in the WorkerClass, you'll see that it has a method used to communicate back to the class that called it to tell it what's happening. Because it's loosely coupled, there's no way for it to really know that it's supposed to call the Notify method defined above, so we do two things. First, we define a handler in the class that defines the shape of the delegate, in this case it returns void and has a single string argument. At the same time, we also define a variable to hold the pointer.

public delegate void LogHandler(string message);
private LogHandler mvLogHandler;

Next, in the WinForm, we tell the class that we have a method that we want the class to call via the delegate and pass it in the name of the function we defined:

wc.OnLogHandler(Notify);

That's all it is, it's really simple. As long as the types of arguments match, all is well with the world. And the only thing that the class needs to do is to see if a delegate has been defined:

void NotifyCaller(string msg)
  {
   if (mvLogHandler != null)
    mvLogHandler(msg);
  }

In this case, if the variable to hold the pointer is null, we don't need to do anything. If it's not, we use the variable as though it were the method.

This is easily understandable when using a separate class, but we also need to use it for threads. The reason for this is that threads run separately and so need a safe way to call back and manipulate the original form. In our WinForm application, we define two delegates that can be used by the timer1 object: NotifyCallback which is the delegate for Notify and ChangeCursorCallback which is a delegate for ChangeCursor. If I had dragged a Timer object on to the form, I wouldn't need these but I chose to use the Timer class.

Threads

I'm not going to cover threads in detail, check out MSDN on the subject for a good introduction, but I will talk about it briefly. Threading basically allows you to do long running processes without appearing to be running long running processes. Timers run on a separate thread, which is why the first line of the timer interrupt method should always disable the timer. And you should never break on that line, always the line after. Otherwise you can end up with multiple instances of the execution queued up, which gets messy. As for the code the timer is actually executing when it gets called, let's take a look at that and then dissect the steps.

private void timer1_Tick(object sender, EventArgs e)
  {
   // Disable the timer so it doesn't go off again while we are processing
   timer1.Enabled = false;
   // Display hourglass
   ChangeCursorCallBack c = new ChangeCursorCallBack(ChangeCursor);
   this.Invoke(c, new object[] { Cursors.WaitCursor });
   // Get processed count from class, could be working on a background thread
   int Processed = wc.ProcessedCount;

   // If the class is in KeepProcessing mode then it is doing work on a background thread
   if (wc.KeepProcessing)
   {
    // Get information from the class and update the form
    toolStripStatusLabel1.Text = string.Format("Background Thread processing at {0} / Processed {1} records", DateTime.Now.ToLongTimeString(), wc.ProcessedCount);
   }
   else // Not currently processing
   {
    // Kick off processing
    wc.StartProcessing();
    // Update form
    toolStripStatusLabel1.Text = string.Format("Kicked off background thread processor {0} / Last round processed {1} records", DateTime.Now.ToLongTimeString(), Processed);
   }
   // Rest the cursor
   this.Invoke(c, new object[] { Cursors.Default });
   // Enable the timer
   timer1.Enabled = true;
  }

As I mentioned in the Delegates section, we aren't allowed to directly call and change objects on the form, so we use the delegates. In this case we explicitly use this.Invoke to call the delegate. But that's not the really interesting part of this function. The interesting part is where we call the WorkerClass.

First, we check to see if KeepProcessing is true. If it is then we know that the class is doing something on the background thread and all we need to do is report progress. If KeepProcessing is false then we know that, for some reason, the process ended, and so we kick it off again. There are several reasons the process may not be running:

  • It finished
  • It was stopped because KeepProcessing was set to false
  • The thread ended for some reason unrelated to our processing

The most likely cause of KeepProcessing being false is one of the first two, so I'm not going to worry about addressing the third.

Why a WinForm?

But you may be wondering, why a class that's going to be run as a service needs a WinForm implementation at all. The best answer is: Debugging. If you want to debug a service, it has to be running and you have to attach to it and, while all of that is doable, this is a whole lot easier.  As a general rule, I will run any new service in WinForm shape for a day or two so that I can see any bugs or unexpected issues that crop up. It gives me a chance to kick the tires of the class, starting with the Run Once button. Using that, I can step through the process and make sure that it does what I expect it to do. Once I am fairly certain that the code in the ProcessData method is stable, I can click the Run on Timer checkbox. Since that does little more than spin up the ProcessData on a thread, there's a good chance that I won't encounter any new errors.

If I can queue up a bunch of data to be processed, I might go ahead and start the timer and click on the Stop Thread button to make sure that the class correctly ends the thread when that's called, but that's a minor point.

Finally, when I'm sure that everything has been working just the way I want, I'm ready to kick off the service.

BasicService

The basic service requires very little work, provided everything tested correctly in the WinForm. The definitions for Start/Stop/Pause/Continue/Shutdown are all there and really boil down to doing one of two things. If we are starting or restarting the service, we enable the timer. If we are stopping the service for any reason, we disable the timer and set KeepProcessing in the class to false. And that's all there is to it.

Generally all I have to do is copy any settings into the service, possibly make a minor change in the Initialize to mimic whatever special initializations were required in the form (usually additional public settings for the class or settings that need passed into the class) and I'm ready to build and install the service.

The only real change I make is to change timer1.Enabled to false in the start up so I can attach to the service before it starts doing anything. Then I set a break point in OnContinue, start the service, attach to the service, pause the service and continue the service, at which point my debugger should be happy.

Finally, I let the service run for a few days as a service to make sure it doesn't throw any errors. Once I'm satisfied, then I can roll it out to be installed on whatever box needs to sit and process it.

Hope this helps

Josef

 (More on services)

Copyright ©2009 Carp Deus

Tags:

Your name:
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
Enter the code shown above in the box below
Add Comment  Cancel 
 
 
  
 
Privacy Statement | Terms Of Use Copyright 2001-2008 by ReluctantDBA.com