解决 FileSystemWatcher 多次触发事件的健壮方案






4.93/5 (20投票s)
这是解决 FileSystemWatcher 多次触发事件的健壮方案
问题
FileSystemWatcher 是一个很棒的类,可以简化文件夹和文件活动监控,但由于它本身没有缺陷,它的行为可能无法预测,对单个操作触发多个事件。
请注意,在某些情况下,例如下面使用的示例,第一个事件将是文件写入的开始,第二个事件将是结束,虽然这不是文档化的行为,但至少是可预测的。 尝试使用一个非常大的文件来亲自查看。
但是,FileSystemWatcher
不能保证对所有操作系统和应用程序行为表现出可预测的行为。 另请参阅 MSDN 文档。
常见的文件系统操作可能会引发多个事件。 例如,当文件从一个目录移动到另一个目录时,可能会引发多个
OnChanged
事件以及一些OnCreated
和OnDeleted
事件。 移动文件是一个复杂的操作,包含多个简单的操作,因此会引发多个事件。 同样,某些应用程序(例如,防病毒软件)可能会导致FileSystemWatcher
检测到的其他文件系统事件。
示例:在记事本中重新创建编辑文件触发 2 个事件
如上所述,我们知道此操作的 2 个事件将标记写入的开始和结束,这意味着如果我们完全信任,我们可以只关注第二个事件。 为了本文的目的,这提供了一个方便的示例来重现。
如果您在 c:\temp 中编辑了文本文件,您将获得 2 个事件触发。
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)
{
// This will fire twice if I edit a file in Notepad
}
}
两种完整的控制台应用程序都可以在 Github 上找到。
一种可靠的解决方案
良好地使用 NotifyFilters(请参阅我的关于如何选择 NotifyFilters 的文章)会有所帮助,但仍然有很多场景,例如上述场景,额外的事件仍然会通过文件系统事件。
我与同事 Ross Sandford 合作,利用 MemoryCache
作为缓冲区来“限制”额外的事件。
- 触发文件事件(在下面的示例中为 changed)
- 该事件由
OnChanged
处理。 但是,不是完成所需的操作,而是将事件存储在MemoryCache
中,过期时间为 1 秒,并设置CacheItemPolicy
回调,以便在过期时执行。 - 当它过期时,回调
OnRemovedFromCache
完成为该文件事件预期的行为。
请注意,我使用 AddOrGetExisting
作为一种简单的方法来阻止在缓存期间添加的任何其他事件触发到缓存中。
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;
}
}
如果您希望处理多个、不同的、来自单个/唯一文件的事件,例如 Created 与 Changed,那么您可以将缓存键设置为文件名和事件名称的串联。