11

I have the following code:


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

When I run it and save changes to a file on my C drive, the code works great, except it executes the watcher_Changed() method four times. Any idea why? The changeType is "4" every time.

Thanks.

John Saunders
  • 160,644
  • 26
  • 247
  • 397
John Kurlak
  • 6,594
  • 7
  • 43
  • 59

15 Answers15

16

From the "Troubleshooting FileSystemWatcher Components" section of the VS.NET documentation...

Multiple Created Events Generated for a Single Action

You may notice in certain situations that a single creation event generates multiple Created events that are handled by your component. For example, if you use a FileSystemWatcher component to monitor the creation of new files in a directory, and then test it by using Notepad to create a file, you may see two Created events generated even though only a single file was created. This is because Notepad performs multiple file system actions during the writing process. Notepad writes to the disk in batches that create the content of the file and then the file attributes. Other applications may perform in the same manner. Because FileSystemWatcher monitors the operating system activities, all events that these applications fire will be picked up.

Note: Notepad may also cause other interesting event generations. For example, if you use the ChangeEventFilter to specify that you want to watch only for attribute changes, and then you write to a file in the directory you are watching using Notepad, you will raise an event . This is because Notepad updates the Archived attribute for the file during this operation.

Community
  • 1
  • 1
John Kurlak
  • 6,594
  • 7
  • 43
  • 59
  • 1
    Interesting to know. So I suppose some combination of my suppositions was close to the right answer? I wouldn't have imagined Notepad would do anything more than dumping the text to a file, but I guess it makes sense. – lc. Jan 16 '09 at 10:43
15

A while ago, I've experience the same problem.

After some searching thtrough the web, it appeared that I was not the only one having this issue. :) So, perhaps it is a flaw in the FileSystemWatcher ...

I've solved it by keeping track of the last time the eventhandler has been raised. If it has been raised less then xxx msec ago, I return from my eventhandler. If anyone knows a fix that is more elegant; plz let me know. :)

This is how I've worked around it:

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
Frederik Gheysels
  • 56,135
  • 11
  • 101
  • 154
  • Haha... you think that's bad? I automatically issues a halt on all events after one was received... until a key was pressed. I think I'll use your method. Hahahah – John Kurlak Jan 16 '09 at 12:07
3

Frederik's solution is by the far the best thing I've come across. However I found 500 milliseconds to be far too slow. In my app a user is able to perform two actions on a file easily within .5 seconds so I lowered it to 100 and so far it's working out fine. His C# was a little fubar (it wouldn't convert) so here's the VB version:

Public LastTimeFileWatcherEventRaised As DateTime

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

LastTimeFileWatcherEventRaised = DateTime.Now

.. handle event here
Justin Emlay
  • 904
  • 9
  • 10
2

My solution to this problem is a bit like Erics except I use a System.Windows.Forms.Timer in stead of starting a new thread. The idea is that I handle the change event only when x ms have passed without any file changed events. Note that everything takes place on the GUI thread so there are no threading issues. I use 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();
    }
LOAS
  • 7,161
  • 2
  • 28
  • 25
2

the watcher changed event handler will fire on 3 events... create, delete,change. Only when you rename a file will the onrenamed event fire. That is probably why you are getting 4 alerts. Also most programs run multiple operations on a file before closing it. Every event is considered a change and so the on_changed event is fired every time.

sam
  • 320
  • 3
  • 14
1

I found this page for the same problem. And from what it looks like, even if you add logic to conditionally process multiple events, any code that is supposed to be processed will be interrupted/aborted when a subsequent (duplicate) event occurs thereby causing an undesired behavior. I think a way around this would be to implement an event handler on a different thread somehow... hope this makes sense.

Cheers,

Nico

1

Here is a proof of concept for how I handle this.

To test, create a new Windows Forms application. On the form, add a multiline text box named "tbMonitor". Right-click on the form and go to View Code. Replace that code with the code I've included below. Note that I set the wait time to a really high number so you can play around with it a bit. In production, you'll probably want to make this number much lower, probably around 10 or 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
Eric Siron
  • 36
  • 3
1

I made a simple class that works fine for me. It may be useful for someone else.

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);
        }
    }
}

Can be used like that:

FileWatcher FileWatcher1 = new FileWatcher();
FileWatcher1.FileChanged += new EventHandler(FileWatcher1_FileChanged);
FileWatcher1.Start("c:\test.txt");
Fedor
  • 43,261
  • 10
  • 79
  • 89
1

This has been a maddening quirk of the FindFirstChangeNotification() Win32 API since day 1 (since Windows 3.x), and it looks like FileSystemWatcher simply wraps that API. The timer approach (presented above) is the common workaround.

I usually create a class that wraps FileSystemWatcher and does the multiple-change-call filtering. A bit of extra work to write, but it pays off in reuse.

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
        }
    }
}

You can then use FileChangeMonitor pretty much like you would FileSystemWatcher:

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

Of course, the code above only handles the Changed event and NotifyFilters.LastWrite, but you get the idea.

dlchambers
  • 3,511
  • 3
  • 29
  • 34
1

Platform independent trick :

// Class level variable
bool m_FileSystemWatcherIsMessy = true;

// inside call back
if (m_FileSystemWatcherIsMessy) {
    m_FileSystemWatcherIsMessy = false;
    return;
} else {
    m_FileSystemWatcherIsMessy = true;
}
Xaqron
  • 29,931
  • 42
  • 140
  • 205
1

Assuming the path is the same every time, is it possible the program you are using to save the file is actually doing the save in pieces? Or do you have more than one Blah instantiated?


Edit: Do you have any antivirus auto-protect software running? Those might be touching the file in the process.

From the MSDN Documentation:

Common file system operations might raise more than one event. For example, when a file is moved from one directory to another, several OnChanged and some OnCreated and OnDeleted events might be raised. Moving a file is a complex operation that consists of multiple simple operations, therefore raising multiple events. Likewise, some applications (for example, antivirus software) might cause additional file system events that are detected by FileSystemWatcher.


Edit: Or maybe there's something to do with how windows is saving the file. You might be getting more than one event from different changes. (One for the size, one for the last write timestamp, one for the last access timestamp, and one more for...something else.) Try setting the FileSystemWatcher's NotifyFilter property to a single type of change and see if you continue to get multiple events.

lc.
  • 113,939
  • 20
  • 158
  • 187
  • I've tried two different programs with the same result. I only have one Blah instantiated, as far as I am aware. – John Kurlak Jan 16 '09 at 10:23
  • Nope. I don't use any antivirus/autoprotect software. – John Kurlak Jan 16 '09 at 10:31
  • I had read about the copying files and how it raises multiple events. I know that can't be it since the file is only a few characters big and because I'm just overwriting it, not copying it. – John Kurlak Jan 16 '09 at 10:33
  • Right, I was more referring to the last sentence in the docs - something else might be playing with the file after it's saved. – lc. Jan 16 '09 at 10:35
1

There is another possibility, which you are making mistake :) Maybe you are instantiate and terminate your "Blah" class before using it for filewatching purpose, and forgetting to implement RemoveHandler by Dispose/or any related teardown method. (?)

user53378
  • 160
  • 4
  • I would assume as Blah goes out of scope and gets garbage collected that watcher would as well. And if he's tried the same in two different programs, I would imagine one is a simple "test" program where he'd make sure only one Blah got instantiated anyway. – lc. Jan 16 '09 at 10:46
1

If you need to display the change events while they are happening on a form, then you need to use threading. Eric's solution is the best in this regard since it can be easily used with or without a form making the solution most flexible. It also handles the multiple duplicate events nicely and makes sure that it only eats duplicate events only if it is for THE SAME FILE. In the accepted solution, if two files are changed at near the same time, one of their events could be incorrectly ignored.

John
  • 11
  • 1
1

I wrote some code that solves this problem and other neat features of FileSystemWatcher. Its posted in my blog at: http://precisionsoftware.blogspot.com/2009/05/filesystemwatcher-done-right.html

Eric
  • 2,029
  • 2
  • 26
  • 36
  • This question is tagged with "vb.net," the question is posed in vb, and all of the answers except this one are in vb. I'm sure this 'file system watcher done right' is well and good, but it's not in vb.net. – Jeremy Sep 06 '10 at 08:53
  • 1
    @Jeremy C# can be easily converted into VB.NET (with the possible exception of LINQ queries). Google "C# to VB.NET". Despite the fact that my reason for visiting this question was for a C# project I'm working on, I personally found it very useful despite being in a different language. – Chad Levy Sep 28 '10 at 20:16
  • Its a good post but its for WPF. I got lost at the Dispatcher lines. I'm trying to get this working for a Class Library that'll be used in a Windows Form application. @Eric, could you post a converted code for your class at your blog please? – DoomerDGR8 Dec 12 '11 at 13:16
0

I've inspired my solution with LAOS example here above. I implement a watcher for the folder, and every times it is triggered, I stop a timer and start it again to reset it. I fire my actions only when the timer ends, which prevent watcher from triggering action twice for a file creation. And as requested, it is in 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
Yannick
  • 41
  • 3