Skip to main content

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);
            }
        }
    }
}