11

我有以下代码:


Imports System.IO

Public Class Blah
    Public Sub New()
        InitializeComponent()

        Dim watcher As New FileSystemWatcher("C:\")
        watcher.EnableRaisingEvents = True

        AddHandler watcher.Changed, AddressOf watcher_Changed
    End Sub

    Private Sub watcher_Changed(ByVal sender As Object, ByVal e As FileSystemEventArgs)
        MsgBox(e.FullPath)
    End Sub
End Class

当我运行它并将更改保存到我的 C 驱动器上的文件时,代码运行良好,除了它执行 watcher_Changed() 方法四次。知道为什么吗?changeType 每次都是“4”。

谢谢。

4

15 回答 15

16

从 VS.NET 文档的“故障排除 FileSystemWatcher 组件”部分...

为单个操作生成多个创建事件

您可能会注意到在某些情况下,单个创建事件会生成多个由您的组件处理的 Created 事件。例如,如果您使用 FileSystemWatcher 组件监视目录中新文件的创建,然后使用记事本创建文件进行测试,即使只创建了一个文件,您也可能会看到生成了两个 Created 事件。这是因为记事本在写入过程中执行了多个文件系统操作。记事本分批写入磁盘,创建文件内容,然后创建文件属性。其他应用程序可以以相同的方式执行。因为 FileSystemWatcher 监视操作系统活动,所以这些应用程序触发的所有事件都将被拾取。

注意:记事本也可能导致其他有趣的事件生成。例如,如果您使用 ChangeEventFilter 指定您只想监视属性更改,然后您使用记事本写入您正在监视的目录中的文件,您将引发一个事件。这是因为记事本会在此操作期间更新文件的存档属性。

于 2009-01-16T10:37:47.380 回答
15

不久前,我遇到了同样的问题。

在网上搜索了一番后,看来我不是唯一一个遇到这个问题的人。:) 所以,也许这是 FileSystemWatcher 的一个缺陷......

我通过跟踪最后一次引发事件处理程序来解决它。如果它在 xxx 毫秒前被提升,我会从我的事件处理程序返回。如果有人知道更优雅的修复程序;请让我知道。:)

这就是我解决它的方法:

if( e.ChangeType == WatcherChangeTypes.Changed )
{

    // There is a nasty bug in the FileSystemWatch which causes the 
    // events of the FileSystemWatcher to be called twice.
    // There are a lot of resources about this to be found on the Internet,
    // but there are no real solutions.
    // Therefore, this workaround is necessary: 
    // If the last time that the event has been raised is only a few msec away, 
    // we ignore it.
    if( DateTime.Now.Subtract (_lastTimeFileWatcherEventRaised).TotalMilliseconds < 500 )
    {
        return;
    }


    _lastTimeFileWatcherEventRaised = DateTime.Now;


    .. handle event
于 2009-01-16T10:46:59.200 回答
2

我对这个问题的解决方案有点像 Erics,除了我使用 System.Windows.Forms.Timer 而不是启动新线程。这个想法是我只在 x 毫秒过去而没有任何文件更改事件的情况下处理更改事件。请注意,一切都发生在 GUI 线程上,因此不存在线程问题。我使用 x = 100。

    private Dictionary<String, FileSystemEventArgs> xmlFileChangedEvents = new Dictionary<string, FileSystemEventArgs>();
    private void debugXmlWatcher_Changed(object sender, FileSystemEventArgs e)
    {
        if (!xmlFileChangedEvents.ContainsKey(e.Name))
            xmlFileChangedEvents.Add(e.Name, e);
        xmlChangeTimer.Stop();//Reset the Forms.Timer so that it times out in 100 ms
        xmlChangeTimer.Start();
    }

    private void xmlChangeTimer_Tick(object sender, EventArgs e)
    {
        foreach (FileSystemEventArgs eventArg in xmlFileChangedEvents.Values)
        {
            //
            //Handle the file changed event here
            //
        }
        xmlFileChangedEvents.Clear();
    }
于 2009-10-20T11:53:56.443 回答
2

观察者更改的事件处理程序将触发 3 个事件...创建、删除、更改。只有当您重命名文件时才会触发 onrenamed 事件。这可能就是您收到 4 个警报的原因。此外,大多数程序在关闭文件之前会对文件运行多项操作。每个事件都被视为一次更改,因此每次都会触发 on_changed 事件。

于 2010-12-08T21:28:34.827 回答
1

假设每次路径都相同,您用来保存文件的程序是否可能实际上是分段保存?或者你有多个Blah实例化?


编辑:您是否正在运行任何防病毒自动保护软件?那些可能会在此过程中触及文件。

MSDN 文档

常见的文件系统操作可能会引发多个事件。例如,当一个文件从一个目录移动到另一个目录时,可能会引发几个 OnChanged 以及一些 OnCreated 和 OnDeleted 事件。移动文件是一个复杂的操作,由多个简单的操作组成,因此会引发多个事件。同样,某些应用程序(例如,防病毒软件)可能会导致 FileSystemWatcher 检测到的其他文件系统事件。


编辑:或者也许与windows如何保存文件有关。您可能会从不同的更改中获得不止一个事件。(一个用于大小,一个用于最后写入时间戳,一个用于最后访问时间戳,还有一个用于...其他内容。)尝试将FileSystemWatcher'NotifyFilter属性设置为单一类型的更改,看看是否继续获取多个事件。

于 2009-01-16T10:23:06.863 回答
1

还有另一种可能性,您犯了错误:) 也许您在将“Blah”类用于文件监视之前实例化并终止它,而忘记通过 Dispose/或任何相关的拆卸方法实现 RemoveHandler。(?)

于 2009-01-16T10:39:42.057 回答
1

我编写了一些代码来解决这个问题以及 FileSystemWatcher 的其他简洁功能。它发布在我的博客中: http: //precisionsoftware.blogspot.com/2009/05/filesystemwatcher-done-right.html

于 2009-05-13T21:58:54.657 回答
1

我发现这个页面有同样的问题。从它的样子来看,即使您添加逻辑来有条件地处理多个事件,任何应该处理的代码都将在发生后续(重复)事件时被中断/中止,从而导致不良行为。我认为解决这个问题的一种方法是以某种方式在不同的线程上实现一个事件处理程序......希望这是有道理的。

干杯,

尼科

于 2009-08-12T14:54:45.340 回答
1

这是我如何处理此问题的概念证明。

要进行测试,请创建一个新的 Windows 窗体应用程序。在表单上,​​添加一个名为“tbMonitor”的多行文本框。右键单击表单并转到查看代码。用我在下面包含的代码替换该代码。请注意,我将等待时间设置为一个非常高的数字,以便您可以尝试一下。在生产中,您可能希望使这个数字低得多,可能在 10 或 15 左右。

Imports System.IO
Imports System.Threading
Public Class Form1
Private Const MILLISECONDS_TO_WAIT As Integer = 1000
Private fw As FileSystemWatcher
Private Shared AccessEntries As List(Of String)
Private Delegate Sub UpdateBoxDelegate(ByVal msg As String)
Private Sub UpdateBox(ByVal msg As String)
    If tbMonitor.InvokeRequired Then
        Invoke(New UpdateBoxDelegate(AddressOf UpdateBox), New Object() {msg})
    Else
        tbMonitor.AppendText(msg + vbCrLf)
    End If
End Sub

Private Sub AccessEntryRemovalTimer(ByVal RawFileName As Object)
    UpdateBox("Sleeping to watch for " + RawFileName.ToString + " on thread ID " + Thread.CurrentThread.ManagedThreadId.ToString)
    Thread.Sleep(MILLISECONDS_TO_WAIT)
    AccessEntries.Remove(RawFileName.ToString)
    UpdateBox("Removed " + RawFileName.ToString + " in thread ID " + Thread.CurrentThread.ManagedThreadId.ToString)
End Sub

Private Sub Changed(ByVal source As Object, ByVal e As FileSystemEventArgs)
    If AccessEntries.Contains(e.Name) Then
        UpdateBox("Ignoring a " + e.ChangeType.ToString + " notification for " + e.Name + " in thread ID " + Thread.CurrentThread.ManagedThreadId.ToString)
        Return
    End If
    Dim AccessTimerThread As Thread

    AccessEntries.Add(e.Name)
    UpdateBox("Adding " + e.Name + " to the collection and starting the watch thread.")
    AccessTimerThread = New Thread(AddressOf AccessEntryRemovalTimer)
    AccessTimerThread.IsBackground = True
    AccessTimerThread.Start(e.Name)

End Sub

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    tbMonitor.ScrollBars = ScrollBars.Both
    AccessEntries = New List(Of String)
    fw = New FileSystemWatcher
    fw.Path = "C:\temp"
    fw.NotifyFilter = NotifyFilters.LastWrite Or NotifyFilters.LastAccess Or NotifyFilters.FileName
    AddHandler fw.Changed, AddressOf Changed
    AddHandler fw.Created, AddressOf Changed
    AddHandler fw.Renamed, AddressOf Changed
    fw.EnableRaisingEvents = True
End Sub

Private Sub Form1_FormClosed(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed
    fw.EnableRaisingEvents = False
    RemoveHandler fw.Changed, AddressOf Changed
    RemoveHandler fw.Created, AddressOf Changed
    RemoveHandler fw.Renamed, AddressOf Changed
    fw.Dispose()
End Sub
End Class
于 2009-09-09T19:12:54.533 回答
1

我做了一个简单的类,对我来说很好。它可能对其他人有用。

using System;
using System.IO;
using System.Timers;

namespace Demo
{
    class FileWatcher
    {
        private FileSystemWatcher watcher = new FileSystemWatcher();
        private Timer t = new Timer();

        public event EventHandler FileChanged;

        public FileWatcher()
        {
            t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed);
            t.Interval = 1000;
        }

        public void Start(String path)
        {
            watcher.Path = Path.GetDirectoryName(path);
            watcher.Filter = Path.GetFileName(path);
            watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime;
            watcher.EnableRaisingEvents = true;
            watcher.Changed += new FileSystemEventHandler(watcher_Changed);
        }

        void watcher_Changed(object sender, FileSystemEventArgs e)
        {
            if (!t.Enabled)
                t.Start();
        }

        void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            t.Stop();
            if (FileChanged != null)
                FileChanged(this, null);
        }
    }
}

可以这样使用:

FileWatcher FileWatcher1 = new FileWatcher();
FileWatcher1.FileChanged += new EventHandler(FileWatcher1_FileChanged);
FileWatcher1.Start("c:\test.txt");
于 2010-04-06T14:40:46.100 回答
1

这是 FindFirstChangeNotification() Win32 API 从第一天(从 Windows 3.x 开始)以来的一个令人抓狂的怪癖,看起来 FileSystemWatcher 只是简单地包装了该 API。计时器方法(如上所示)是常见的解决方法。

我通常创建一个包装 FileSystemWatcher 并进行多次更改调用过滤的类。编写一些额外的工作,但它在重用中得到了回报。

public class FileChangeMonitor
{
    private FileSystemWatcher _fsw;
    DateTime _lastEventTime;

    public event FileSystemEventHandler Changed;

    public FileChangeMonitor(string path, string filter)
    {
        _fsw = new FileSystemWatcher(path, filter);
        _fsw.Changed += new FileSystemEventHandler(_fsw_Changed);
        _fsw.EnableRaisingEvents = true;
        _fsw.NotifyFilter = NotifyFilters.LastWrite;
        _fsw.IncludeSubdirectories = false;
    }

    private void _fsw_Changed(object sender, FileSystemEventArgs e)
    {
        // Fix the FindFirstChangeNotification() double-call bug
        if (DateTime.Now.Subtract(_lastEventTime).TotalMilliseconds > 100)
        {
            _lastEventTime = DateTime.Now;
            if (this.Changed != null)
                this.Changed(sender, e);  // Bubble the event
        }
    }
}

然后,您可以像使用 FileSystemWatcher 一样使用 FileChangeMonitor:

FileChangeMonitor fcm = new FileChangeMonitor(path, filter);
fsm.Changed += new FileSystemEventHandler(fsm_Changed);
...

当然,上面的代码只处理 Changed 事件和 NotifyFilters.LastWrite,但你明白了。

于 2010-05-25T17:38:44.627 回答
1

平台独立技巧:

// Class level variable
bool m_FileSystemWatcherIsMessy = true;

// inside call back
if (m_FileSystemWatcherIsMessy) {
    m_FileSystemWatcherIsMessy = false;
    return;
} else {
    m_FileSystemWatcherIsMessy = true;
}
于 2010-06-09T01:41:58.983 回答
1

如果您需要在表单上发生更改事件时显示它们,那么您需要使用线程。Eric 的解决方案在这方面是最好的,因为它可以很容易地使用或不使用使解决方案最灵活的形式。它还可以很好地处理多个重复事件,并确保仅当它用于相同的文件时才吃重复事件。在公认的解决方案中,如果两个文件几乎同时更改,其中一个事件可能会被错误地忽略。

于 2011-04-30T20:00:17.607 回答
1

Frederik 的解决方案是迄今为止我遇到的最好的解决方案。但是我发现 500 毫秒太慢了。在我的应用程序中,用户可以在 0.5 秒内轻松对文件执行两个操作,因此我将其降低到 100,到目前为止一切正常。他的 C# 有点 fubar(它不会转换)所以这里是 VB 版本:

Public LastTimeFileWatcherEventRaised As DateTime

If DateTime.Now.Subtract(LastTimeFileWatcherEventRaised).TotalMilliseconds < 100 Then Return

LastTimeFileWatcherEventRaised = DateTime.Now

.. handle event here
于 2012-03-05T18:00:06.053 回答
0

我用上面的 LAOS 示例启发了我的解决方案。我为文件夹实现了一个观察器,每次触发它时,我都会停止一个计时器并再次启动它来重置它。我仅在计时器结束时触发我的操作,这会阻止观察者触发两次操作以创建文件。根据要求,它在 VB.Net 中:)

    <PermissionSet(SecurityAction.Demand, Name:="FullTrust")> Public Sub StartWatcher()

    Dim watcher As FileSystemWatcher = New FileSystemWatcher()
    watcher.Path = _MWVM.TemplatesFolder

    'Watch for changes in LastWrite times, And the renaming of files Or directories. 
    watcher.NotifyFilter = NotifyFilters.LastWrite Or NotifyFilters.FileName Or NotifyFilters.DirectoryName

    ' Only watch text files.
    watcher.Filter = "*.txt"

    'Define timer to 100 ms
    WatcherTimer.Interval = New TimeSpan(0, 0, 0, 0, 100) '100 ms

    ' Add event handlers.
    AddHandler watcher.Changed, AddressOf WatcherHandler
    AddHandler watcher.Created, AddressOf WatcherHandler
    AddHandler watcher.Deleted, AddressOf WatcherHandler
    AddHandler watcher.Renamed, AddressOf WatcherHandler

    ' Begin watching
    watcher.EnableRaisingEvents = True

End Sub

'Instantiate a timer which will prevent the 
Private WithEvents WatcherTimer As New System.Windows.Threading.DispatcherTimer
Private xmlFileChangedEvents As New Dictionary(Of String, FileSystemEventArgs)

Private Sub WatcherHandler(ByVal Sender As Object, ByVal e As FileSystemEventArgs)
    WatcherTimer.Stop() 'Reset the timer
    WatcherTimer.Start()
End Sub

Private Sub WatcherTimer_Tick(ByVal Sender As Object, ByVal e As EventArgs) Handles WatcherTimer.Tick
    WatcherTimer.Stop()
    PopulateMailTemplateList()
End Sub
于 2018-09-12T06:17:16.070 回答