FileSystemWatcher的Changed事件被触发两次

390

我有一个应用程序,在其中寻找一个文本文件,如果文件有任何更改,我将使用 OnChanged 事件处理程序来处理该事件。我正在使用 NotifyFilters.LastWriteTime 但是仍然会触发两次事件。以下是代码。

public void Initialize()
{
   FileSystemWatcher _fileWatcher = new FileSystemWatcher();
  _fileWatcher.Path = "C:\\Folder";
  _fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
  _fileWatcher.Filter = "Version.txt";
  _fileWatcher.Changed += new FileSystemEventHandler(OnChanged);
  _fileWatcher.EnableRaisingEvents = true;
}

private void OnChanged(object source, FileSystemEventArgs e)
{
   .......
}
在我的情况下,当我改变文本文件version.txt并保存它时,OnChanged被调用了两次。

这是一个解决方法,但应该根据解决方法的质量来评判。跟踪更改效果完美且简单。OP正在寻找一种抑制重复事件的方法,下面的回复就提供了这种方法。https://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.created.aspx解释了多个事件可能是由于反病毒软件或其他“复杂的文件系统问题”引起的(听起来只是借口)。 - Tyler Montney
2
我最近打开了这个问题 https://github.com/Microsoft/dotnet/issues/347 - Stephan Ahlf
3
我创建了一个类,可以帮助您获取一个事件。您可以从https://github.com/melenaos/FileSystemSafeWatcher获取代码。 - Menelaos Vergis
上面Menelaos Vergis提供的解决方案百分之百有效。 - undefined
45个回答

4

一种可能的“黑客”方法是使用Reactive Extensions对事件进行节流,例如:

var watcher = new FileSystemWatcher("./");

Observable.FromEventPattern<FileSystemEventArgs>(watcher, "Changed")
            .Throttle(new TimeSpan(500000))
            .Subscribe(HandleChangeEvent);

watcher.EnableRaisingEvents = true;

在这种情况下,我将限制到50毫秒,在我的系统上这已经足够了,但更高的值应该更安全。(并且像我说的那样,这仍然是一种“hack”)。

1
我使用了.Distinct(e => e.FullPath),我发现这种方式更加直观易懂。这样你就可以恢复API所期望的行为了。 - Kjellski
Reactive库似乎是处理这个问题的一种相当强大的方式!绝非hack。这里有一些示例代码,用于处理文件目录的写入和重命名。它使用GroupBy将Change流拆分为子流(每个文件一个),然后对其进行Throttle。它似乎非常有效,即使在网络传输缓慢的情况下也是如此! - David Peters

3

我有一个非常快速简单的解决方法,对我有效,无论事件是否偶尔触发一次或两次或多次,请查看:

private int fireCount = 0;
private void inputFileWatcher_Changed(object sender, FileSystemEventArgs e)
    {
       fireCount++;
       if (fireCount == 1)
        {
            MessageBox.Show("Fired only once!!");
            dowork();
        }
        else
        {
            fireCount = 0;
        }
    }
}

起初我以为这对我有用,但实际上并不是。我的情况是,文件内容有时会被覆盖,有时会被删除并重新创建。虽然你的解决方案似乎在文件被覆盖的情况下有效,但在文件被重新创建的情况下并不总是有效。在后一种情况下,有时会丢失事件。 - user4849927
尝试将不同类型的事件分类并分别处理,我只是提供了一个可能的解决方法。祝你好运。 - Xiaoyuvax
虽然我没有测试过,但我不太确定这个方法在创建和删除方面是否有效。从理论上讲,它应该是可行的。因为fireCount++和if()语句都是原子操作,不会被阻塞。即使有两个触发事件相互竞争,也不会出现问题。我猜你遇到麻烦可能是因为其他原因。(你说的“丢失”是什么意思?) - Xiaoyuvax

3

这里有一个新的解决方案,您可以尝试一下。对于更改事件的事件处理程序,您可以编程删除处理程序,如果需要,从设计器中输出一条消息,然后再次编程添加处理程序。例如:

public void fileSystemWatcher1_Changed( object sender, System.IO.FileSystemEventArgs e )
    {            
        fileSystemWatcher1.Changed -= new System.IO.FileSystemEventHandler( fileSystemWatcher1_Changed );
        MessageBox.Show( "File has been uploaded to destination", "Success!" );
        fileSystemWatcher1.Changed += new System.IO.FileSystemEventHandler( fileSystemWatcher1_Changed );
    }

1
您不需要调用委托类型的构造函数。this.fileSystemWatcher1.Changed -= this.fileSystemWatcher1_Changed; 应该可以正常工作。 - bartonjs
@bartonjs 谢谢你,我不确定为什么我调用了整个构造函数。老实说,这很可能是新手的错误。无论如何,我的临时解决方法似乎运行得相当不错。 - Fancy_Mammoth

3
我希望只对最后一个事件做出反应,以防万一,在Linux文件更改时,似乎在第一次调用时该文件为空,然后在下一次填充后再次填充,并且不介意浪费一些时间,以防操作系统决定进行一些文件/属性更改。我在这里使用.NET异步来帮助我进行线程处理。"Original Answer"的翻译是"最初的回答"。
    private static int _fileSystemWatcherCounts;
    private async void OnChanged(object sender, FileSystemEventArgs e)
    {
        // Filter several calls in short period of time
        Interlocked.Increment(ref _fileSystemWatcherCounts);
        await Task.Delay(100);
        if (Interlocked.Decrement(ref _fileSystemWatcherCounts) == 0)
            DoYourWork();
    }

非常聪明的解决方案。谢谢你分享这段代码。 - Alexandru Dicu

2
主要原因是第一个事件的最后访问时间是当前时间(文件写入或更改时间),然后第二个事件是文件的原始最后访问时间。我通过以下代码解决了这个问题。
        var lastRead = DateTime.MinValue;

        Watcher = new FileSystemWatcher(...)
        {
            NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
            Filter = "*.dll",
            IncludeSubdirectories = false,
        };
        Watcher.Changed += (senderObject, ea) =>
        {
            var now = DateTime.Now;
            var lastWriteTime = File.GetLastWriteTime(ea.FullPath);

            if (now == lastWriteTime)
            {
                return;
            }

            if (lastWriteTime != lastRead)
            {
                // do something...
                lastRead = lastWriteTime;
            }
        };

        Watcher.EnableRaisingEvents = true;

以下是与FileSystemWatcher类相关的问题。当文件系统中的文件更改时,FileSystemWatcher对象引发多个Changed事件。这是因为某些编辑器在保存文件时会执行两次操作:一次是将文件保存到临时文件夹中,另一次是将文件移回原始位置。这可能导致FileSystemWatcher对象引发多个Changed事件。解决此问题的方法之一是使用一个计时器来延迟处理事件,以便在一定时间内收集所有事件并仅处理最后一个事件。另一种方法是检查事件的时间戳,并仅处理最新的事件。 - Jean-Paul

2

主要是为了以后方便自己 :)

我使用Rx编写了一个包装器:

 public class WatcherWrapper : IDisposable
{
    private readonly FileSystemWatcher _fileWatcher;
    private readonly Subject<FileSystemEventArgs> _infoSubject;
    private Subject<FileSystemEventArgs> _eventSubject;

    public WatcherWrapper(string path, string nameFilter = "*.*", NotifyFilters? notifyFilters = null)
    {
        _fileWatcher = new FileSystemWatcher(path, nameFilter);

        if (notifyFilters != null)
        {
            _fileWatcher.NotifyFilter = notifyFilters.Value;
        }

        _infoSubject = new Subject<FileSystemEventArgs>();
        _eventSubject = new Subject<FileSystemEventArgs>();

        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Changed").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);
        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Created").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);
        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Deleted").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);
        Observable.FromEventPattern<FileSystemEventArgs>(_fileWatcher, "Renamed").Select(e => e.EventArgs)
            .Subscribe(_infoSubject.OnNext);

        // this takes care of double events and still works with changing the name of the same file after a while
        _infoSubject.Buffer(TimeSpan.FromMilliseconds(20))
            .Select(x => x.GroupBy(z => z.FullPath).Select(z => z.LastOrDefault()).Subscribe(
                infos =>
                {
                    if (infos != null)
                        foreach (var info in infos)
                        {
                            {
                                _eventSubject.OnNext(info);
                            }
                        }
                });

        _fileWatcher.EnableRaisingEvents = true;
    }

    public IObservable<FileSystemEventArgs> FileEvents => _eventSubject;


    public void Dispose()
    {
        _fileWatcher?.Dispose();
        _eventSubject.Dispose();
        _infoSubject.Dispose();
    }
}

使用方法:

var watcher = new WatcherWrapper(_path, "*.info");
// all more complicated and scenario specific filtering of events can be done here    
watcher.FileEvents.Where(x => x.ChangeType != WatcherChangeTypes.Deleted).Subscribe(x => //do stuff)

2
这段代码对我有效。
        private void OnChanged(object source, FileSystemEventArgs e)
    {

        string fullFilePath = e.FullPath.ToString();
        string fullURL = buildTheUrlFromStudyXML(fullFilePath);

        System.Diagnostics.Process.Start("iexplore", fullURL);

        Timer timer = new Timer();
        ((FileSystemWatcher)source).Changed -= new FileSystemEventHandler(OnChanged);
        timer.Interval = 1000;
        timer.Elapsed += new ElapsedEventHandler(t_Elapsed);
        timer.Start();
    }

    private void t_Elapsed(object sender, ElapsedEventArgs e)
    {
        ((Timer)sender).Stop();
        theWatcher.Changed += new FileSystemEventHandler(OnChanged);
    }

2
我认为解决这个问题的最佳方案是使用响应式扩展。当你将事件转换为可观察对象时,只需添加Throttling(..)(原名Debounce(..))即可。
以下是示例代码:
        var templatesWatcher = new FileSystemWatcher(settingsSnapshot.Value.TemplatesDirectory)
        {
            NotifyFilter = NotifyFilters.LastWrite,
            IncludeSubdirectories = true
        };

        templatesWatcher.EnableRaisingEvents = true;

        Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>(
                addHandler => templatesWatcher.Changed += addHandler,
                removeHandler => templatesWatcher.Changed -= removeHandler)
            .Throttle(TimeSpan.FromSeconds(5))
            .Subscribe(args =>
            {
                _logger.LogInformation($"Template file {args.EventArgs.Name} has changed");
                //TODO do something
            });

2
尝试这个,它运行良好:

最初的回答

  private static readonly FileSystemWatcher Watcher = new FileSystemWatcher();
    static void Main(string[] args)
    {
        Console.WriteLine("Watching....");

        Watcher.Path = @"D:\Temp\Watcher";
        Watcher.Changed += OnChanged;
        Watcher.EnableRaisingEvents = true;
        Console.ReadKey();
    }

    static void OnChanged(object sender, FileSystemEventArgs e)
    {
        try
        {
            Watcher.Changed -= OnChanged;
            Watcher.EnableRaisingEvents = false;
            Console.WriteLine($"File Changed. Name: {e.Name}");
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
        finally
        {
            Watcher.Changed += OnChanged;
            Watcher.EnableRaisingEvents = true;
        }
    }

我认为在此处不需要取消订阅和重新订阅Changed事件。只需在第一次触发时将EnableRaisingEvents设置为false,进行处理,然后再将其设置为true即可。不需要使用try catch finally,这只有在Watcher为空时才会失败。 - Alexandru Dicu

2
这是另一种方法。与传播快速连续事件序列的第一个事件并抑制其余事件不同,这里除了最后一个事件外,所有事件都被抑制。我认为可以从这种方法中获益的情况更为普遍。这种策略的技术术语是去抖动
为了实现这一点,我们必须使用滑动延迟。每个传入的事件都会取消将触发先前事件的计时器,并启动新的计时器。这打开了永无止境的事件系列会永远延迟传播的可能性。为了保持简单,下面的扩展方法没有针对这种异常情况提供任何规定。
public static class FileSystemWatcherExtensions
{
    /// <summary>
    /// Subscribes to the events specified by the 'changeTypes' argument.
    /// The handler is not invoked, until an amount of time specified by the
    /// 'dueTime' argument has elapsed after the last recorded event associated
    /// with a specific file or directory (debounce behavior). The handler
    /// is invoked with the 'FileSystemEventArgs' of the last recorded event.
    /// </summary>
    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(); // Cancel the previous event
                }

                Task delayTask = Task.Delay(dueTime, cts.Token);
                await Task.WhenAny(delayTask).ConfigureAwait(false); // No throw
                if (delayTask.IsCanceled) return; // Preempted

                lock (dictionary)
                {
                    CancellationTokenSource existingCts;
                    dictionary.TryGetValue(key, out existingCts);
                    if (!ReferenceEquals(existingCts, cts)) return; // Preempted
                    dictionary.Remove(key); // Clean up before invoking the handler
                }
            }

            // Invoke the handler the same way it's done in the .NET source code
            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));

这行代码将订阅两个事件,CreatedChanged。因此,它大致相当于以下代码:

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 源代码中复制粘贴。


1
我在这里做了一个类似的概念 https://dev59.com/AnI-5IYBdhLWcg3wm5zN#75678580。它还有一个处理异常情况的版本,尽管对于日志文件来说可能是正常的。 - eglasius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接