View Part 2 of this article series.
Introduction
One of my first official tasks as a DotNet programmer was to write a Windows Service which monitored a folder on a server for new files, processed those files, and then transferred them to another server (via FTP). I became immediately aware of the shortcomings of the FileSystemWatcher
class, and those shortcomings have always kind stuck to my brain. A day or so ago, someone posted a question regarding a similar task, and I immediately suggested the same route I had taken. It was then, that the thought occurred to me that these FileSystemWatcher
problems have never really be addressed (that I could find).
The Problem
My primary issue is that the FileSystemWatcher
allows you to be notified that a file in a folder has changed, but not precisely what change was made. I found this particularly irritating since they provided eight notification filters to make the FileSystemWatcher
send the Changed
event. You could use one or more of these filters, but upon receiving the Changed
event, there is no way to see what
filter actually triggered the event. The ChangeType
property in the FileSystemEventArgs
parameter for the event merely indicated whether the item in question was changed, deleted, created, or renamed, and there is no property indicating the applicable filter in the case of the Changed
event.
My Solution
I came to the realization that you would need up to NINE FileSystemWatcher
objects to pull this off - one to handle all of the other ChangeTypes, and one for each of the eight NotifyFilter
enumerators. You can pretty much guess that trying to manage that many FileSystemWatcher
objects in a form would be excruciatingly painful. The technique presented in this article is in the form of a wrapper class than manages these individual FileSystemWatcher
objects and provides a handy interface and event mechanism that can be used to cherry-pick the filters you want to use. Gone (I hope) are the days of multiple events that are fired when programs like Notepad create a file. You should now be able to pick and choose which events to handle, and when.
Something Borrowed - The FileSystemWatcherEx Class
While I was researching for this article, I stumbled across a CodeProject article by George Oakes, called Advanced FileSystemWatcher. In that article, George created an extension of the FileSystemWatcher
class that allows it to monitor the folder being watched to make sure it's available. The reasons are laid out in his article, but I'll summarize his description by saying that if the watched directory somehow becomes inaccessible (maybe it's on a remote machine, maybe it was deleted), the FileSystemWatcher
object doesn't recover - at all. The article was written in VB.Net, so I had to convert it to C#, and I also made the following modification.
From Timer To Thread
The original version used a Timer
object to trigger verification of the folder's existence. I have an almost unnatural disdain for the Timer
object (Windows timer events are the lowest priority event, and are NOT guaranteed to be sent on a very busy system), and prefer to use honest-to-god threads for this kind of work. So, the first thing we have to do is create the thread.
private void CreateThread()
{
Interval = Math.Max(0, Math.Min(Interval, MaxInterval));
if (Interval > 0)
{
thread = new Thread(new ThreadStart(MonitorFolderAvailability));
thread.Name = Name;
thread.IsBackground = true;
}
}
The first line of the method normalizes the interval (how long the object waits before checking for the watched folder). The minimum acceptable value is 0 milliseconds, and the longest acceptable value is 60,000 milliseconds. A value of zero indicates that the programmer doesn't want to use this folder-checking functionality. The default value is 100 milliseconds. (This demo uses 250 milliseconds.)
Next, we need to create a thread method.
public void MonitorFolderAvailability()
{
while (Run)
{
if (IsNetworkAvailable)
{
if (!Directory.Exists(base.Path))
{
IsNetworkAvailable = false;
RaiseEventNetworkPathAvailablity();
}
}
else
{
if (Directory.Exists(base.Path))
{
IsNetworkAvailable = true;
RaiseEventNetworkPathAvailablity();
}
}
Thread.Sleep(Interval);
}
}
The method simply spins until it's told to stop, and during each cycle it checks to see if the watched folder exists. If the status changes, an event is sent that indicates the new status. The event handler code looks like this:
public class FileSystemWatcherEx : FileSystemWatcher
{
public event PathAvailabilityHandler EventPathAvailability = delegate{};
private void RaiseEventNetworkPathAvailablity()
{
EventPathAvailability(this, new PathAvailablitiyEventArgs(IsNetworkAvailable));
}
}
public class PathAvailablitiyEventArgs : EventArgs
{
public bool PathIsAvailable { get; set; }
public PathAvailablitiyEventArgs(bool available)
{
PathIsAvailable = available;
}
}
public delegate void PathAvailabilityHandler(object sender, PathAvailablitiyEventArgs e);
Other Minor Changes
I added a read-only variable that specifies the maximum allowed interval (in milliseconds), a way to name the file system watcher, and several constructor overloads to make the object more versatile.
public class FileSystemWatcherEx : FileSystemWatcher
{
public readonly int MaxInterval = 60000;
public event PathAvailabilityHandler EventPathAvailability = delegate{};
private bool IsNetworkAvailable = true;
private int Interval = 100;
private Thread thread = null;
public string Name = "FileSystemWatcherEx";
private bool Run = false;
#region Constructors
public FileSystemWatcherEx():base()
{
CreateThread();
}
public FileSystemWatcherEx(string path):base(path)
{
CreateThread();
}
public FileSystemWatcherEx(int interval):base()
{
Interval = interval;
CreateThread();
}
public FileSystemWatcherEx(string path, int interval):base(path)
{
Interval = interval;
CreateThread();
}
public FileSystemWatcherEx(int interval, string name):base()
{
Interval = interval;
Name = name;
CreateThread();
}
public FileSystemWatcherEx(string path, int interval, string name):base(path)
{
Interval = interval;
Name = name;
CreateThread();
}
}
Like I said before, the credit for the original class extension goes to George Oakes. He rocks!
Support Classes
The classes which support the WatcherEx
class are few. They're used to abstract out some of the housekeeping from the main class and keep things a bit more organized and reusable.
The WatcherInfo Class
This class is responsible for initializing the WatcherEx
class.
public class WatcherInfo
{
public string WatchPath { get; set; }
public bool IncludeSubFolders { get; set; }
public bool WatchForError { get; set; }
public bool WatchForDisposed { get; set; }
public System.IO.NotifyFilters ChangesFilters { get; set; }
public WatcherChangeTypes WatchesFilters { get; set; }
public string FileFilter { get; set; }
public uint BufferKBytes { get; set; }
public int MonitorPathInterval { get; set; }
public WatcherInfo()
{
WatchPath = "";
IncludeSubFolders = false;
WatchForError = false;
WatchForDisposed = false;
ChangesFilters = NotifyFilters.Attributes;
WatchesFilters = WatcherChangeTypes.All;
FileFilter = "";
BufferKBytes = 8;
MonitorPathInterval = 0;
}
}
- WatchPath - This is the path that is watched by all of the internal
FileSystemWatcherEx
objects.
- IncludeSubFolders - This value allows the programmer to specify
whether or not to include subfolders during the watch.
- WatchForError - If true, watches for
Error
events
- WatchForDispose - If true, watches for
Disposed
events
- ChangeFilters - This is a flags-base enumerator that allows the programmer to specify which
NotifyFilters
to monitor.
- WatchesFilters - This is a flags-based enumerator that allows the programmer to specify which basic events to handle (Changed, Created, Deleted, Renamed, All).
- FileFilter - This is the file mask of files to monitor. The default value is an empty string.
- BufferKBytes - This is the desired size of the internal buffer.
- MonitorPathInterval - This is the sleep interval between verifications that the watched folder exists. If this is set to 0, the
Availability
event will not be sent.
This class is generally created/modified in the subscribing object, and passed as a constructor parameter for the WatcherEx
object.
The WatcherExEventArgs Class
Anytime you see a custom event in a program, chances are pretty good that they will require their own custom EventArgs
-derived object. This particular example is no different. The reason you almost always want to create a custom argument object is so that you can pass specific data back to the
event subscriber. The data we need to pass back follows.
- The
FileSystemWatcherEx that's sending the event - I know this seems redundant since the sender parameter is exactly the same thing but you can never send back too much info. :)
- The original EventArgs-derived event argument - The
WatcherEx
class is essentially reflecting many different events, and some of them are of different types. Since we may want to be able to see the origial event arguments that were posted, we have to be able to pass them
back as part of this class.
- The EventArgs-derived argument type - This is represented as an enumerator for easier identification in the subscribing object. Instead of investigating the type, you can simply check this enumerator and cast more efficiently.
- The
NotifyFilters
item that triggered the event - This would allow you to handle all Changed events in a single subscriber method, and decide how to handle the even from a switch statement.
Here's the class source:
public class WatcherExEventArgs
{
public FileSystemWatcherEx Watcher { get; set; }
public object Arguments { get; set; }
public ArgumentType ArgType { get; set; }
public NotifyFilters Filter { get; set; }
public WatcherExEventArgs(FileSystemWatcherEx watcher,
object arguments,
ArgumentType argType,
NotifyFilters filter)
{
Watcher = watcher;
Arguments = arguments;
ArgType = argType;
Filter = filter;
}
public WatcherExEventArgs(FileSystemWatcherEx watcher,
object arguments,
ArgumentType argType)
{
Watcher = watcher;
Arguments = arguments;
ArgType = argType;
Filter = NotifyFilters.Attributes;
}
}
The WatcherEx Class
This is the class that wraps all of the internal FileSystemWatcherEx
objects. The first item of note is that the class inherits from IDisposable
. The reason for this is that the FileSystemWatcher
object is disposable, and I felt the need to control the disposing. (It may not even be necessary, but I'm doing it
anyway.)
public class WatcherEx : IDisposable
{
There are very few member variables - just enough to keep track of our internal watchers, the initialization object, and whether or not the object has been disposed.
private bool disposed = false;
private WatcherInfo watcherInfo = null;
private WatchersExList watchers = new WatchersExList();
Next, we have the event delegate definitions. You'll notice that there is one event delegate for each of the possible NotifyFilters
triggers.
public event WatcherExEventHandler EventChangedAttribute = delegate {};
public event WatcherExEventHandler EventChangedCreationTime = delegate {};
public event WatcherExEventHandler EventChangedDirectoryName = delegate {};
public event WatcherExEventHandler EventChangedFileName = delegate {};
public event WatcherExEventHandler EventChangedLastAccess = delegate {};
public event WatcherExEventHandler EventChangedLastWrite = delegate {};
public event WatcherExEventHandler EventChangedSecurity = delegate {};
public event WatcherExEventHandler EventChangedSize = delegate {};
public event WatcherExEventHandler EventCreated = delegate {};
public event WatcherExEventHandler EventDeleted = delegate {};
public event WatcherExEventHandler EventRenamed = delegate {};
public event WatcherExEventHandler EventError = delegate {};
public event WatcherExEventHandler EventDisposed = delegate {};
public event WatcherExEventHandler EventPathAvailability = delegate {};
Then we see some helper methods that make remove some of the chore of typing. These methods simply manipulate the two flag enumerators to find out if the specified ChangeType
or NotifyFilter
have been specified.
public bool HandleNotifyFilter(NotifyFilters filter)
{
return (((NotifyFilters)(watcherInfo.ChangesFilters & filter)) == filter);
}
public bool HandleWatchesFilter(WatcherChangeTypes filter)
{
return (((WatcherChangeTypes)(watcherInfo.WatchesFilters & filter)) == filter);
}
After the subscribing object has created a WatcherEX
object, it at some point call the Initialize method. This method is responsible for creating all of the necessary internal FileSystemWatcherEx
objects.
private void Initialize()
{
watcherInfo.BufferKBytes = Math.Max(4, Math.Min(watcherInfo.BufferKBytes, 64));
CreateWatcher(false, watcherInfo.ChangesFilters);
CreateWatcher(true, NotifyFilters.Attributes);
CreateWatcher(true, NotifyFilters.CreationTime);
CreateWatcher(true, NotifyFilters.DirectoryName);
CreateWatcher(true, NotifyFilters.FileName);
CreateWatcher(true, NotifyFilters.LastAccess);
CreateWatcher(true, NotifyFilters.LastWrite);
CreateWatcher(true, NotifyFilters.Security);
CreateWatcher(true, NotifyFilters.Size);
}
The first line in the method performs a sanity check on the buffer size. Default is 8k, minimum is 4k, and the maximum is 64k. The next line creates what I call the "main" FileSystemWatcherEx
object. This watcher is responsible for handling everything except Changed
events. Finally, the last eight lines create a FileSystemWatcherEx
object for each of the NotifyFilters
.
Here's the common CreateWatcher
method called from Initialize
. First, we create a watcher, and calculate the actual buffer size.
private void CreateWatcher(bool changedWatcher, NotifyFilters filter)
{
FileSystemWatcherEx watcher = null;
int bufferSize = (int)watcherInfo.BufferKBytes * 1024;
If the watcher we're trying to create is one of the Changed events. This code only creates a watcher for the specified filter if the filter was included in the WatcherInfo
initializing object. The appropriate settings are applied to the watcher, and the Changed
event is registered.
if (changedWatcher)
{
if (HandleNotifyFilter(filter))
{
watcher = new FileSystemWatcherEx(watcherInfo.WatchPath);
watcher.IncludeSubdirectories = watcherInfo.IncludeSubFolders;
watcher.Filter = watcherInfo.FileFilter;
watcher.NotifyFilter = filter;
watcher.InternalBufferSize = bufferSize;
switch (filter)
{
case NotifyFilters.Attributes :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedAttribute);
break;
case NotifyFilters.CreationTime :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedCreationTime);
break;
case NotifyFilters.DirectoryName :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedDirectoryName);
break;
case NotifyFilters.FileName :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedFileName);
break;
case NotifyFilters.LastAccess :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedLastAccess);
break;
case NotifyFilters.LastWrite :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedLastWrite);
break;
case NotifyFilters.Security :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedSecurity);
break;
case NotifyFilters.Size :
watcher.Changed += new FileSystemEventHandler(watcher_ChangedSize);
break;
}
}
}
If the watcher is the "main" watcher, we setup all of the appropriate events for it.
else
{
if (HandleWatchesFilter(WatcherChangeTypes.Created) ||
HandleWatchesFilter(WatcherChangeTypes.Deleted) ||
HandleWatchesFilter(WatcherChangeTypes.Renamed) ||
watcherInfo.WatchForError ||
watcherInfo.WatchForDisposed)
{
watcher = new FileSystemWatcherEx(watcherInfo.WatchPath,
watcherInfo.MonitorPathInterval);
watcher.IncludeSubdirectories = watcherInfo.IncludeSubFolders;
watcher.Filter = watcherInfo.FileFilter;
watcher.InternalBufferSize = bufferSize;
}
if (HandleWatchesFilter(WatcherChangeTypes.Created))
{
watcher.Created += new FileSystemEventHandler(watcher_CreatedDeleted);
}
if (HandleWatchesFilter(WatcherChangeTypes.Deleted))
{
watcher.Deleted += new FileSystemEventHandler(watcher_CreatedDeleted);
}
if (HandleWatchesFilter(WatcherChangeTypes.Renamed))
{
watcher.Renamed += new RenamedEventHandler(watcher_Renamed);
}
if (watcherInfo.MonitorPathInterval > 0)
{
watcher.EventPathAvailability += new PathAvailabilityHandler(watcher_EventPathAvailability);
}
}
Finally, we register the Error and Disposed events if necessary, and add the watcher to our list. Notice that these handlers are registered for EVERY watcher we create if the programmer specified them in the WatcherInfo
object. If you want more "atomic" application of these events, I leave it to you to implement.
if (watcher != null)
{
if (watcherInfo.WatchForError)
{
watcher.Error += new ErrorEventHandler(watcher_Error);
}
if (watcherInfo.WatchForDisposed)
{
watcher.Disposed += new EventHandler(watcher_Disposed);
}
watchers.Add(watcher);
}
}
The last thing of any interest in the class are the Start
/Stop
methods. They simply set all of the watchers' EnableRaisingEvent
property to the value appropriate for the method.
public void Start()
{
watchers[0].StartFolderMonitor();
for (int i = 0; i < watchers.Count; i++)
{
watchers[i].EnableRaisingEvents = true;
}
}
public void Stop()
{
watchers[0].StopFolderMonitor();
for (int i = 0; i < watchers.Count; i++)
{
watchers[i].EnableRaisingEvents = false;
}
}
The remaining methods in the object are the watcher event handlers, and while not very interesting, I'll show you one of them in the interest of completeness.
private void watcher_ChangedAttribute(object sender, FileSystemEventArgs e)
{
EventChangedAttribute(this, new WatcherExEventArgs(sender as FileSystemWatcherEx,
e,
ArgumentType.FileSystem,
NotifyFilters.Attributes));
}
Usage Example - The Form In The Demo Application
The form itself is a simple affair, providing the following controls:
- A TextBox/Browse button combo that allows you to specify
a folder to monitor
- A checkbox to indicate that you want to include sub-directories while monitoring
- A listView to display events as the form receives them
- A Clear button to clear the list contents (mostly because I wanted to keep the
ListView
fairly free of clutter for Part 2 of this article.
- A Start/Stop button to control the watcher
We start off with some helper methods. The first is one registers/unregisters WatcherEx
events. Notice that I handle all of the Changed
events in one method. This certainly isn't a
requirement, and you should feel free to do it differently if you so choose. In the interest of brevity, the snippet below just shows the registering code (the unregistering code is in the actual demo).
private void ManageEventHandlers(bool add)
{
if (fileWatcher != null)
{
if (add)
{
fileWatcher.EventChangedAttribute += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventChangedCreationTime += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventChangedDirectoryName += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventChangedFileName += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventChangedLastAccess += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventChangedLastWrite += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventChangedSecurity += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventChangedSize += new WatcherExEventHandler(fileWatcher_EventChanged);
fileWatcher.EventCreated += new WatcherExEventHandler(fileWatcher_EventCreated);
fileWatcher.EventDeleted += new WatcherExEventHandler(fileWatcher_EventDeleted);
fileWatcher.EventDisposed += new WatcherExEventHandler(fileWatcher_EventDisposed);
fileWatcher.EventError += new WatcherExEventHandler(fileWatcher_EventError);
fileWatcher.EventRenamed += new WatcherExEventHandler(fileWatcher_EventRenamed);
fileWatcher.EventPathAvailability += new WatcherExEventHandler(fileWatcher_EventPathAvailability);
}
else
{
}
}
}
Next, we have the InitWatcher()
method. It's purpose in life is to create and initialize the WatcherEx
object. For the purpose of this demo, I'm monitoring all of the NotifyFilter
events, but in actuality, you probably will never want to do that.
private bool InitWatcher()
{
bool result = false;
if (Directory.Exists(this.textBoxFolderToWatch.Text) ||
File.Exists(this.textBoxFolderToWatch.Text))
{
WatcherInfo info = new WatcherInfo();
info.ChangesFilters = NotifyFilters.Attributes |
NotifyFilters.CreationTime |
NotifyFilters.DirectoryName |
NotifyFilters.FileName |
NotifyFilters.LastAccess |
NotifyFilters.LastWrite |
NotifyFilters.Security |
NotifyFilters.Size;
info.IncludeSubFolders = this.checkBoxIncludeSubfolders.Checked;
info.WatchesFilters = WatcherChangeTypes.All;
info.WatchForDisposed = true;
info.WatchForError = false;
info.WatchPath = this.textBoxFolderToWatch.Text;
info.BufferKBytes = 8;
info.MonitorPathInterval = 250;
fileWatcher = new WatcherEx(info);
ManageEventHandlers(true);
result = true;
}
else
{
MessageBox.Show("The folder (or file) specified does not exist...");
}
return result;
}
Because the ListView
could potentially be updated from another thread (remember, the FileSystemWatcherEx
object runs a thread that monitors the existance of the folder being watched), we need to be able to access it through a delegate.
private delegate void DelegateCreateListViewItem(string eventName,
string filterName,
string fileName);
And our delegate method looks like this.
private void InvokedCreateListViewItem(string eventName, string filterName, string fileName)
{
ListViewItem lvi = new ListViewItem();
lvi.Text = DateTime.Now.ToString("HH:mm");
lvi.SubItems.Add(new ListViewItem.ListViewSubItem(lvi, eventName));
lvi.SubItems.Add(new ListViewItem.ListViewSubItem(lvi, filterName));
lvi.SubItems.Add(new ListViewItem.ListViewSubItem(lvi, fileName));
this.listView1.Items.Add(lvi);
}
All of the event handlers call this method in order to update the ListView
.
void CreateListViewItem(string eventName, string filterName, string fileName)
{
if (this.listView1.InvokeRequired)
{
DelegateCreateListViewItem method = new DelegateCreateListViewItem(InvokedCreateListViewItem);
Invoke(method, new object[3]{eventName, filterName, fileName} );
}
else
{
InvokedCreateListViewItem(eventName, filterName, fileName);
}
}
When you're done with the WatcherEx
object, you should call Dispose
on it.
private void Form1Ex_FormClosing(object sender, FormClosingEventArgs e)
{
if (fileWatcher != null)
{
ManageEventHandlers(false);
fileWatcher.Dispose();
fileWatcher = null;
}
}
The ListView Control
While playing with the demo app, I noticed that it flickered quite a bit, and searched around until I found a quick-and-easy fix. I ended up with a solution from a user named stromenet on Stackoverflow.
This solution involves writing a new class that inherits from the original ListView
class, sets DoubleBuffering
and intercepts/eats WM_BACKGROUND
messages. Here's the code (and
many thanks once more to the StackOverflow user stormenet.
public class ListViewNoFlicker : System.Windows.Forms.ListView
{
public ListViewNoFlicker()
{
this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.EnableNotifyMessage, true);
}
protected override void OnNotifyMessage(Message m)
{
if (m.Msg != 0x14)
{
base.OnNotifyMessage(m);
}
}
}
I don't know what side-effects this will have regarding the background image in cells, but since we're not directly concerned with those issues in this demo application, I'll leave it as an exercise for the reader to figure out. I also did NOT use this inherited ListView in the regular form (that inherits from the original DotNet version of the FileSystemWatcher
object).
Other Comments
When I started the demo app, I had originally created the Watcher
class that used the original DotNet FileSystemWatcher
object. During my research into this object, I discovered George Oakes article (referenced and linked to near the top of this article), and thought it might make for a more complete and appropriate implementation. For this reason, I essentially duplicated the original Watcher
object in the newer WatcherEx
class. I was originally going to remove the Watcher
object from the code, but I figured that there might be a number of you that don't want/need to use the WatcherEx
object, so I left BOTH versions in the demo. Each version of the class has its own form in the demo. The demo currently only uses the WatcherEx version of the form, but it's a simple matter to change Program.cs
to use the form you want to use. Be aware that the non-Ex version of the form may not be as up-to-date, and therefore might required some tweaking.
Part 2 In The Series
Because this article ended up being fairly lengthy, I decided to create a multi-part article series, with Part 2 centering on observations made while using demo application in preparation for writing this article.
View Part 2 of this article series.
History
12/18/2010 - Changed some text to make it more readable, and fixed a few misspellings.
09/30/2010 - Fixed a link to another CodeProject artical, and some errant <code> tags.
02/14/2010 - Original version.