VB.NET FileSystemWatcher多个更改事件

时间:2009-01-16 10:19:11

标签: .net vb.net filesystemwatcher

我有以下代码:


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”。

感谢。

15 个答案:

答案 0 :(得分:16)

来自VS.NET文档的“疑难解答FileSystemWatcher组件”部分...

  

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

     

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

     

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

答案 1 :(得分:14)

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

经过一些搜索网络后,似乎我不是唯一一个有这个问题的人。 :) 所以,也许这是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

答案 2 :(得分:2)

我对这个问题的解决方案有点像Erics,除了我使用System.Windows.Forms.Timer而不是启动一个新线程。我的想法是,只有在没有任何文件更改事件的情况下传递x ms时才处理更改事件。请注意,一切都发生在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();
    }

答案 3 :(得分:2)

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

答案 4 :(得分:1)

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


编辑: 你有运行的防病毒自动保护软件吗?那些可能正在触摸过程中的文件。

来自MSDN Documentation

  

通用文件系统操作可能   举办不止一场比赛。对于   例如,从一个文件移动文件时   目录到另一个,几个   OnChanged和一些OnCreated和   可能会引发OnDeleted事件。   移动文件是一项复杂的操作   它由多个简单组成   操作,因此提高了多个   事件。同样,一些应用程序   (例如,防病毒软件)   可能会导致其他文件系统   被检测到的事件   FileSystemWatcher的。


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

答案 5 :(得分:1)

还有另一种可能性,你犯了错误:) 也许你实例化并终止你的“Blah”类,然后再将其用于文件监视目的,并忘记通过Dispose /或任何相关的拆解方法实现RemoveHandler。 (?)

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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

干杯,

尼科

答案 8 :(得分: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

答案 9 :(得分: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");

答案 10 :(得分:1)

自从第1天(从Windows 3.x开始)以来,这一直是FindFirstChangeNotification()Win32 API的一个疯狂的怪癖,看起来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,但你明白了。

答案 11 :(得分:1)

独立于平台的技巧:

// Class level variable
bool m_FileSystemWatcherIsMessy = true;

// inside call back
if (m_FileSystemWatcherIsMessy) {
    m_FileSystemWatcherIsMessy = false;
    return;
} else {
    m_FileSystemWatcherIsMessy = true;
}

答案 12 :(得分:1)

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

答案 13 :(得分:1)

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

Public LastTimeFileWatcherEventRaised As DateTime

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

LastTimeFileWatcherEventRaised = DateTime.Now

.. handle event here

答案 14 :(得分: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