这是另一种方法。与传播快速连续事件序列的第一个事件并抑制其余事件不同,这里除了最后一个事件外,所有事件都被抑制。我认为可以从这种方法中获益的情况更为普遍。这种策略的技术术语是
去抖动。
为了实现这一点,我们必须使用滑动延迟。每个传入的事件都会取消将触发先前事件的计时器,并启动新的计时器。这打开了永无止境的事件系列会永远延迟传播的可能性。为了保持简单,下面的扩展方法没有针对这种异常情况提供任何规定。
public static class FileSystemWatcherExtensions
{
public static IDisposable OnAnyEvent(this FileSystemWatcher source,
WatcherChangeTypes changeTypes, FileSystemEventHandler handler,
TimeSpan dueTime)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(handler);
if (dueTime < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(dueTime));
Dictionary<string, CancellationTokenSource> dictionary = new(
StringComparer.OrdinalIgnoreCase);
if (changeTypes.HasFlag(WatcherChangeTypes.Created))
source.Created += FileSystemWatcher_Event;
if (changeTypes.HasFlag(WatcherChangeTypes.Deleted))
source.Deleted += FileSystemWatcher_Event;
if (changeTypes.HasFlag(WatcherChangeTypes.Changed))
source.Changed += FileSystemWatcher_Event;
if (changeTypes.HasFlag(WatcherChangeTypes.Renamed))
source.Renamed += FileSystemWatcher_Event;
return new Disposable(() =>
{
source.Created -= FileSystemWatcher_Event;
source.Deleted -= FileSystemWatcher_Event;
source.Changed -= FileSystemWatcher_Event;
source.Renamed -= FileSystemWatcher_Event;
lock (dictionary)
{
foreach (CancellationTokenSource cts in dictionary.Values)
cts.Cancel();
dictionary.Clear();
}
});
async void FileSystemWatcher_Event(object sender, FileSystemEventArgs e)
{
string key = e.FullPath;
using (CancellationTokenSource cts = new())
{
lock (dictionary)
{
CancellationTokenSource existingCts;
dictionary.TryGetValue(key, out existingCts);
dictionary[key] = cts;
existingCts?.Cancel();
}
Task delayTask = Task.Delay(dueTime, cts.Token);
await Task.WhenAny(delayTask).ConfigureAwait(false);
if (delayTask.IsCanceled) return;
lock (dictionary)
{
CancellationTokenSource existingCts;
dictionary.TryGetValue(key, out existingCts);
if (!ReferenceEquals(existingCts, cts)) return;
dictionary.Remove(key);
}
}
ISynchronizeInvoke syncObj = source.SynchronizingObject;
if (syncObj != null && syncObj.InvokeRequired)
syncObj.BeginInvoke(handler, new object[] { sender, e });
else
handler(sender, e);
}
}
public static IDisposable OnAllEvents(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.All, handler, delay);
public static IDisposable OnCreated(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Created, handler, delay);
public static IDisposable OnDeleted(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Deleted, handler, delay);
public static IDisposable OnChanged(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Changed, handler, delay);
public static IDisposable OnRenamed(this FileSystemWatcher source,
FileSystemEventHandler handler, TimeSpan delay)
=> OnAnyEvent(source, WatcherChangeTypes.Renamed, handler, delay);
private class Disposable : IDisposable
{
private Action _action;
public Disposable(Action action) => _action = action;
public void Dispose()
{
try { _action?.Invoke(); } finally { _action = null; }
}
}
}
使用示例:
IDisposable subscription = myWatcher.OnAnyEvent(
WatcherChangeTypes.Created | WatcherChangeTypes.Changed,
MyFileSystemWatcher_Event, TimeSpan.FromMilliseconds(100));
这行代码将订阅两个事件,Created
和 Changed
。因此,它大致相当于以下代码:
myWatcher.Created += MyFileSystemWatcher_Event;
myWatcher.Changed += MyFileSystemWatcher_Event;
两个事件被视为单一类型的事件,不同之处在于,在这些事件快速连续发生的情况下,只有最后一个事件将被传播。例如,如果一个Created
事件后跟着两个Changed
事件,并且这三个事件之间没有大于100毫秒的时间间隔,则仅通过调用MyFileSystemWatcher_Event
处理程序传播第二个Changed
事件,而前面的事件将被丢弃。
IDisposable
返回值可用于取消订阅事件。调用subscription.Dispose()
会取消并丢弃所有记录的事件,但不会停止或等待正在执行的任何处理程序。
特别针对Renamed
事件,可以将FileSystemEventArgs
参数转换为RenamedEventArgs
以便访问此事件的额外信息。例如:
void MyFileSystemWatcher_Event(object s, FileSystemEventArgs e)
{
if (e is RenamedEventArgs re) Console.WriteLine(re.OldFullPath);
如果已经配置,防抖事件将在FileSystemWatcher.SynchronizingObject
上调用,否则将在ThreadPool
上调用。调用逻辑已从.NET 7 源代码中复制粘贴。