У меня такой код:
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».
Спасибо.





Предполагая, что путь каждый раз один и тот же, возможно ли, что программа, которую вы используете для сохранения файла, действительно выполняет сохранение по частям? Или у вас более одного экземпляра Blah?
Редактировать: У вас работает какое-либо антивирусное программное обеспечение для автоматической защиты? Возможно, они касаются файла в процессе.
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.
Обновлено: Или, может быть, есть какое-то отношение к тому, как Windows сохраняет файл. Вы можете получить более одного события из-за разных изменений. (Один для размера, один для метки времени последней записи, один для метки времени последнего доступа и еще один для ... чего-то еще.) Попробуйте установить свойство FileSystemWatcherNotifyFilter на один тип изменения и посмотрите, продолжаете ли вы получать несколько событий.
Неа. Я не использую никаких антивирусных программ и программ для автоматической защиты.
Я читал о копировании файлов и о том, как это вызывает несколько событий. Я знаю, что этого не может быть, поскольку размер файла всего несколько символов, и потому что я просто перезаписываю его, а не копирую.
Правильно, я больше имел в виду последнее предложение в документации - что-то еще могло играть с файлом после его сохранения.
В разделе «Устранение неполадок компонентов FileSystemWatcher» документации VS.NET ...
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.
Интересно знать. Полагаю, какая-то комбинация моих предположений была близка к правильному ответу? Я бы и представить себе не мог, что Блокнот сделает что-то большее, чем выгружает текст в файл, но я думаю, это имеет смысл.
Есть еще одна возможность, в которой вы ошибаетесь :) Возможно, вы создаете экземпляр и завершаете свой класс «Blah» перед его использованием для отслеживания файлов и забываете реализовать RemoveHandler с помощью Dispose / или любого связанного метода разрыва. (?)
Я предполагаю, что, поскольку Blah выходит за рамки и собирает мусор, этот наблюдатель тоже. И если бы он попробовал то же самое в двух разных программах, я бы предположил, что одна из них представляет собой простую «тестовую» программу, в которой он все равно проверяет, был ли создан только один экземпляр Blah.
Некоторое время назад я столкнулся с той же проблемой.
После некоторого поиска в сети оказалось, что эта проблема возникла не только у меня. :) Так что, возможно, это ошибка 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
Ха-ха ... ты думаешь, что это плохо? Я автоматически останавливаю все события после того, как одно было получено ... пока не была нажата клавиша. Думаю, я воспользуюсь вашим методом. Ха-ха-ха
Я написал код, который решает эту проблему и другие полезные функции FileSystemWatcher. Он размещен в моем блоге по адресу: http://precisionsoftware.blogspot.com/2009/05/filesystemwatcher-done-right.html
Этот вопрос помечен тегом «vb.net», вопрос задан в vb, и все ответы, кроме этого, находятся в vb. Я уверен, что этот «наблюдатель файловой системы, сделанный правильно» - это хорошо, но его нет в vb.net.
@Jeremy C# можно легко преобразовать в VB.NET (за возможным исключением запросов LINQ). Погуглите "C# на VB.NET". Несмотря на то, что я обратился к этому вопросу по причине проекта C#, над которым я работаю, я лично нашел его очень полезным, несмотря на то, что я на другом языке.
Это хороший пост, но для WPF. Заблудился на диспетчерских линиях. Я пытаюсь заставить это работать для библиотеки классов, которая будет использоваться в приложении Windows Form. @ Эрик, не могли бы вы опубликовать преобразованный код для вашего класса в своем блоге?
Я нашел эту страницу по той же проблеме. И, судя по тому, как это выглядит, даже если вы добавите логику для условной обработки нескольких событий, любой код, который должен быть обработан, будет прерван / прерван при возникновении последующего (дублирующего) события, что приведет к нежелательному поведению. Я думаю, что способ обойти это было бы как-то реализовать обработчик событий в другом потоке ... надеюсь, что это имеет смысл.
Ваше здоровье,
Нико
Вот доказательство концепции того, как я с этим справляюсь.
Для проверки создайте новое приложение Windows Forms. В форме добавьте многострочное текстовое поле с именем «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
Мое решение этой проблемы немного похоже на Erics, за исключением того, что я использую System.Windows.Forms.Timer вместо запуска нового потока. Идея состоит в том, что я обрабатываю событие изменения только тогда, когда прошло x мс без каких-либо событий изменения файла. Обратите внимание, что все происходит в потоке графического интерфейса, поэтому проблем с потоками нет. Я использую 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();
}
Я сделал простой класс, который мне подходит. Это может быть полезно кому-то другому.
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");
Это сводящая с ума причуда Win32 API FindFirstChangeNotification () с первого дня (начиная с 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
}
}
}
Затем вы можете использовать FileChangeMonitor почти так же, как FileSystemWatcher:
FileChangeMonitor fcm = new FileChangeMonitor(path, filter);
fsm.Changed += new FileSystemEventHandler(fsm_Changed);
...
Конечно, приведенный выше код обрабатывает только событие Changed и NotifyFilters.LastWrite, но вы поняли идею.
Независимый от платформы трюк:
// Class level variable
bool m_FileSystemWatcherIsMessy = true;
// inside call back
if (m_FileSystemWatcherIsMessy) {
m_FileSystemWatcherIsMessy = false;
return;
} else {
m_FileSystemWatcherIsMessy = true;
}
обработчик событий, измененный наблюдателем, будет запускаться при трех событиях ... создать, удалить, изменить. Только когда вы переименуете файл, сработает переименованное событие. Вероятно, поэтому вы получаете 4 предупреждения. Также большинство программ запускают несколько операций с файлом перед его закрытием. Каждое событие считается изменением, поэтому событие on_changed запускается каждый раз.
Если вам нужно отображать события изменения, когда они происходят в форме, вам нужно использовать потоки. Решение Эрика является лучшим в этом отношении, поскольку его можно легко использовать с формой или без нее, что делает решение наиболее гибким. Он также прекрасно обрабатывает множественные повторяющиеся события и гарантирует, что он использует только повторяющиеся события, только если они предназначены для ОДНОГО ФАЙЛА. В принятом решении, если два файла изменяются почти одновременно, одно из их событий может быть неправильно проигнорировано.
Решение Фредерика, безусловно, лучшее, что я когда-либо встречал. Однако я обнаружил, что 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
Я вдохновил свое решение на приведенный выше пример 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
Я пробовал две разные программы с одинаковым результатом. Насколько мне известно, у меня есть только один экземпляр Blah.