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

Multiple Created Events Generated for a Single Action

You may notice in certain situations that a single creation event generates multiple Created events that are handled by your component. For example, if you use a FileSystemWatcher component to monitor the creation of new files in a directory, and then test it by using Notepad to create a file, you may see two Created events generated even though only a single file was created. This is because Notepad performs multiple file system actions during the writing process. Notepad writes to the disk in batches that create the content of the file and then the file attributes. Other applications may perform in the same manner. Because FileSystemWatcher monitors the operating system activities, all events that these applications fire will be picked up.

Note: Notepad may also cause other interesting event generations. For example, if you use the ChangeEventFilter to specify that you want to watch only for attribute changes, and then you write to a file in the directory you are watching using Notepad, you will raise an event. This is because Notepad updates the Archived attribute for the file during this operation.

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

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 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 event FileSystemEvent Change;

    public void Start()
    {
      m_fileSystemWatcher.EnableRaisingEvents = true;
    }

    private void OnChange(object sender, FileSystemEventArgs e)
    {
      // 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);
      }
    }
  }
}
Conversation
  • Hamomelette says:

    Both post and code were supremely helpful. Thanks.

  • D says:

    Thanks, I was able to adapt this to watch a single file instead of an entire directory.

  • M Miles says:

    A big thank you from the future! Unsurprisingly, (thanks Microsoft) this is still an issue that I have dug deep to find an issue, I will try out your implementation and try to improve on it!

  • Comments are closed.