Filesystemwatcher Changed Event Is Raised Twice

c# FileSystemWatcher triggers twice when listening to OnChanged

You can filter it out yourself, as I've posted here.

FileSystemWatcher events called multiple times

This behavior is documented. What is also documented is that you are not guaranteed that all events will be delivered. Application must protect itself against either case.

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.

Also note that the above is Win32 behavior, FSW is just wrapping up the operating system feature into a nicer interface.

A robust solution for FileSystemWatcher firing events multiple times

An approach utilising MemoryCache as a buffer that will 'throttle' additional events.

  1. A file event (Changed in this example) is triggered
  2. 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.

Note that I use AddOrGetExisting as an simple way to block any additional events firing within the cache period being added to the cache.


  1. When it expires, the callback OnRemovedFromCache completes the behaviour intended for that file event

.

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

// Add file event to cache for CacheTimeMilliseconds
private void OnChanged(object source, FileSystemEventArgs e)
{
_cacheItemPolicy.AbsoluteExpiration =
DateTimeOffset.Now.AddMilliseconds(CacheTimeMilliseconds);

// Only add if it is not there already (swallow others)
_memCache.AddOrGetExisting(e.Name, e, _cacheItemPolicy);
}

// Handle cache item expiring
private void OnRemovedFromCache(CacheEntryRemovedArguments args)
{
if (args.RemovedReason != CacheEntryRemovedReason.Expired) return;

// Now actually handle file event
var e = (FileSystemEventArgs) args.CacheItem.Value;
}
}

Could easily extend to:

  • Check file lock on expiry from cache and if not available, put it back in the cache again (sometimes events fire so fast the file isn't ready for some operations). Preferable to using try/catch loops.
  • Key cache on file name + event type combined

FileSystemWatcher raised only after first change

My suggestion is to try adding this.watcher.Renamed to the events that are handled, and then take care not to filter out the Renamed event when you apply the this.watcher.NotifyFilter expression. This will at least compensate for the way Excel performs a Save operation called from its menu. Based on the test methodology shown below, the Changed event will never be fired. When I made a mock MachineWatcher and hooked up all the events including Renamed, the first events occur when a temporary file named ~$ExcelTestFile.xlsx is created when opening the file. Then, every time a Save operation happens, the following sequence takes place:

  1. Created event for temporary file (for example, on this pass it's arbitrarily named ECA83C30).
  2. This tmp file fires two Changed events (presumably as the modified version of the main file is copied to it).
  3. Renamed event for D03DF176.tmp (presumably from ECA83C30)
  4. Renamed event for the real target, a file named ExcelTestFile.xlxs (presumably a copy operation from D03DF176.tmp)
  5. Delete event for D03DF176.tmp

But no Change events for the target Excel file even though it ends up with a new LastWriteTime. This was a shocker to me, but see if you can repro!

test log


Mock a minimal MachineWatcher

public class MachineWatcher
{
public MachineWatcher(string type, string directoryStock, string fileFilter)
{
watcher = new FileSystemWatcher(directoryStock, fileFilter);
watcher.Created += onModified;
watcher.Changed += onModified;
watcher.Renamed += onModified;
watcher.Deleted += onModified;
// watcher.NotifyFilter = NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.CreationTime;
watcher.EnableRaisingEvents = true;
}
FileSystemWatcher watcher { get; }

private void onModified(object sender, FileSystemEventArgs e)
{
switch (e.ChangeType)
{
case WatcherChangeTypes.Created:
OnFeedBackNesterCreated(sender, e);
Console.WriteLine($" LastWriteTime: {new FileInfo(e.FullPath).LastWriteTime}");
break;
case WatcherChangeTypes.Deleted:
Console.WriteLine($"Deleted: {e.Name}");
break;
case WatcherChangeTypes.Changed:
var ext = Path.GetExtension(e.FullPath);
switch (ext)
{
case ".xlsx":
Console.Write($"Changed: {e.Name}");
break;
case ".txt":
try
{
Console.Write($"Changed: {e.Name} {File.ReadAllLines(e.FullPath).Last()}");
}
catch
{
Console.Write($"Changed: {e.Name} (in transition)");
}
break;
case "":
Console.Write($"Changed: {e.Name} (no extension)");
break;
default:
Console.Write($"The '{ext}' extension is not supported");
break;
}
Console.WriteLine($" LastWriteTime: {new FileInfo(e.FullPath).LastWriteTime}");
break;
case WatcherChangeTypes.Renamed:
Console.Write($"Renamed: {e.Name}");
Console.WriteLine($" LastWriteTime: {new FileInfo(e.FullPath).LastWriteTime}");
break;
default:
break;
}
}

private void OnFeedBackNesterCreated(object source, FileSystemEventArgs e)
{
Console.Write($"Created: {e.Name}");
}
}

Exercise the MachineWatcher using Console

static void Main(string[] args)
{
const int SPACING = 500;
string appData = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"custom_file_system_watcher");

// Ensure that the root directory exists
Directory.CreateDirectory(appData);

// Make an instance of MachineWatcher
var mw = new MachineWatcher(
null, // In minimal reproducible sample this is unused
appData,
"*.*");

// Force Delete (if exists)
var testFile = Path.Combine(appData, "testFile.txt");
File.Delete(testFile);
Thread.Sleep(SPACING);

// Force Create + Change
File.WriteAllText(
testFile,
$"{DateTime.Now}{Environment.NewLine}");
Thread.Sleep(SPACING);

// Force N Changes
var N = 5;
for (int i = 1; i <= N; i++)
{
// Using Append because File.WriteAllText is two events not one.
File.AppendAllText(testFile, $"Change #{i}{Environment.NewLine}");
Thread.Sleep(SPACING);
}

// Force Rename
var testFileRenamed = Path.Combine(appData, "testFile.Renamed.txt");
File.Copy(testFile, testFileRenamed, overwrite: true);
Thread.Sleep(SPACING);

// Prove that if the Excel file LastWriteTime changes, we'll see it
var excelFile = Path.Combine(appData, "ExcelTestFile.xlsx");
var fileInfo = new FileInfo(excelFile);
if(fileInfo.Exists)
{
Console.WriteLine();
Console.WriteLine("Proves that if the Excel file LastWriteTime changes, we'll see it:");
try
{
fileInfo.LastWriteTime = DateTime.Now;
}
catch
{
Console.WriteLine("CANNOT CHANGE TIMESTAMP: EXCEL FILE IS ALREADY OPEN");
}
}
Thread.Sleep(SPACING);

Console.WriteLine();
Console.WriteLine("Waiting for Excel file changes...");
Console.ReadKey();
}


Related Topics



Leave a reply



Submit