The File Sentinel Program

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.

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.

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.

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.

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 Program

Control

Property

Value

Button

Name

btnEnable

Text

&Enable Sentinel

Button

Name

btnDisable

Text

&Disable Sentinel

Label

Name

lblWatching

BackColor

Lime

BorderStyle

Fixed3D

Form

FormBorderStyle

FixedDialog

Icon

<Your choice>

Text

File Sentinel

StartPosition

CenterScreen

Locked

True

ToolTip

Name

ttTip

DriveListBox

Name

DriveListBox1

DirListBox

Name

DirListBox1

FileListBox

Name

FileListBox1

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.

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 run time (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.

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:

    Imports System Imports System.Diagnostics Imports System.IO Imports System.Threading Namespace SystemObserver Public Class sentinel Private m_Watcher As System.IO.FileSystemWatcher 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.ToString .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 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 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 Name

Description

Attributes

The attributes of the file or folder

CreationTime

The time the file or folder was created

DirectoryName

The name of the directory

FileName

The name of the file

LastAccess

The date the file or folder was last opened

LastWrite

The date the file or folder last had anything written to it

Security

The security settings of the file or folder

Size

The 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.LastAccess Or _ notifyFilters.LastWrite Or NotifyFilters.Security Or _ 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 FileSystem EventArgs 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.Message()) 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 Designer. 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 omitted. 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 Designer. 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 Drive ListBox 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.

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 Name

Description

Attributes

The attributes of the file or folder

CreationTime

The time the file or folder was created

DirectoryName

The name of the directory

FileName

The name of the file

LastAccess

The date the file or folder was last opened

LastWrite

The date the file or folder last had anything written to it

Security

The security settings of the file or folder

Size

The 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.NotifyFilters.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.



Coding Techniques for Microsoft Visual Basic. NET
Coding Techniques for Microsoft Visual Basic .NET
ISBN: 0735612544
EAN: 2147483647
Year: 2002
Pages: 123
Authors: John Connell

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net