FileSystemWatcher will fire multiple times for the same event; the example wrapper attempts to solve that problem.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
// ----------------------------------------------------------------------------
// Consolidate Multiple FileSystemWatcher Events
//
// The .NET framework provides a FileSystemWatcher class that can be used to
// monitor the file system for changes. My requirements were to monitor a
// directory for new files or changes to existing files. When a change occurs,
// the application needs to read the file and immediately perform some operation
// based on the contents of the file.
//
// While doing some manual testing of my initial implementation it was very
// obvious that the FileSystemWatcher was firing multiple events whenever I made
// a change to a file or copied a file into the directory being monitored. I
// came across the following in the MSDN documentation's Troubleshooting
// FileSystemWatcher Components.
//
// I did some searching and was surprised that .NET did not provide any kind of
// wrapper around the FileSystemWatcher to make it a bit more user friendly. I
// ended up writing my own wrapper that would monitor a directory and only throw
// one event when a new file was created, or an existing file was changed.
//
// In order to consolidate the multiple FileSystemWatcher events down to a
// single event, I save the timestamp when each event is received, and I check
// back every so often (using a Timer) to find paths that have not caused
// additional events in a while. When one of these paths is ready, a single
// Changed event is fired. An additional benefit of this technique is that the
// event from the FileSystemWatcher is handled very quickly, which could help
// prevent its internal buffer from filling up.
//
// Here is the code for a DirectoryMonitor class that consolidates multiple
// Win32 events into a single Change event for each change:
//
// https://spin.atomicobject.com/2010/07/08/consolidate-multiple-filesystemwatcher-events/
// July 8, 2010 by: Patrick Bacon
// ----------------------------------------------------------------------------
namespace FileSystem
{
public delegate void FileSystemEvent(string path);
public interface IDirectoryMonitor
{
event FileSystemEvent Change;
void Start();
}
public class DirectoryMonitor : IDirectoryMonitor
{
private readonly FileSystemWatcher m_fileSystemWatcher = new FileSystemWatcher();
private readonly Dictionary<string, DateTime> m_pendingEvents = new Dictionary<string, DateTime>();
private readonly Timer m_timer;
private bool m_timerStarted = false;
public event FileSystemEvent Change;
public DirectoryMonitor(string dirPath)
{
m_fileSystemWatcher.Path = dirPath;
m_fileSystemWatcher.IncludeSubdirectories = false;
m_fileSystemWatcher.Created += new FileSystemEventHandler(OnChange);
m_fileSystemWatcher.Changed += new FileSystemEventHandler(OnChange);
m_timer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite);
}
public void Start()
{
m_fileSystemWatcher.EnableRaisingEvents = true;
}
private void OnChange(object sender, FileSystemEventArgse)
{
// Don't want other threads messing with the pending events right now
lock (m_pendingEvents)
{
// Save a timestamp for the most recent event for this path
m_pendingEvents[e.FullPath] = DateTime.Now;
// Start a timer if not already started
if (!m_timerStarted)
{
m_timer.Change(100, 100);
m_timerStarted = true;
}
}
}
private void OnTimeout(object state)
{
List<string> paths;
// Don't want other threads messing with the pending events right now
lock (m_pendingEvents)
{
// Get a list of all paths that should have events thrown
paths = FindReadyPaths(m_pendingEvents);
// Remove paths that are going to be used now
paths.ForEach(delegate(string path)
{
m_pendingEvents.Remove(path);
});
// Stop the timer if there are no more events pending
if (m_pendingEvents.Count == 0)
{
m_timer.Change(Timeout.Infinite, Timeout.Infinite);
m_timerStarted = false;
}
}
// Fire an event for each path that has changed
paths.ForEach(delegate(string path)
{
FireEvent(path);
});
}
private List<string> FindReadyPaths(Dictionary<string, DateTime> events)
{
List<string> results = new List<string>();
DateTime now = DateTime.Now;
foreach (KeyValuePair<string, DateTime> entry in events)
{
// If the path has not received a new event in the last 75ms
// an event for the path should be fired
double diff = now.Subtract(entry.Value).TotalMilliseconds;
if (diff >= 75)
{
results.Add(entry.Key);
}
}
return results;
}
private void FireEvent(string path)
{
FileSystemEvent evt = Change;
if (evt != null)
{
evt(path);
}
}
}
}