views:

4357

answers:

11

Hi. 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.

A: 

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
I've tried two different programs with the same result. I only have one Blah instantiated, as far as I am aware.
John
Nope. I don't use any antivirus/autoprotect software.
John
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
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
+5  A: 

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

--- begin quote --- 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. --- end quote ---

John
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
A: 

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. (?)

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
+5  A: 

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
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
A: 

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
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
@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.
Paperjam
A: 

Hi there. 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

A: 

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
+1  A: 

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
+1  A: 

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
A: 

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.

A: 

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