The Problem
FileSystemWatcher is a great little class to take the hassle out of monitoring activity in folders and files but, through no real fault of its own, it can behave unpredictably, firing multiple events for a single action.
Note that in some scenarios, like the example used below, the first event will be the start of the file writing and the second event will be the end, which, while not documented behaviour, is at least predictable. Try it with a very large file to see for yourself.
However, FileSystemWatcher
cannot make any promises to behave predictably for all OS and application behaviours. See also, MSDN documentation:
Common file system operations might raise more than one event. For example, when a file is moved from one directory to another, several OnChanged
and some OnCreated
and OnDeleted
events might be raised. Moving a file is a complex operation that consists of multiple simple operations, therefore raising multiple events. Likewise, some applications (for example, antivirus software) might cause additional file system events that are detected by FileSystemWatcher
.
Example: Recreating Edit a File in Notepad Firing 2 Events
As stated above, we know that 2 events from this action would mark the start and end of a write, meaning we could just focus on the second, if we had full confidence, this would be the consistent behaviour. For the purposes of this article, it makes for a convenient example to recreate.
If you edited a text file in c:\temp, you would get 2 events firing.
class ExampleAttributesChangedFiringTwice
{
public ExampleAttributesChangedFiringTwice(string demoFolderPath)
{
var watcher = new FileSystemWatcher()
{
Path = demoFolderPath,
NotifyFilter = NotifyFilters.LastWrite,
Filter = "*.txt"
};
watcher.Changed += OnChanged;
watcher.EnableRaisingEvents = true;
}
private static void OnChanged(object source, FileSystemEventArgs e)
{
}
}
Complete Console applications for both are available on Github.
A Robust Solution
Good use of NotifyFilters (see my post on how to select NotifyFilters) can help but there are still plenty of scenarios, like those above, where additional events will still get through for a file system event.
I worked on a nice little idea with a colleague, Ross Sandford, utilising MemoryCache
as a buffer to ‘throttle’ additional events.
- A file event (changed in the example below) is triggered
- The event is handled by
OnChanged
. But instead of completing the desired action, it stores the event in MemoryCache
with a 1 second expiration and a CacheItemPolicy
callback setup to execute on expiration. - When it expires, the callback
OnRemovedFromCache
completes the behaviour intended for that file event.
Note that I use AddOrGetExisting
as a simple way to block any additional events firing within the cache period being added to the cache.
class BlockAndDelayExample
{
private readonly MemoryCache _memCache;
private readonly CacheItemPolicy _cacheItemPolicy;
private const int CacheTimeMilliseconds = 1000;
public BlockAndDelayExample(string demoFolderPath)
{
_memCache = MemoryCache.Default;
var watcher = new FileSystemWatcher()
{
Path = demoFolderPath,
NotifyFilter = NotifyFilters.LastWrite,
Filter = "*.txt"
};
_cacheItemPolicy = new CacheItemPolicy()
{
RemovedCallback = OnRemovedFromCache
};
watcher.Changed += OnChanged;
watcher.EnableRaisingEvents = true;
}
private void OnChanged(object source, FileSystemEventArgs e)
{
_cacheItemPolicy.AbsoluteExpiration =
DateTimeOffset.Now.AddMilliseconds(CacheTimeMilliseconds);
_memCache.AddOrGetExisting(e.Name, e, _cacheItemPolicy);
}
private void OnRemovedFromCache(CacheEntryRemovedArguments args)
{
if (args.RemovedReason != CacheEntryRemovedReason.Expired) return;
var e = (FileSystemEventArgs) args.CacheItem.Value;
}
}
If you’re looking to handle multiple, different, events from a single/unique file, such as Created with Changed, then you could key the cache on the file name and event named, concatenated.