Coding Techniques for Microsoft Visual Basic .NET by John Connell, Paperback | Barnes & Noble
Coding Techniques for Microsoft Visual Basic .NET

Coding Techniques for Microsoft Visual Basic .NET

4.0 3
by John Connell
     
 

This unique title goes beyond simply using academic snippets of code to demonstrate a point or language construct to teach Visual Basic.Net. Designed for the beginning, self-taught, or even experienced programmers who are switching to Microsoft Visual Basic.Net from other languages, this book provides insights.

Overview

This unique title goes beyond simply using academic snippets of code to demonstrate a point or language construct to teach Visual Basic.Net. Designed for the beginning, self-taught, or even experienced programmers who are switching to Microsoft Visual Basic.Net from other languages, this book provides insights.

Editorial Reviews

bn.com
The Barnes & Noble Review
As John Connell puts it, traditional Visual Basic programs have reached their "glass ceiling." Classic VB offers no direct access to underlying APIs, no inheritance, and less-than-ideal scalability and "tune"-ability. To the rescue: Visual Basic .NET. Daunted by the looming transition? Read on.

Or, more precisely, read Coding Techniques for Microsoft Visual Basic .NET.

This book thoroughly illuminates VB.NET for experienced developers. Connell, who currently leads a large team of enterprise VB.NET developers, teaches the language through the construction of real business applications -- not what he calls "academic snippets." He starts with a high-level, uncommonly clear explanation of the .NET framework, and follows with an equally elegant discussion of VB.NET's now full-fledged object-orientation.

Using a VB.NET form, Connell demonstrates how objects are spawned from classes; and how properties, methods, inheritance, and namespaces work. You'll write your first class, then create new classes that inherit from it, and then master reference and primitive data types -- nice in VB6, crucial in VB.NET.

Gradually, using significant chunks of code, you'll build your VB.NET skills. You'll master arrays and collections, then error handling and debugging (which are taught through the construction of a generic error-handling class that provides trace logs and can be integrated into any VB.NET program you write).

There's a comprehensive chapter on assemblies, VB.NET's fundamental building block for deployment, version control, reuse, and security. Connell goes far beyond the basics, introducing powerful techniques such as strongly named assemblies, which allow two assemblies with the same name to coexist in the same folder without DLL conflicts (thank goodness!)

The book includes systematic coverage of data access and ADO.NET (including ADO.NET's impressive XML-based features for data sharing among heterogeneous systems). You'll also build complete web applications and services from scratch, including a working ASP.NET-based loan calculator. Oh, and relax: All this code's on CD-ROM, along with a fully searchable copy of the book. (Bill Camarda)

Bill Camarda is a consultant, writer, and web/multimedia content developer with nearly 20 years' experience in helping technology companies deploy and market advanced software, computing, and networking products and services. He served for nearly ten years as vice president of a New Jersey–based marketing company, where he supervised a wide range of graphics and web design projects. His 15 books include Special Edition Using Word 2000 and Upgrading & Fixing Networks For Dummies®, Second Edition.

Product Details

ISBN-13:
9780735612549
Publisher:
Microsoft Press
Publication date:
12/01/2001
Edition description:
BK&CD-ROM
Pages:
633
Product dimensions:
7.44(w) x 9.26(h) x 1.78(d)
Age Range:
13 Years

Related Subjects

Read an Excerpt

Chapter 9.
File System Monitoring


  • The File Sentinel Program
    • How the File Sentinel Program Works
    • Starting to Write the File Sentinel Program
    • Adding the Sentinel Class to Our Program
    • Delegates
    • Handling the Changed, Created, and Deleted Events
    • Handling the Renamed and Error Events
    • Writing to Our Log File
    • Wiring Up the User Interface
    • Possible Enhancements to the File Sentinel
  • Introduction to Windows Services
    • The Life and Death of a Service
    • Building Our File Sentinel into a Windows Service
    • Adding Our Sentinel Class to Our Service
    • Updating the Service1.vb File
    • How Our Service Works
    • Looking at vbMonitorService in the Services Window
    • Debugging a Windows Service
    • Conclusion


Chapter 9   File System Monitoring

I've come across applications that are designed to wait for files to show up in a particular directory and then process them—for example, an application that imports data from a file into a database. Data files can be downloaded from a mainframe or transferred to an input directory by some other means, and then an application imports them into a database. Instead of constantly polling the directory for new files, the application can wait for notifications indicating that a new file has been created. You can create programs with this capability in Visual Basic 6, but you have to use and understand Win32 APIs. This task becomes trivial in Visual Basic .NET by using the .NET Framework classes. The implementation of such a program in Microsoft .NET is also consistent with the way you do everything else in .NET, so the learning curve is minimal.

The .NET Framework has a built-in class named System.IO.FileSystemWatcher that a program can use to watch the file system. This class provides properties that let you set which path to monitor and specify whether you are interested in changes at the file or subdirectory level. The System.IO.FileSystemWatcher class also lets you specify which filenames and types to watch for. (For example, *.txt is the instruction you use to watch for changes to all text files.) Finally, you can even specify the types of changes you're interested in monitoring—for instance, new file creations, changes to file attributes, or changes to file size.

After you establish what to watch and what to watch for, you can wire in event handlers for the various events that interest you. The FileSystemWatcher class events that we can trap are Changed, Created, Deleted, Error, and Renamed. To handle an event, you write an event handler with the same declaration as the FileSystemEventHandler delegate and then add this handler to the FileSystemWatcher class. (The program we build in this chapter will illustrate the use of delegates.) This delegate-based architecture lets you add multiple handlers for the same event or use one handler for multiple events, which you couldn't do in Visual Basic 6.

The File Sentinel Program

The File Sentinel program will help you learn more about the .NET Framework and will be immediately useful to you if your work involves network administration. This program runs in the background and monitors any directory or file changes. If you help administer a network, you probably want to keep an eye on certain files and be notified if they are changed. You might also want to run a program such as File Sentinel on your Web server to notify you if someone (a.k.a., a hacker) is tampering with files on your server. Or you might want to have this program monitor your cookies directory to see whether there is any unauthorized access to your PC.

Many times, network managers in larger organizations create a dummy file with a tempting name such as salary.xls or passwords.bin and check to see whether anyone tampers with it. This technique is called placing a "honey pot" on the network. Like a high-tech sting operation, the honey pot is posted for any snooping user to attempt to view. The File Sentinel program could be set to monitor the honey pot file and notify you when someone tampers with it. If a user tries to take the honey, our File Sentinel program helps sting them.

The File Sentinel program can monitor files on either a local machine or across a network. In the process of creating this program, you'll learn more about creating and writing to files, events, and the new .NET delegate. In Visual Basic 6, events were acts of God—you could use only what you were given. Visual Basic .NET delegates permit us to add our own events to a program.


NOTE:
The File Sentinel program works only with Windows 2000 or Windows NT 4. Unfortunately, the .NET Framework does not have the plumbing for this particular program for Windows 9x or Windows Me. Also, the FileSystemWatcher class can watch disks as long as they are not switched or removed. FileSystemWatcher does not raise events for CDs and DVDs because time stamps and properties cannot change. Remote machines must have one of these operating system platforms installed for the component to function properly. Unfortunately, however, you cannot watch a remote Windows NT 4 computer from a Windows NT 4 computer. Hopefully, these limitations will be removed in later editions of the .NET Framework.

How the File Sentinel Program Works

Before we write the code for this program, let's take a look at what it does. In File Sentinel, the user selects either a file or a folder to monitor. Notice in Figure 9-1 that the Disable Sentinel button is disabled. The user must first select either a file or a directory to monitor before the program can actually do anything. But, just in case a user has a quick trigger finger and clicks Enable Sentinel before making a selection, we default to the current drive. As always, we protect the user from simple or thoughtless mistakes.

Figure 9-1   The File Sentinel program. Nothing happens until the user selects a file or a folder. (Image unavailable)

The program also includes a few tooltips to ensure that our users know how to operate the software, as shown in Figure 9-2. A tooltip is extremely easy to implement—it takes only a single line of code—and it provides an application with a professional, finished look. In earlier versions of Visual Basic, we used the Tag property of a control to hold a string with Help information about the control. Then, through the convoluted use of a timer and the Mouse_Move event, we added code to manually display a tooltip. In Visual Basic .NET, we now have a much easier way to display tooltips.

Figure 9-2   We implement a tooltip to help users know what to do. (Image unavailable)

We will write the output generated by the program to a simple text file. Of course, you could easily design the program to send e-mail messages to your machine or even page you with a message if someone tampers with files on the server. But for now, a simple text file will do nicely.

The program will write the date and time of the tampering, the file or files affected, and what type of activity was detected. If we open the output file in Notepad, we can see the date and time of each monitored access. Figure 9-3 shows that a file named trapdoor.bin was renamed to trash.doc and then examined. I think you'll agree that this program can be useful to you right away.

Figure 9-3   Output from the File Sentinel program is written to a text file. (Image unavailable)

Starting to Write the File Sentinel Program

As I've mentioned in previous chapters, the three steps in writing a Visual Basic .NET program are:

  1. Draw the interface.
  2. Set the properties of the controls.
  3. Write the code.

Unlike the days before visual languages such as Visual Basic and Visual C++, when the user interface was thrown on as an afterthought, in Visual Basic .NET, building the interface is the first step we want to perform. Even though the File Sentinel program is a small one, building the interface first is a low-rent way to prototype its look and feel. We want to get the interface right from the start. Remember, to the user, the interface is the program.

Adding Controls to the Toolbox

We need to add three controls to the toolbox: the DirListBox, DriveListBox, and the FileListBox. These controls are old friends from classic Visual Basic that have been revamped to work in .NET. To add the controls, right-click the toolbox and then select Customize Toolbox. Click the .NET Framework Components tab, shown in Figure 9-4, select the controls, and then click OK.

Figure 9-4   These three controls will be added under the Windows Forms tab of your toolbox. (Image unavailable)

Building the User Interface and Setting the Properties

Create a new Visual Basic .NET Windows project named FileSentinel, add the controls listed in Table 9-1 to the default form, and set the values for the properties listed. Your form should look similar to Figure 9-5 in design mode.

Table 9-1 Controls and Property Settings for the File Sentinel

ControlPropertyValue
ButtonNamebtnEnable
Text&Enable Sentinel
ButtonNamebtnDisable
Text&Disable Sentinel
LabelNamelblWatching
BackColorLime
BorderStyleFixed3D
FormFormBorderStyleFixedDialog
Icon<Your choice>
TextFile Sentinel
StartPositionCenterScreen
LockedTrue
ToolTipNamettTip
DriveListBoxNameDriveListBox1
DirListBoxNameDirListBox1
FileListBoxNameFileListBox1


NOTE:
As I mentioned in Chapter 2, "Object-Oriented Programming in Visual Basic .NET," because the tooltip is not visible in the finished product, the control is placed in the trough below the form, where controls such as a ToolTip, the Error Provider, the Timer, and others that are not visible are placed. The trough is a nice touch because in the Visual Studio .NET IDE, controls you put there don't take up any valuable real estate on the form as you design the user interface.

Figure 9-5   The File Sentinel form with the interface controls added. (Image unavailable)

Now that we've drawn the controls and set their properties, it's time to roll up our sleeves and write some code. We're going to encapsulate the functionality of the File Sentinel program in a class. The form we just drew will be its face to the outside world.


A Word on Legacy ActiveX Controls

You might be wondering why we don't use any of the COM ActiveX .ocx controls we're familiar with. Remember that the common language runtime (CLR) manages all code that runs inside the .NET Framework. Code that executes under the control of the CLR is called managed code. Conversely, code that runs outside the CLR is called unmanaged code. COM components, ActiveX interfaces, and Win32 API functions are all unmanaged code.

Of course, you might have built some custom COM controls or have purchased several expensive controls that are not yet available for .NET. In many cases, it is neither practical nor necessary to upgrade a COM component simply to incorporate its features into your managed application. Accessing existing functionality through interoperation services provided by the CLR often makes more sense.

.NET Windows forms can only host controls that are part of System.Windows.Forms.Control. For .NET to use a legacy ActiveX control, you need to make it appear as a Windows Forms control. By the same token, the ActiveX control does not expect to be hosted by .NET but instead by an ActiveX container. Fortunately, the System.Windows.Forms.AxHost class does the trick here. This class is really a Windows Forms control on the outside and an ActiveX control container on the inside. Essentially, the AxHost class creates a wrapper class that exposes its properties, methods, and events. Some ActiveX controls will work better than others, but if you really need to use a legacy control, fire up the WinCV program we reviewed earlier in Chapter 5, "Examining the .NET Class Framework Using Files and Strings," and see what it's all about.


Adding the Sentinel Class to Our Program

The .NET Framework FileSystemWatcher class is so handy that it's provided as a component in the toolbox. While we could add a FileSystemWatcher control to our form and set some properties, we're instead going to build our own component in a class. We take this step because we want to inherit from the built-in framework class and then add functionality, such as writing to a file and permitting the user to select files or directories to monitor. You can see the FileSystemWatcher component in Figure 9-6, under the Components tab of the toolbox.

Figure 9-6   The Components tab of the toolbox. (Image unavailable)

Adding a Class to Our Project

The class that implements our FileSystemWatcher component does all the heavy lifting for our program. When we finish the class, we will wire it into the user interface. But for now, let's understand how the class works.

  1. Select Project | Add Class.
  2. Select Class from the templates available, name the class Sentinel.vb, and click Open. Delete the skeleton code placed in the class, and then add the following code:
  3. Imports System
    Imports System.Diagnostics
    Imports System.IO
    Imports System.Threading
     
    Namespace SystemObserver
     
    Public Class sentinel
     
    Private m_Watcher As System.IO.FileSystemWatche r
    Private m_ObserveFileWrite As StreamWriter
    Private fiFileInfo As FileInfo
     
    Public Sub New(ByVal sToObserve As String)
    m_Watcher = New FileSystemWatcher()
    fiFileInfo = New FileInfo(sToObserve)
     
    If (fiFileInfo.Exists = False) Then
     
    If (Not sToObserve.EndsWith("\")) Then
    sToObserve.Concat("\")
    End If
     
    With m_Watcher
    .Path = sToObserve
    .Filter = ""
    .IncludeSubdirectories = False
    End With
    Else
    With m_Watcher
    .Path = fiFileInfo.DirectoryName.ToS tring
    .Filter = fiFileInfo.Name.ToString
    .IncludeSubdirectories = False
    End With
    End If
     
    m_Watcher.NotifyFilter = _
    NotifyFilters.FileName Or _
    NotifyFilters.Attributes Or _
    NotifyFilters.LastAccess Or _
    NotifyFilters.LastWrite Or _
    NotifyFilters.Security Or _
    NotifyFilters.Size Or _
    NotifyFilters.CreationTime Or _
    NotifyFilters.DirectoryName
     
    AddHandler m_Watcher.Changed, AddressOf OnCh anged
    AddHandler m_Watcher.Created, AddressOf OnCh anged
    AddHandler m_Watcher.Deleted, AddressOf OnCh anged
    AddHandler m_Watcher.Renamed, AddressOf OnRe named
    AddHandler m_Watcher.Error, AddressOf onErro r

    m_Watcher.EnableRaisingEvents = True
    m_ObserveFileWrite = _
    New StreamWriter("C:\observer.txt", True )
    End Sub
     
    Private Sub OnChanged(ByVal source As Object, _
    ByVal e As FileSystemEventArgs)
     
    Dim sChange As String
     
    Select Case e.ChangeType
    Case WatcherChangeTypes.Changed : _
    sChange = "Changed"
    Case WatcherChangeTypes.Created : _
    sChange = "Created"
    Case WatcherChangeTypes.Deleted : _
    sChange = "Deleted"
    End Select
     
    If (Len(sChange) > 0) Then
    If (e.FullPath.IndexOf("observer.txt") > 0) _
    Then
     
    Exit Sub
    End If
    End If
     
    writeToFile("File: " & e.FullPath & _
    " " & sChange)
    End Sub
     
    Private Sub OnRenamed(ByVal source As Object, _
    ByVal e As RenamedEventArgs)
     
    writeToFile("File: " & e.OldFullPath & _
    " remaned to " & e.FullPath)
    End Sub
     
    Private Sub onError(ByVal source As Object, _
    ByVal errevent As ErrorEventArgs)
     
    writeToFile("ERROR: " & _
    errevent.GetException.Message())
    End Sub
     
    Private Sub writeToFile( _
    ByRef observeString As String)
    Dim sRightNow As String = _
    Date.Now.ToLongDateString() & _
    " " & Date.Now.ToLongTimeString()
     
    Try
    m_ObserveFileWrite.WriteLine(sRightNow & _
    " " & observeString)
    m_ObserveFileWrite.Flush()
    Catch
    End Try
    End Sub
     
    Public Sub dispose()
    m_Watcher.EnableRaisingEvents = False
    m_Watcher = Nothing
    m_ObserveFileWrite.Close()
    End Sub
     
    End Class
     
    End Namespace

How the Code Works

We want to import these four namespaces:

Imports System
Imports System.Diagnostics
Imports System.IO
Imports System.Threading

Next we wrap our class in the SystemObserver namespace. The name of our class within the namespace is sentinel. We declare three private class member variables. The first is the variable m_Watcher, of type FileSystemWatcher. The FileSystemWatcher framework class lives in the System.IO namespace.

Because we want to write our output to a file, we also create a member variable m_ObserveFileWrite as type StreamWriter. The third variable we include is needed to check whether we will be monitoring a file or a directory. The fiFileInfo variable of type FileInfo will provide the methods we need. Because these variables are scoped at the top of the class, they are visible to the entire class.

Namespace SystemObserver
 
Public Class sentinel
 
Private m_Watcher As System.IO.FileSystemWatcher
Private m_ObserveFileWrite As StreamWriter
Private fiFileInfo As FileInfo

When we instantiate a sentinel object, we will pass into the class's constructor as a string the path of either the file or the directory we want to monitor. A new instance of the FileSystemWatcher class is instantiated, but at this point we don't know whether the string variable sToObserve contains a file or a directory to monitor. We use the FileInfo class to determine which it is.

Public Sub New(ByVal sToObserve As String)
m_Watcher = New FileSystemWatcher()
fiFileInfo = New FileInfo(sToObserve)

Configuring the FileSystemWatcher

We need to set several properties of the FileSystemWatcher class that affect how it behaves. These properties determine what directories and subdirectories the object will monitor and the exact occurrences within those directories that will raise events.

The first two properties that determine what directories FileSystemWatcher should watch are Path and IncludeSubdirectories. The Path property indicates the fully qualified path of the root directory to watch. The property's value can be set in standard directory path notation (c:\directory) or in UNC format (\\server\directory). The IncludeSubdirectories property indicates whether subdirectories within the root directory should be monitored. If this property is set to True, the component watches for the same changes in the subdirectories as it does in the main directory that it is watching. However, you will not be happy to find that if you set IncludeSubdirctories to True, each event you want to watch might generate an additional 10 to 15 unwanted events. The Windows operating system generates tons of messages on all sorts of internal files each and every time a user changes a file. I've found that it's better to leave IncludeSubdirectories set to False if you are monitoring the root directory of the drive.

If the user passes in the fully qualified path of a file, the fiFileInfo.Exists method returns True. If the path is a directory, the Exists method returns False. So, let's first check for any directories.

If the user selects a directory path such as "C:\", we have no problem. However, if the user selects a path such as "C:\Program Files\Common Files," we want to place a backslash (\) to delimit the directory. Using two methods of the String object makes doing this a snap. If the string sToObserve does not end with a backslash, we concatenate one. Couldn't be easier.

When a directory is selected, we set the path to the variable sToObserve. If the user wants to monitor all the files in the directory "C:\Program Files\Common Files," we would have added a trailing backslash character and set the Path property.

To monitor changes in all files, set the Filter property to an empty string (""). We do that here because we want to monitor all files in the selected directory. To monitor a specific file, set the Filter property to the filename. For example, to watch for changes in the file Passwords.bin, set the Filter property to "Passwords.bin". You can also watch for changes in a certain type of file. For example, to watch for changes in any Microsoft Word files, set the Filter property to "*.doc".

When a user selects a file instead of a directory to monitor, we know that the fiFileInfo object has all the information about the file we need. It's easy to set the Path property of our FileSystemWatcher object by setting it to fiFileInfo.DirectoryName.ToString. This call returns the fully qualified directory name where the file is located. Likewise, the Name property provides the name of the file to monitor. In both cases—monitoring files or directories—we have set IncludeSubDirectories to False. This setting makes our log file cleaner. It will contain only relevant entries on the files in question.

If (fiFileInfo.Exists = False) Then
 
If (Not sToObserve.EndsWith("\")) Then
sToObserve.Concat("\")
End If
 
With m_Watcher
.Path = sToObserve
.Filter = ""
.IncludeSubdirectories = False
End With
Else
With m_Watcher
.Path = fiFileInfo.DirectoryName.ToString
.Filter = fiFileInfo.Name.ToString
.IncludeSubdirectories = False
End With
End If

Now that we have set the Path, Filter, and IncludeSubdirectories properties of the FileSystemWatcher object, we will specify which changes to watch for in a file or folder by setting the NotifyFilter property.

m_Watcher.NotifyFilter = NotifyFilters.FileName Or _
NotifyFilters.Attributes Or _
NotifyFilters.LastAccess Or _
NotifyFilters.LastWrite Or _
NotifyFilters.Security Or _
NotifyFilters.Size Or _
NotifyFilters.CreationTime Or _
NotifyFilters.DirectoryName

In our program, we are going to look for all types of changes by bundling them together with Or statements. Because these values are enumerated (in other words, represented by a number under the hood), using Or simply adds them together. Table 9-2 describes the different values for NotifyFilters.

Table 9-2 Values for the NotifyFilters Property

Member NameDescription
AttributesThe attributes of the file or folder
CreationTimeThe time the file or folder was created
DirectoryNameThe name of the directory
FileNameThe name of the file
LastAccessThe date the file or folder was last opened
LastWriteThe date the file or folder last had anything written to it
SecurityThe security settings of the file or folder
SizeThe size of the file or folder

We combined all the members of this enumeration in order to watch for all changes. You can easily select only one or two of the NotifyFilters properties by simply Oring them together as we did. For example, you can monitor changes in the size of a file or folder and for changes in security settings.

m_Watcher.NotifyFilter = NotifyFilters.FileName Or _
NotifyFilters.Attributes Or NotifyFilters.LastAcces s Or _
notifyFilters.LastWrite Or NotifyFilters.Security O r _
NotifyFilters.Size or NotifyFilters.CreationTime Or _
NotifyFilters.DirectoryName

Now you are probably wondering just how the events we want to monitor are wired to the event handlers. That's where the concept of a delegate comes in.

Delegates

As you know, an event is nothing more than a message sent by something to let whatever is listening know that something has happened. This may shock you, but in .NET, the object that triggers an event is known as the event sender, and the object that is listening is known as the event receiver. The problem we need to address is whether the event receiver knows what to listen for. We can send events all day long, but if no receiver is listening it does us little good.

Although the event sender does not need to know who is listening and the event receiver does not need to know who is sending, we still need to link the sender with a receiver to ensure that the event message is passed correctly. To do this, we use a delegate. A delegate formalizes the process of declaring a procedure that will respond to an event.

A delegate is used to communicate the message (of the event being triggered) between the source and the listener. A receiver registers the delegate with a sender, letting the sender know that the receiver will respond to the sender's events. Another powerful feature of delegates is multicast functionality, which means that a single sender can be dispatched to several receivers, acting as a one-to-many relationship. We are going to implement the reverse and use a delegate in a situation in which messages from several senders are sent to a single receiver. In this situation, we can easily determine which sender sent the message, making our code more streamlined and easier to read.

The FileSystemWatcher object knows how to raise four different events, depending on the types of changes that occur in the directory or file it is watching. These events are:

  • Created, which is raised whenever a directory or file is created.
  • Deleted, which is raised whenever a directory or file is deleted.
  • Renamed, which is raised whenever the name of a directory or file is changed.
  • Changed, which is raised whenever changes are made to the size, system attributes, last write time, last access time, or NTFS security permissions of a directory or file. Of course, as we have just seen, we can use the NotifyFilter property to limit the amount of events the Changed event raises.

For each of these four FileSystemWatcher events, we define handlers that call methods when a change occurs. Each event handler provides two parameters that allow you to handle the event properly—the sender parameter, which provides an object reference to the object responsible for the event, and the e parameter, which provides an object for representing the event and its information.

We know that an event is a message sent by an object to signal the occurrence of an action. The action might be caused by user interaction such as a mouse click, or it might be triggered by some other program logic or even the operating system itself. In our case, an event will be generated when a user performs an action such as renaming a file, for example.

The FileSystemWatcher component is the event sender in our example. The object or procedure that captures and responds to the event is the event receiver. In event communication, however, the event sender class does not know which object or method will receive (handle) the events it raises. Therefore, what is needed is an intermediary between the source and the receiver. The .NET Framework defines a special type, or Delegate, which provides this functionality.

In our example, we will add a handler for the Changed, Created, Deleted, Renamed, and Error events that can be raised by m_Watcher. We do this by using the AddressOf operator, which we use to create a function delegate. This delegate points to the function specified by the procedure name of the operator. Whenever our m_Watcher object triggers a Changed event, we want to know about it and probably do something. So, we can add an event handler, OnChanged, that will receive each Changed event. The following line from our program tells us that we are adding a handler to the m_Watcher.Changed event and that this handler can be found at the location specified by the AddressOf operator for the procedure OnChanged.

AddHandler m_Watcher.Changed, AddressOf OnChanged

After we add the delegate that instructs m_Watcher where to send any Changed events, we have to write the OnChanged event procedure itself. We make these procedures private so that they can be seen only in our class.

Private Sub OnChanged(ByVal source As Object, _
ByVal e As FileSystemEventArgs)
 
‘Do things when a file or directory is changed
 
End Sub

As I mentioned, we use the Visual Basic .NET AddressOf operator to create a function delegate that points to the function specified by procedurename, in this case OnChanged. I've used shorthand notation for creating our delegate; however, both of the following lines are equivalent:

AddHandler m_Watcher.Changed, AddressOf OnChanged
AddHandler m_Watcher.Changed, _
New EventHandler(AddressOf OnChanged)

With this code we have registered the receiver, OnChanged, with the sender of the message, m_Watcher.Changed. Each time our object m_Watcher raises a Changed event, it will be captured by the OnChanged event handler.

Notice that we are going to handle all five events that can be fired by the m_Watcher object. However, the Changed, Created, and Deleted events will all be handled by the OnChanged event handler, which shows our many-to-one relationship. The following code defines and registers the delegates:

AddHandler m_Watcher.Changed, AddressOf OnChanged
AddHandler m_Watcher.Created, AddressOf OnChanged
AddHandler m_Watcher.Deleted, AddressOf OnChanged
AddHandler m_Watcher.Renamed, AddressOf OnRenamed
AddHandler m_Watcher.Error, AddressOf onError

We wrap up our constructor code by setting the EnableRaisingEvents property to True. The object will not start operating until you have set both the Path and EnableRaisingEvents properties. (We covered writing to files in Chapter 5, so the StreamWriter object is an old friend by now.)

m_Watcher.EnableRaisingEvents = True
m_ObserveFileWrite = _
New StreamWriter("C:\observer.txt", True)

At this point, the m_Watcher object is ready and waiting for any relevant events to trap and write to our text file, C:\observer.txt.

Handling the Changed, Created, and Deleted Events

As I mentioned above, when the Changed, Created, or Deleted events are fired, each will be handled by the OnChanged event handler. The source parameter tells us who sent the event. The FileSystemEventArgs parameter contains information about the specific message. The ChangeType property of FileSystemEventArgs tells us what type of change occurred. We can simply interrogate FileSystemEventArgs to find out what occurred.

Let's take a brief detour, visit our friend the WinCV tool again, and search for FileSytemEventArgs. We will interrogate these properties to find out the type of change that occurred in the ChangeType property as well as the file affected in the FullPath property. Again, spend time with the WinCV tool not only for practice in reading the .NET classes, but also for finding out exactly what the classes can do.

public class System.IO.FileSystemEventArgs :
EventArgs
{
 
// Fields
 
// Constructors
public FileSystemEventArgs(
System.IO.WatcherChangeTypes changeType,
string directory, string name);
 
// Properties
public WatcherChangeTypes ChangeType { get; }
public string FullPath { get; }
public string Name { get; }
 
// Methods
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public Type GetType();
public virtual string ToString();
} // end of System.IO.FileSystemEventArgs

When one of these three events (Changed, Created, or Deleted) is fired by m_Watcher, the OnChanged event handler is called. We can determine which of the three events occurred and write a string literal to our local string variable, sChange.

Private Sub OnChanged(ByVal source As Object,  _
ByVal e As FileSystemEventArgs)
 
Dim sChange As String
 
Select Case e.ChangeType
Case WatcherChangeTypes.Changed : _
sChange = "Changed"
Case WatcherChangeTypes.Created : _
sChange = "Created"
Case WatcherChangeTypes.Deleted : _
sChange = "Deleted"
End Select

We now know what event was fired. When we write to our file, an event will also be fired for this change. We want to ignore changes to the observer.txt file.

If (Len(sChange) > 0) Then
If (e.FullPath.IndexOf("observer.txt") > 0) _
Then
 
Exit Sub
End If
End If

Finally, we can write the change to our file by calling our routine writeToFile. By passing in the FullPath property of the file and the type of change, we will know exactly what happened.

writeToFile("File: " & e.FullPath & "  " & sChange)

Handling the Renamed and Error Events

These event handlers are similar to those we learned about in the previous section. As another exercise, take a look at the WinCV tool and check out RenamedEventArgs. You can see that you can read the FullPath, OldFullPath, and OldName properties to know exactly what the file was renamed to.

public class System.IO.RenamedEventArgs :
System.IO.FileSystemEventArgs
{
 
// Fields
 
// Constructors
public RenamedEventArgs(
System.IO.WatcherChangeTypes changeType,
string directory, string name, string oldName);
 
// Properties
public WatcherChangeTypes ChangeType { get; }
public string FullPath { get; }
public string Name { get; }
public string OldFullPath { get; }
public string OldName { get; }
 
// Methods
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public Type GetType();
public virtual string ToString();
} // end of System.IO.RenamedEventArgs

Here we simply interrogate the RenamedEventArgs parameter to determine everything we need to know about a renamed file. Likewise, by interrogating ErrorEventArgs, we can see what type of error was generated. Both of these handlers build a string and pass it into our writeToFile routine to log the renaming and error events.

Private Sub OnRenamed(ByVal source As Object, _
ByVal e As RenamedEventArgs)
 
writeToFile("File: " & e.OldFullPath & _
" remaned to " & e.FullPath)
End Sub
 
Private Sub onError(ByVal source As Object, _
ByVal errevent As ErrorEventArgs)
 
writeToFile("ERROR: " & errevent.GetException.Messa ge())
End Sub

Writing to Our Log File

Having a timestamp for changes we are interested in can be helpful, so we dimension a string and grab the current time and date. The Try…Catch block was described in Chapter 7, "Handling Errors and Debugging Programs," so that part of the code should be familiar to you. Because we might have trouble writing to a file, we place the file-access code in the protected Try block. Catch is empty, but it will catch any error and not cause our program to crash and burn if there is any difficulty writing to our file. We then use the WriteLine method of the StreamWriter object being held in the private member variable m_ObserveFileWrite. After we write the entry, calling the Flush method ensures that the line is immediately written to disk.

Private Sub writeToFile(ByRef observeString As String)
Dim sRightNow As String = _
Date.Now.ToLongDateString() & _
" " & Date.Now.ToLongTimeString()
 
Try
m_ObserveFileWrite.WriteLine(sRightNow & _
" " & observeString)
m_ObserveFileWrite.Flush()
Catch
End Try
 
End Sub

Of course, when our object is released, the dispose method of the class is called. We turn off capturing events by setting EnableRaisingEvents to False, set the object to Nothing, and then close the file.


NOTE:
Remember that setting our object to Nothing only flags the object for deletion but, unlike in Visual Basic 6, does not immediately release memory and resources. These operations are performed the next time the garbage collector makes its rounds. It will see that our object is flagged for deletion and remove it, but its removal could be up to several minutes later.

Public Sub dispose()
m_Watcher.EnableRaisingEvents = False
m_Watcher = Nothing
m_ObserveFileWrite.Close()
End Sub

That wraps up our class that monitors important events in files and directories. Now let's wire it to our user interface.

Wiring Up the User Interface

Ready to write some code? In the Visual Basic .NET IDE, switch to the Form1.Vb tab so that you can start writing the code required to use our file sentinel. As usual, we really don't have to write much code for what this program does. The functionality we get even without writing much code again illustrates how powerful the .NET Framework is. Add the following code to Form1.vb:

Imports FileSentinel.SystemObserver.sentinel

Public Class Form1
Inherits System.Windows.Forms.Form
 
Private m_sFilesToScan As String
Private m_fsSentinel As _
FileSentinel.SystemObserver.sentinel


Public Sub New()
MyBase.New()
 
‘ This call is required by the Windows Form Des igner.
InitializeComponent()
 
‘ Add any initialization after the
‘ InitializeComponent() call
 
lblWatching Text = DriveListBox1.Drive.ToUpper & "\"
 
‘-- Call our private routine that initializes the GUI
InitializeGUI()
 
End Sub
 
Private Sub InitializeGUI()
 
‘-- Disable the options until a legitimate Dir/File
‘ selection is made
btnEnable.Enabled = True
btnDisable.Enabled = False
 
ttTip.SetToolTip(btnEnable, _
"Enable the File Sentinel to monitor a folder " & _
"or directory. ")
ttTip.SetToolTip(btnDisable, _
"Stop monitoring a folder or directory.")
ttTip.SetToolTip(DriveListBox1, _
"Select the drive to monitor a folder or " & _
"directory.")
End Sub
 
Private Sub btnDisable_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnDisable.Click
 
m_fsSentinel.dispose()
btnEnable.Enabled = True
btnDisable.Enabled = False
DriveListBox1.Enabled = True
DirListBox1.Enabled = True
FileListBox1.Enabled = True
End Sub
 
Private Sub DriveListBox1_SelectedIndexChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles DriveListBox1.SelectedIndexChanged
 
Try
DirListBox1.Path = DriveListBox1.Drive
lblWatching.Text = DriveListBox1.Drive
Catch
DriveListBox1.Drive = DirListBox1.Path
End Try
 
End Sub

Private Sub DirListBox1_SelectedIndexChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles DirListBox1.SelectedIndexChanged
 
Try
FileListBox1.Path = DirListBox1.Path
lblWatching.Text = DirListBox1.Path
Catch
End Try
End Sub
 
Private Sub FileListBox1_SelectedIndexChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles FileListBox1.SelectedIndexChanged
 
If FileListBox1.Path.EndsWith("\") Then
lblWatching.Text = FileListBox1.Path & _
FileListBox1.FileName
Else
lblWatching.Text = FileListBox1.Path & _
"\" & FileListBox1.FileName
End If
 
End Sub
 
Protected Sub startWatching()

 
‘-- Initialize the Tool Tips
‘-- Create a new instance of the File Sentinel
m_fsSentinel = _
New FileSentinel.SystemObserver.sentinel( _
m_sFilesToScan)

 
‘-- Update the UI --
btnEnable.Enabled = False
btnDisable.Enabled = True
DriveListBox1.Enabled = False
DirListBox1.Enabled = False
FileListBox1.Enabled = False
End Sub
 
Private Sub btnEnable_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnEnable.Click
 
startWatching()
End Sub
 
Private Sub lblWatching_TextChanged( _
ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles lblWatching.TextChanged
 
m_sFilesToScan = lblWatching.Text
End Sub
 

‘ Other Windows Form Designer generated code omitte d.
 
End Class

How the Interface Code Works

The first task we need to take care of is importing the SystemObserver class. Once we've imported our new class, we can reference the object in our user interface.

Imports FileSentinel.SystemObserver.sentinel

At the top of our Form1 class, we add two private variables. The first, m_sFilesToScan, is used to hold the file or directory selected for monitoring. The second variable, m_fsSentinel, will of course hold a reference to an instance of our sentinel class.

Public Class Form1
Inherits System.Windows.Forms.Form
 
Private m_sFilesToScan As String
Private m_fsSentinel As _
File_Sentinel.SystemObserver.sentinel

In the form constructor, we want to initialize a value to display in the label named lblWatching. If the user clicks the Enable Sentinel button immediately after the program starts, at least some default value is included and our program won't crash. Next we call our built-in routine InitializeGUI, which sets up the user interface. Notice that we set the label after the built-in InitializeComponent routine is called. This order ensures that all the controls are sited and the form is completely built and displayed. We can then write to the form, or to any controls contained in it, and not get an error. Be sure that any code that manipulates a visible part of a form is executed after the InitializeComponent routine.

Public Sub New()
MyBase.New()
 
‘ This call is required by the Windows Form Designe r.
InitializeComponent()
 
‘ Add any initialization after the
‘ InitializeComponent() call
lblWatching.Text = DriveListBox1.Drive.ToUpper & "\ "
 
‘-- Call our private routine that initializes the GUI
InitializeGUI()
 
End Sub

The ToolTip Control

When the form is loaded, built, and displayed, our routine InitializeGUI is called. In this routine, we activate the Enable Sentinel button and construct our tooltips. We add a tooltip to both the Enable Sentinel and Disable Sentinel buttons, as well as to the DriveListBox control. The tooltip is very easy to set by using the following format:

ToolTipControl.SetToolTip(controlToAssociate, _
"Message to display")

You can get fancy with a tooltip by setting some of the control's properties. For example, you can set multiple delay values for the Windows Forms ToolTip control. The unit of measure for these properties is milliseconds. The InitialDelay property determines how long the user must point at the associated control before the tooltip string appears. The ReshowDelay property sets the number of milliseconds that pass for subsequent tooltip strings to appear as the mouse moves from one tooltip-associated control to another. The AutoPopDelay property determines the length of time the tooltip string is shown. You can set these values individually or by setting the value of the AutomaticDelay property, which will then set the other delay values in a fixed ratio to the value set for AutomaticDelay. (When AutomaticDelay is set to a value of N, InitialDelay is set to N, ReshowDelay is set to N/5, and AutoPopDelay is set to 5N.) We are going to use the default values because they are fine for our program.

Private Sub InitializeGUI()
 
‘-- Disable the options until a legit Dir/File
‘ selection is made
btnEnable.Enabled = True
btnDisable.Enabled = False
 
‘-- Initialize the ToolTips
ttTip.SetToolTip(btnEnable, _
"Enable the File Sentinel to monitor a folder " & _
"or directory. ")
ttTip.SetToolTip(btnDisable, _
"Stop monitoring a folder or directory.")
ttTip.SetToolTip(DriveListBox1, _
"Select the drive to monitor a folder " & _
"or directory.")
End Sub

When the user enables the File Sentinel, the Enable Sentinel button is disabled and the Disable Sentinel button is enabled. We also enable the drive, directory, and file list boxes to permit the user to make a selection.

Private Sub btnDisable_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnDisable.Click
 
m_fsSentinel.dispose()
btnEnable.Enabled = True
btnDisable.Enabled = False
DriveListBox1.Enabled = True
DirListBox1.Enabled = True
FileListBox1.Enabled = True
End Sub

We want the user to be able to select any drive that the computer can see, whether local or remote. The Visual Basic 6 DriveListBox control was usually used to select or change drives in a File Open or a Save dialog box. Unfortunately, Visual Basic .NET has no equivalent for the DriveListBox control. If you upgrade older Visual Basic programs to Visual Basic .NET, any existing DriveListBox controls are upgraded to the VB6.DriveListBox control that is provided as a part of the compatibility library (Microsoft.VisualBasic.Compatibility). We'll use this control for our program because it was converted to .NET and is considered to be managed code. Of course, when we added the control it was listed within the .NET Framework Components tab of the Customize Toolbox dialog box.

When the user selects a drive, we want to set the directory list box to the new drive. However, if the user selects, say, drive A and no disk is inserted, we will get an error. By placing the following line within a protected Try block, we can handle any error:

DirListBox1.Path = DriveListBox1.Drive

If the drive is legitimate, we set the directory list box to the new drive and display the current drive in the label. If, however, the change in drives throws an error, the Catch block is executed and we reset the drive to the previously good drive from the directory list box. This simple technique will prevent a run-time error.

Private Sub DriveListBox1_SelectedIndexChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles DriveListBox1.SelectedIndexChanged
 
Try
DirListBox1.Path = DriveListBox1.Drive
lblWatching.Text = DriveListBox1.Drive
Catch
DriveListBox1.Drive = DirListBox1.Path
End Try
End Sub

If the drive selected is available, the directory list box is set to the path of the new drive letter. If this change does not cause an error, we display the new directory in the label lblWatching. As with the DriveListBox control, Visual Basic .NET does not include a .NET version of the DirListBox control.

Private Sub DirListBox1_SelectedIndexChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles DirListBox1.SelectedIndexChanged
 
Try
FileListBox1.Path = DirListBox1.Path
lblWatching.Text = DirListBox1.Path
Catch
End Try
End Sub

If the directory list box does not throw an error, the file list box is updated. As I mentioned, we want to terminate the string with a back slash if the character is not there.

Private Sub FileListBox1_SelectedIndexChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles FileListBox1.SelectedIndexChanged
 
If FileListBox1.Path.EndsWith("\") Then
lblWatching.Text = FileListBox1.Path & _
FileListBox1.FileName
Else
lblWatching.Text = FileListBox1.Path & "\" & _
FileListBox1.FileName
End If
 
End Sub

Whenever a legitimate drive, directory, or folder is selected, the label lblWatching is updated. This update fires the TextChanged event of the label. We set the class-level private variable m_sFilesToScan to the contents of the label's Text property.

Private Sub lblWatching_TextChanged( _
ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles lblWatching.TextChanged
 
m_sFilesToScan = lblWatching.Text
End Sub

When a file or directory is successfully selected, the user clicks the Enable Sentinel button we created with the name btnEnable. This event simply calls the procedure startWatching.

Private Sub btnEnable_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnEnable.Click
 
startWatching()
End Sub

When the sentinel is enabled, we create a new instance of our sentinel class. Then, to ensure that the user does not start clicking other buttons or drives when the sentinel is enabled, the Enable Sentinel button and the drive, directory, and file list boxes are disabled, which avoids confusion on the part of the user. We gently guide them through what they can and cannot select in the context of the running program.

Protected Sub startWatching()
 
‘-- Create a new instance of the File Sentinel
m_fsSentinel = _
New File_Sentinel.SystemObserver.sentinel( _
m_sFilesToScan)
 
‘-- Update the UI --
btnEnable.Enabled = False
btnDisable.Enabled = True
DriveListBox1.Enabled = False
DirListBox1.Enabled = False
FileListBox1.Enabled = False
End Sub

Because we encapsulated all of the code in our class—following good design practice—within our user interface we can see only the dispose and GetType methods. Everything else is hidden, as you can see in Figure 9-7.

Figure 9-7   All we see in the interface are the dispose and GetType methods. (Image unavailable)

Possible Enhancements to the File Sentinel

If you were using the File Sentinel program in a work environment, you might want to give the user more granularity in what to monitor. To do this, you could add three WriteOnly properties: WatchAttributes, WatchFileSize, and WatchLast-Access. Our program watches these properties by default, but you can give the user the option if you want to by adding these to the sentinel class. While we added all of them, you could add only a few default filters, such as LastWrite, Security, CreationTime, and DirectoryName.

m_Watcher.NotifyFilter = NotifyFilters.FileName Or _
NotifyFilters.LastWrite Or _
NotifyFilters.Security Or _
NotifyFilters.CreationTime Or _
NotifyFilters.DirectoryName

Then you could add three custom WriteOnly properties to your class. If the user wanted to monitor additional events, such as monitoring the attributes, file size, or last access of a file, these properties would be set to True within your class. Because the NotifyFilters property is enumerated, simply add whichever attribute you want by adding the enumerated data type to the NotifyFilters property of the class.

You can combine the members of the NotifyFilter enumeration to watch for more than one kind of change because it allows a bitwise combination of its member values. For example, you can watch for changes in the size of a file or folder and for changes in security settings. This raises an event any time a change in size or security settings of a file or folder occurs. Table 9-3 lists the members of NotifyFilters.

Table 9-3 NotifyFilters Members

Member NameDescription
AttributesThe attributes of the file or folder
CreationTimeThe time the file or folder was created
DirectoryNameThe name of the directory
FileNameThe name of the file
LastAccessThe date the file or folder was last opened
LastWriteThe date the file or folder last had anything written to it
SecurityThe security settings of the file or folder
SizeThe size of the file or folder

Here are three custom WriteOnly properties that you might add to your class to watch for changes in file attributes, file size, and file access:

WriteOnly Property WatchAttributes() As Boolean
Set(ByVal Value As Boolean)
If (value = True) Then
m_Watcher.NotifyFilter += _
IO.N otifyFilters.Attributes
End If
End Set
End Property

WriteOnly Property WatchFileSize() As Boolean
Set
If (value = True) Then
m_Watcher.NotifyFilter += _
IO.NotifyFilters.Size
End Set
End Property

WriteOnly Property WatchLastAccess() As Boolean
Set
If (value = True) Then
m_Watcher.NotifyFilter += _
IO.NotifyFilters.LastAccess
End If
End Set
End Property

Then, within your user interface, you could add three check boxes. If the user checked one or more of the optional items to watch, you simply set that particular class property to True.

If chkAttributes.Checked = True Then _
m_fsSentinel.WatchAttributes = True
If chkSize.Checked = True Then _
m_fsSentinel.WatchFileSize = True
If chkAccess.Checked = True Then _
m_fsSentinel.WatchLastAccess = True

That's it for our File Sentinel class, or is it? You might be wondering why we didn't add extensive user interface capabilities (such as displaying notification messages in a dialog box) to the class. Well, the reason is that we are going to convert our class into a Windows service, and services don't have a user interface.


NOTE:
The DriveListBox, DirListBox, and FileListBox legacy controls behave somewhat erratically in the .NET platform—when the File Sentinel program is running, you might need to click the controls several times before they display the proper elements.

Introduction to Windows Services

Microsoft Windows services, formerly known as NT services, enable you to create long-running executable applications that run in their own Windows sessions. Service applications can be set up to start when the computer boots, and they can be paused and restarted.

These services do not have any user interface, which makes converting our class to a service for use on a server (or wherever you need long-running functionality that does not interfere with other users working on the same computer) quite easy. You can also run services in the security context of a specific user account that is different from the logged-on user or the default computer account.

You create a service by creating an application that is installed as a service. If we want to use the sentinel class as a service, we can convert it to run on a server in the background. And because we used object-oriented programming, we can simply use the class as-is, giving real meaning to reusability.

The Life and Death of a Service

A service goes through several internal states in its lifetime. First the service is installed onto the system on which it will run, such as your Web server. This process executes the installers for the service project and loads the service into the Windows Service Control Manager (SCM) for that computer. The SCM is the central utility provided by Windows to administer services. It can be configured to run automatically when booted or can be started and stopped at will. A running service can exist in this state indefinitely until it is either stopped or paused or until the computer shuts down. A service can exist in one of three basic states: running, paused, or stopped.

Unlike some types of projects, for a service you must create installation components for the service application. The installation components install and register the service on the server and create an entry for your service with the SCM. Luckily, .NET makes this task very easy, and I'll cover the steps in detail.

Windows service applications run in a different Windows station—a secure object that contains a clipboard, a set of global atoms, and a group of desktop objects—than other applications. Because of this, dialog boxes raised from within a Windows service application will not be seen and will probably even cause your program to stop responding. Therefore, you want to be sure to write all messages, including error messages, to a file rather than raise any messages in the user interface by means of something such as a message box.

Building Our File Sentinel into a Windows Service

Start a new Visual Basic project, and this time select the Windows Service template, shown in Figure 9-8. Name the project vbFileMonitorService. Of course, the IDE will create a new directory with that name under the directory listed in the Location text box.

Figure 9-8   Use the Windows Service project icon to create a Windows service application. (Image unavailable)

When the project is created, you'll see a blank designer screen. Click the hyperlink that reads "click here to switch to code view," shown in Figure 9-9. The IDE adds the service template for us, but we'll need to add a few lines of code in order to include our sentinel class and also to write to an event log file.

Figure 9-9   Use the hyperlink to switch to code view. (Image unavailable)

Adding Our Sentinel Class to Our Service

We're going to add the sentinel class we built earlier in this chapter to this project to illustrate object-oriented class reusability. We will make this addition in the easiest way—using Windows Explorer to copy the file Sentinel.vb in the directory of your last project to the vbFileMonitorService directory. Sentinel.vb is the text file that contains the code with the namespace SystemObserver and the public class sentinel.

After the file is in the vbFileMonitorService directory, we want to add the file to our current project.

  1. From the Visual Studio .Net IDE, Select Project | Add Existing Item.
  2. When the Add Existing Item dialog box comes up, select Sentinel.vb to include our class file in the vbFileMonitorService project. Now the file is physically in our current project directory and is added to our project solution file.
  3. Click the Sentinel.vb tab to display the code for our sentinel class. Comment out the EnableRaisingEvent property line. We are going to add two properties to turn this event on and off. Also change the name of the log file to vbFMS.txt. With separate names, we can keep both log files on disk and use them for different purposes.
  4.            AddHandler m_Watcher.Changed, AddressOf OnCh anged
    AddHandler m_Watcher.Created, AddressOf OnCh anged
    AddHandler m_Watcher.Deleted, AddressOf OnCh anged
    AddHandler m_Watcher.Renamed, AddressOf OnRe named
    AddHandler m_Watcher.Error, AddressOf onErro r
     
    ‘ m_Watcher.EnableRaisingEvents = True
     
    m_ObserveFileWrite = _
    New StreamWriter("C:\vbFMS.txt", True)

Remember that the OnChanged event handler checks to see whether the log file was being modified. If it was, we ignored the event. Because we changed the name of our log file to vbFMS.txt, add this name in lowercase characters to the IndexOf property. This setting will prevent our log from recording each and every write to the file.

If (Len(sChange) > 0) Then
If (e.FullPath.IndexOf("vbfms.txt") > 0) Then
Exit Sub
End If
End If

Finally, add the following two public properties to the class. With these properties, when our service starts and stops, we can instruct the sentinel class to start and stop logging changes to files.

Public Sub StartLogging()
m_Watcher.EnableRaisingEvents = True
End Sub
 
Public Sub StopLogging()
m_Watcher.EnableRaisingEvents = False
End Sub

With these minor changes, we're able to import our sentinel class and quickly change it from a Windows application to a Windows service.

Updating the Service1.vb File

As with other project templates, the Visual Studio .NET project template Windows Service does much of the work of building a service for you. It references the appropriate classes and namespaces, sets up the inheritance from the base class for services, and overrides several of the methods you're likely to want to override.

With this work done for you, at a minimum you must do the following to create a functional service:

  • Set the ServiceName property.
  • Create the necessary installers for your service application.
  • Override and specify code for the OnStart and OnStop methods to customize the ways in which your service behaves.

Click the Service1.vb tab to switch to the service template code generated by the IDE. In the interest of space, I'll simply highlight the lines of code that you have to add. Because we are referencing our File Sentinel program, we had to first add it to our project.

Imports System.ServiceProcess
Imports vbFileMonitorService.SystemObserver.sentinel

 
Public Class Service1
Inherits System.ServiceProcess.ServiceBase
 
Dim vbFMS As vbFileMonitorService.SystemObserver.sentinel

#Region " Component Designer generated code "
 
Public Sub New()
MyBase.New()
 
‘ This call is required by the Component Design er.
InitializeComponent()
 
‘ Add any initialization after the
‘ InitializeComponent() call
EventLog.EnableRaisingEvents = True
Me.AutoLog = True
Me.CanStop = True
 
vbFMS = New _
vbFileMonitorService.SystemObserver.sentinel("C:\")


End Sub
 
‘ The main entry point for the process
Shared Sub Main()
Dim ServicesToRun() As _
System.ServiceProcess.ServiceBase
 
‘ More than one NT Service may run within the s ame
‘ process. To add another service to this proce ss,
‘ change the following line to create a second
‘ service object. For example,

‘ ServicesToRun = New _
‘ System.ServiceProcess.ServiceBase ()
‘ {New Service1, New MySecondUserService}

ServicesToRun = New _
System.ServiceProcess.ServiceBase () _
{New Service1}
 
System.ServiceProcess.ServiceBase.Run(ServicesT oRun)
End Sub
 
‘ Required by the Component Designer
Private components As System.ComponentModel.Contain er
 
‘ NOTE: The following procedure is required by the
‘ Component Designer.
‘ It can be modified using the Component Designer.
‘ Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
components = New System.ComponentModel.Containe r()
Me.ServiceName = "vbFileMonitorService"
End Sub
 
#End Region
 
Protected Overrides Sub OnStart(ByVal args() As Str ing)
‘ Add code here to start your service. This met hod
‘ should set things in motion so your service c an
‘ do its work.
vbFMS.StartLogging()
EventLog.WriteEntry("Started logging files.")

End Sub
 
Protected Overrides Sub OnStop()
‘ Add code here to perform any tear-down
‘ necessary to stop your service.
vbFMS.StopLogging()
EventLog.WriteEntry("Stopped logging files.")

End Sub
 
End Class

How Our Service Works

The Main method for our service application must use the Run command for the services your project contains. The Run method loads the services into the SCM on the appropriate server. Because we used the Windows Services project template, the Run method is written for us. Keep in mind that loading a service is not the same operation as starting a service.

We first have to import our class that contains the File Sentinel. Because we are including the class in our vbFileMonitorService project, we add the following Imports statement:

Imports vbFileMonitorService.SystemObserver.sentinel

We now add a class-level variable that contains a reference to an instance of the sentinel class.

Dim vbFMS As vbFileMonitorService.SystemObserver.sentinel

Logging Events to the Event Viewer

Event logging in Microsoft Windows provides a standard, centralized way to have applications record important software and hardware events. For example, when an error occurs, the system administrator must determine what caused the error. It is helpful if applications, the operating system, and other system services record important events such as low-memory conditions or failed attempts to access a disk. The system administrator can then use the event log to help determine what conditions caused the error and the context in which it occurred.

Windows supplies a standard user interface for viewing these event logs, the Event Viewer, shown in Figure 9-10, and a programming interface for examining log entries. In Visual Basic 6, you could perform limited write operations to some event logs, but you could not easily read or interact with all the logs available to you.

Figure 9-10   The Windows Event Viewer. (Image unavailable)

In .NET, however, we simply use the EventLog component, which allows us to connect to event logs on both local and remote computers and then write entries. You can also read entries from existing logs and create your own custom event logs. In our class, we will simply write to the standard application log. As our service executes, we can have it write events to the application log for any event we deem important. By default, the event type is set to Information if you do not specify otherwise. However, you can set the type of event by using a parameter on an overloaded form of the WriteEntry method of the EventLog object.

By double-clicking one of the events in the Event Viewer, you can see the event's Event Properties dialog box. In Figure 9-11, you can see one of our custom messages logged to the Application log.

Figure 9-11   The Event Properties dialog box. (Image unavailable)

The EnableRaisingEvents property determines whether the EventLog object raises events when entries are written to the log. When the property is True, components receiving the EventWritten event will receive notification any time an entry is written to the log. If EnableRaisingEvents is False, no events are raised. In our example, we set this property to True but don't check for the event. I did it this way in the example simply to illustrate the concept and syntax.

EventLog.EnableRaisingEvents = True

You must be prudent when writing to the log file because it can easily become so filled with messages that the messages become meaningless to whoever is viewing them. In full production services, we want to write entries for resource problems. For example, if your application is in a low-memory situation (caused by a code bug or inadequate memory) that degrades performance, logging a warning event when memory allocation fails might provide a clue about what went wrong. We can also log information events. A server-based application (such as a database server) might want to record a user logging on, opening a database, or starting a file transfer. The server can also log error events it encounters (failed file access, host process disconnected, and so on), corruptions in the database, or whether a file transfer was successful.

By default, all Windows Service projects can interact with the Application event log by writing information and exceptions to it. Use the AutoLog property to indicate whether you want this built-in functionality in your application. By default, logging is turned on for any service you create with the Windows Service project template. In our project, we use a static form of the EventLog class to write service information to a log. We don't have to create an instance of an EventLog component or manually register a source.

If you want to write to an event log other than the Application log, you must set the AutoLog property to False, create your own custom event log within your services code, and register your service as a valid source of entries for that log.

Me.AutoLog = True

At times, we might want to stop logging file changes. In case of maintenance or normal file access, we can simply turn off our service when needed. When Stop is called on a service, the SCM verifies whether the service accepts Stop commands using the value of CanStop. For most services, the value of CanStop is True, but some operating system services do not allow the user to stop them.

If CanStop is True, the Stop command is passed to the service and the OnStop method is called, as it is in our service. However, if we don't define an OnStop method, the SCM handles the Stop command through the empty base class method ServiceBase.OnStop.

Me.CanStop = True

The next line should be familiar. With it we instantiate a new instance of our sentinel class. The root directory of drive C is passed into the constructor as a parameter. Because Windows services do not have a user interface, we have to pass in a value manually and not from a UI. You might want to code this parameter as the root directory that holds your Web pages or some other location of importance. But for now, we will monitor all files in the root directory.

vbFMS = New _
vbFileMonitorService.SystemObserver.sentinel("C:\")

In the next line of code from the InitializeComponent procedure, we change the service name to something that is meaningful to our program.

Me.ServiceName = "vbFileMonitorService"

We then add two one-line methods to our file sentinel. The methods, StartLogging and StopLogging, will turn on and off the EnableRaisingEvents property of the FileSystemMonitor object. When the service is started, we set the property to True, and when it's stopped, we toggle the value to False.

After each call to our class for turning on and off the events, we write to the event log to track when our service starts and ends. We use the WriteEntry method of the static EventLog. It will automatically time stamp the entry for us.

Protected Overrides Sub OnStart(ByVal args() As String)
‘ Add code here to start your service. This method
‘ should set things in motion so your service can
‘ do its work.
vbFMS.StartLogging()
EventLog.WriteEntry("Started logging files.")
End Sub
 
Protected Overrides Sub OnStop()
‘ Add code here to perform any tear-down
‘ necessary to stop your service
vbFMS.StopLogging()
EventLog.WriteEntry("Stopped logging files.")
End Sub

The Main method for the vbFileSystemMonitor service application issues the Run command for the services your project contains. Because we used the Windows Services project template, this method is provided for us. Once the service is loaded, we will manually start and stop the service from the SCM.

Shared Sub Main()
Dim ServicesToRun() As System.ServiceProcess.Servic eBase
 
ServicesToRun = New _
System.ServiceProcess.ServiceBase () _
{New Service1}
System.ServiceProcess.ServiceBase.Run(ServicesToRun )
End Sub

Adding an Installer to Our Windows Service

We have to add an installer to our Windows service, which we don't have to do with a standard Windows program. The installer is responsible for doing the heavy lifting required to register our new service with the SCM. The ServiceInstaller class does work specific to the service with which it is associated. It is used by the installation utility we will add to the service to write registry values associated with the service to a subkey within the HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services registry key.

The service is identified by its ServiceName within this subkey. The subkey also includes the name of the executable or DLL to which the service belongs. If you are interested, use Regedit.exe to examine the registry entry of the service after we install it. You can see in Figure 9-12 that the registry contains the fully qualified name of our service in the ImagePath key.

Figure 9-12   Viewing our registered service in the Registry Editor. (Image unavailable)

Adding the ServiceInstaller

Adding a ServiceInstaller to our Windows service is pretty straightforward. Go to the Service1.vb [Design] tab, and then right-click. On the pop-up menu, select the Add Installer option, shown in Figure 9-13.

Figure 9-13   Adding a ServiceInstaller. (Image unavailable)

As you can see in Figure 9-14, the IDE adds both a ServiceProcessInstaller and a ServiceInstaller. Most of the code for these objects will be added for us. When we run the installer program, InstallUtil.exe, it will read this code and install the vbService as a Windows service for us.

Figure 9-14   The IDE adds a ServiceProcessInstaller and a ServiceInstaller. (Image unavailable)

Double-click both ServiceProcessInstaller1 and the ServiceInstaller1 on the ProjectInstaller.vb [Design] tab in the IDE. The template code does the bulk of the work, but we need to add a few lines of code to make the installer functional for our program. Again, in the interest of space, only the lines you need to add are highlighted.

Imports System.ComponentModel
Imports System.Configuration.Install
 
<RunInstaller(True)> Public Class ProjectInstaller
Inherits System.Configuration.Install.Installer
 
#Region " Component Designer generated code "
 
Public Sub New()
MyBase.New()
 
‘ This call is required by the Component Design er.
InitializeComponent()
 
‘ Add any initialization after the
‘ InitializeComponent() call
 
End Sub
Friend WithEvents ServiceProcessInstaller1 As _
System.ServiceProcess.ServiceProcessInstaller
Friend WithEvents ServiceInstaller1 As _
System.ServiceProcess.ServiceInstaller
 
‘ Required by the Component Designer
Private components As System.ComponentModel.Contain er
 
‘ NOTE: The following procedure is required by the
‘ Component Designer
‘ It can be modified using the Component Designer.
‘ Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
Me.ServiceProcessInstaller1 = New _
System.ServiceProcess.ServiceProcessInstall er()
Me.ServiceInstaller1 = New _
System.ServiceProcess.ServiceInstaller()

‘ ServiceProcessInstaller1

Me.ServiceProcessInstaller1.Account = _
System.ServiceProcess.ServiceAccount.LocalSystem

Me.ServiceProcessInstaller1.Password = Nothing
Me.ServiceProcessInstaller1.Username = Nothing

‘ ServiceInstaller1

Me.ServiceInstaller1.ServiceName = _
"vbFileMonitorService"

‘ ProjectInstaller

Me.Installers.AddRange(New _
System.Configuration.Install.Installer() _
{Me.ServiceProcessInstaller1, Me.ServiceIn staller1})
 
End Sub
 
#End Region
 
Private Sub ServiceInstaller1_AfterInstall( _
ByVal sender As System.Object, _
ByVal e As _
System.Configuration.Install.InstallEventAr gs) _
Handles ServiceInstaller1.AfterInstall
 
End Sub
 
Private Sub ServiceProcessInstaller1_AfterInstall( _
ByVal sender As System.Object, _
ByVal e As _
System.Configuration.Install.InstallEventAr gs) _
Handles ServiceProcessInstaller1.AfterInstall
 
End Sub
End Class

How the Installation Code Works

In this code, we are telling the ServiceProcessInstaller which local account process space under which to run. By default, the service will start manually. Another option is to have the service start automatically when the machine boots. When you get your service up and running, you might want to change this setting. For now, a manual start is what we want.

Me.ServiceProcessInstaller1.Account = _
System.ServiceProcess.ServiceAccount.LocalSystem

As before, we give our service the name that we want to show up in the Services window.

Me.ServiceInstaller1.ServiceName = _
"vbFileMonitorService"

When logging is turned on, the installer for our service registers the service as a valid source of events with the Application log on the computer where the service is installed. The service logs information each time the service is started, stopped, paused, resumed, installed, or uninstalled. It also logs any failures that occur. You do not need to add any code to write entries to the log when using the default behavior. The service handles these details for you. Pretty cool, eh?

Installing Our Service

Now we're ready to install our service. Build the project by selecting Build | Build from the IDE. This creates the file vbFileMonitorService in the \bin directory.

After you create and build the application, you install the service by running the command-line utility InstallUtil.exe and passing the path to the service's executable file. The easiest way to do this is to use Windows Explorer to find InstallUtil.exe on your drive. Copy the file to the directory where your service executable is located. On my drive, the file is located under C:\Chapter 9\vbFileMonitorService\bin. If you install the companion disc files to the default location, your path will be C:\Coding Techniques for Visual Basic .NET\Chap09\vbFileMonitorService\bin.

Bring up an MS-DOS command prompt window, run installutil, and pass it the full name of the service. You will see several messages printed to the command window. Here's what the installation process will look like:

C:\Chapter 9\vbFileMonitorService\bin>installutil
vbfilemonitorservice.exe
Microsoft (R) .NET Framework Installation utility
Copyright (C) Microsoft Corp 2001. All rights reserved.
 
Running a transacted installation.
 
Beginning the Install phase of the installation.
See the contents of the log file for the C:\Chapter
8\vbFileMonitorService\bin\vbfilemonitorservice.exe
assembly's progress.
The file is located at C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.Instal lLog.
Call Installing. on the C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.exe as sembly.
Affected parameters are:
assemblypath = C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.exe
 
logfile = C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.Instal lLog
Installing service vbFileMonitorService...
Service vbFileMonitorService has been successfully inst alled.
Creating EventLog source vbFileMonitorService in log
Application...
 
The Install phase completed successfully, and the Commi t
phase is beginning.
See the contents of the log file for the C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.exe
assembly's progress.
The file is located at C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.Instal lLog.
Call Committing. on the C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.exe as sembly.
Affected parameters are:
assemblypath = C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.exe
 
logfile = C:\Chapter
9\vbFileMonitorService\bin\vbfilemonitorservice.Instal lLog
 
The Commit phase completed successfully.
 
The transacted install has completed.


NOTE:
To uninstall the service, you must first close the Service Management Console. If the console is open, it must be closed and reopened to allow the uninstall to complete. Use the syntax installutil /u vbFileMonitorService.exe to perform this operation.

Looking at vbMonitorService in the Services Window

Now that the service has been installed, we can start to use it.

  1. Bring up the Services window by clicking Start | Programs | Administrative Tools | Services on the Windows task bar. (In Windows 2000 Professional, open the Control Panel, double-click Administrative Tools, and then double-click Services.) In the Services dialog box, shown in Figure 9-15, you'll see our new service registered with the other system services.
  2. Figure 9-15   Our service among the others in the Services window. (Image unavailable)

  3. Double-click vbMonitorService to display the properties dialog box, shown in Figure 9-16. Add the name File Sentinel to the Description text box. This name will show up in the Description column of the Services window. Note that if you are running Windows XP, the description text cannot be changed.
  4. Figure 9-16   The properties dialog box for vbFileMonitorService. (Image unavailable)

  5. Click Apply to add the description to the service.
  6. Now we're ready to start running our new service and put it to work. Click the Start Service button. A Service Control dialog box with a progress bar will be displayed for a few seconds while our service is started, as shown in Figure 9-17.

Figure 9-17   A progress bar is displayed while our service starts running. (Image unavailable)

By examining the Services window, shown in Figure 9-18, you can see that the description of our service has been added and that it is also now running. As the service runs, it will monitor any changes to files in the root directory of the C drive.

Figure 9-18   Our service is running. (Image unavailable)

We can examine the vbFMS.txt file and see whether any suspicious file modifications occurred. A quick look at the text file might reveal some interesting changes.

Monday, July 30, 2001 9:33:13 PM File: C:\passwords.exe
renamed to C:\trash.exe
Monday, July 30, 2001 9:33:13 PM File: C:\trash.exe Ch anged
Monday, July 30, 2001 9:33:18 PM File: C:\trash.exe Ch anged
Monday, July 30, 2001 9:33:39 PM File: C:\trash.exe De leted
Monday, July 30, 2001 9:34:08 PM File: C:\salaries.xls Changed
Monday, July 30, 2001 9:34:08 PM File: C:\salaries.xls Changed
Monday, July 30, 2001 9:34:18 PM File: C:\passwords.txt Changed

When we want to stop our service, we simply double-click vbFileMonitorService in the Services window and then click Stop. A Service Control dialog box is displayed again (see Figure 9-19), and the service is stopped for us. Now you can see why we wanted to add those two methods, StartLogging and StopLogging, to the sentinel class. These methods make turning our service on and off pretty trivial.

Figure 9-19   Progress is made while our service is stopped. (Image unavailable)

Debugging a Windows Service

As you've just seen, after we finish writing our service, the compiled executable file must be installed on the computer where it will run before the project can function in a meaningful way. In addition, you cannot debug or run a service application by pressing F5 or F11 in the IDE. Because of the nature of services, you cannot immediately run a service or step into its code.

Because a service must be run within the context of the SCM rather than within Visual Studio .NET, debugging a service is not as straightforward as debugging other Visual Studio application types. To debug a service, you must start the service and then attach a debugger to the process in which it is running. You can then debug your application by using all the standard debugging functionality of the Visual Studio IDE.


NOTE:
You should not attach to a process unless you know what the process is and understand the consequences of attaching to and possibly killing that process. For example, if you attach to the WinLogon process and then stop debugging, the system will halt because it cannot operate without WinLogon.

You can only attach the debugger to a running service. As you might expect, the attachment process interrupts the functioning of your service instead of stopping or pausing the service's processing—that is, if your service is running when you begin debugging it, the service is still technically in the started state as you debug it, but its processing has been suspended.

Attaching a debugger to the service's process allows you to debug most but not all of the service's code. For example, because the service has already been started, you cannot debug the code in the service's OnStart method, nor can the code in the Main method that is used to load the service be debugged. Both procedures have already been executed to get the service loaded.

One way to work around this limitation is to create a temporary second service within your service application that exists only to aid in debugging. You can install both services and then start the dummy service to load the service process. After the temporary service has started the process, you can use the Debug menu in Visual Studio .NET to attach to the service process.

After attaching to the process, you can set breakpoints and use these to debug your code. Once you exit the dialog box you use to attach to the process, you are effectively in debug mode. You can use the SCM to start, stop, pause, and continue your service, thus hitting the breakpoints you've set. Remove the dummy service later after debugging is successful.

When you are ready to start debugging your service, use the SCM to start the service. Once the service is running, you can start debugging.

  1. Select Debug | Process from the IDE to display the Processes dialog box. Be sure to check the Show System Processes check box, which is cleared by default. Because we wrote a system process, if this option were not checked we would not see the process. Of course, our service does not have a user interface, so there is no title for our application to display in the Title column, as you can see in Figure 9–20.
  2. Figure 9-20   Attaching to a process to debug our service. (Image unavailable)

  3. Double-click the vbFileMonitorService process to display the Attach To Process dialog box, shown in Figure 9-21. Check the Common Language Runtime option, and then click OK to attach the debugger to this process.
  4. Figure 9-21   The Attach To Process dialog box. (Image unavailable)

  5. We've now successfully attached the debugger to our vbFileMonitorService process, as you can see in Figure 9-22. Click Close to dismiss the Processes dialog box.
  6. Figure 9-22   Success—we've attached the debugger to our service's process. (Image unavailable)

You are now in debug mode. Go ahead and set any breakpoints you want to use in your code. Run the SCM and work with your service, sending stop, pause, and continue commands to hit your breakpoints.

Conclusion

Well, this was a pretty important chapter. We learned more about the .NET Framework and how to build classes in Visual Basic .NET. In the process, we learned about delegates and also used our knowledge of stream writers to log events. In addition, we converted the class to an interfaceless Windows service.

These first nine chapters have provided you with the techniques you use to work with Visual Basic .NET and how to use any framework class in any namespace. Armed with this knowledge, we'll now move on to learn about ADO.NET, putting our knowledge to use by understanding and using the .NET database access methodologies.

Customer Reviews

Average Review:

Write a Review

and post it to your social network

     

Most Helpful Customer Reviews

See all customer reviews >