问题
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.
回答1:
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.
回答2:
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
回答3:
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();
}
回答4:
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.
回答5:
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.
回答6:
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. (?)
回答7:
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
回答8:
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
回答9:
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");
回答10:
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.
回答11:
Platform independent trick :
// Class level variable
bool m_FileSystemWatcherIsMessy = true;
// inside call back
if (m_FileSystemWatcherIsMessy) {
m_FileSystemWatcherIsMessy = false;
return;
} else {
m_FileSystemWatcherIsMessy = true;
}
回答12:
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.
回答13:
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
回答14:
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
回答15:
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
来源:https://stackoverflow.com/questions/449993/vb-net-filesystemwatcher-multiple-change-events