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() четыре раза. Любая идея почему? Тип изменения - "4" каждый раз.

Спасибо.

Ответ 1

В разделе "Устранение неполадок компонентов FileSystemWatcher" документации VS.NET...

Несколько созданных событий, созданных для одиночного действия

В некоторых случаях вы можете заметить, что одно событие создания генерирует несколько созданных событий, которые обрабатываются вашим компонентом. Например, если вы используете компонент FileSystemWatcher для отслеживания создания новых файлов в каталоге, а затем протестируйте его, используя Notepad для создания файла, вы можете увидеть два созданных события, даже если был создан только один файл. Это связано с тем, что в процессе записи Notepad выполняет несколько действий файловой системы. Блокнот записывает на диск партиями, которые создают содержимое файла, а затем атрибуты файла. Другие приложения могут работать одинаково. Поскольку FileSystemWatcher контролирует действия операционной системы, все события, которые срабатывают эти приложения, будут подняты.

Примечание. Блокнот также может вызывать другие интересные поколения событий. Например, если вы используете ChangeEventFilter, чтобы указать, что вы хотите смотреть только за изменениями атрибутов, а затем вы записываете файл в каталог, который вы просматриваете с помощью Блокнота, вы поднимете событие. Это связано с тем, что Notepad обновляет атрибут Archived для файла во время этой операции.

Ответ 2

Некоторое время назад у меня была та же проблема.

После некоторого поиска через Интернет оказалось, что я не единственный, у кого есть эта проблема.:) Итак, возможно, это недостаток в FileSystemWatcher...

Я решил это, отслеживая последний раз, когда обработчик событий был поднят. Если он был поднят меньше, чем xxx msec назад, я возвращаюсь из своего обработчика событий. Если кто-то знает исправление, более элегантное; PLZ дайте мне знать.:)

Вот как я работал над этим:

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

Мое решение этой проблемы немного похоже на Erics, за исключением того, что вместо запуска нового потока я использую System.Windows.Forms.Timer. Идея заключается в том, что я обрабатываю событие изменения только тогда, когда x ms прошел без каких-либо изменений в файлах. Обратите внимание, что все происходит в потоке графического интерфейса, поэтому нет проблем с потоками. Я использую 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

наблюдатель изменяет обработчик событий, запускает три события... создает, удаляет, изменяет. Только когда вы переименуете файл, будет onrenamed event fire. Вероятно, поэтому вы получаете 4 предупреждения. Также большинство программ запускает несколько операций над файлом перед его закрытием. Каждое событие считается изменением, поэтому событие on_changed запускается каждый раз.

Ответ 5

Предполагая, что путь один и тот же каждый раз, возможно ли, что программа, которую вы используете для сохранения файла, фактически выполняет сохранение в кусках? Или вы создаете несколько экземпляров Blah?


Изменить: У вас есть антивирусное программное обеспечение для автоматической защиты? Это может касаться файла в процессе.

Из Документация MSDN:

Общие действия файловой системы могут вызывают более одного события. Для Например, когда файл перемещается из одного каталог другому, несколько OnChanged и некоторые OnCreated и События OnDeleted могут быть подняты. Перемещение файла - сложная операция который состоит из нескольких простых операций, поэтому повышение количества Мероприятия. Аналогично, некоторые приложения (например, антивирусное программное обеспечение) может вызвать дополнительную файловую систему события, обнаруженные FileSystemWatcher.


Изменить: Или, может быть, что-то связано с тем, как Windows сохраняет файл. Вы можете получать более одного события из разных изменений. (Один для размера, один для последней метки времени записи, один для последней метки времени доступа и еще один для... что-то еще.) Попробуйте установить свойство FileSystemWatcher NotifyFilter в один тип изменений и посмотрите, вы продолжаете получать несколько событий.

Ответ 6

Есть еще одна возможность, которую вы делаете ошибкой:) Возможно, вы создаете экземпляр и завершаете свой класс "Blah", прежде чем использовать его для целей отслеживания файлов, и забываете реализовать RemoveHandler посредством Dispose/или любого связанного метода срыва. (?)

Ответ 8

Я нашел эту страницу для той же проблемы. И из того, как это выглядит, даже если вы добавляете логику для условного обработки нескольких событий, любой код, который должен быть обработан, будет прерван/прерван, когда произойдет следующее (дублирующееся) событие, что приведет к нежелательному поведению. Я думаю, что путь вокруг этого будет каким-то образом реализовать обработчик событий в другом потоке... надеюсь, что это имеет смысл.

Приветствия,

Нико

Ответ 9

Вот доказательство концепции того, как я справляюсь с этим.

Чтобы протестировать, создайте новое приложение 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

Ответ 10

Я сделал простой класс, который отлично подходит для меня. Это может быть полезно для кого-то другого.

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

Ответ 11

Это была безумная причуда API FindFirstChangeNotification() Win32 с первого дня (начиная с 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, но вы получаете идею.

Ответ 12

Независимый от платформы трюк:

// Class level variable
bool m_FileSystemWatcherIsMessy = true;

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

Ответ 13

Если вам нужно отображать события изменений, когда они происходят в форме, вам нужно использовать потоки. Решение Eric является лучшим в этом отношении, поскольку его можно легко использовать с формой или без нее, что делает решение наиболее гибким. Он также отлично справляется с несколькими повторяющимися событиями и гарантирует, что он только ест повторяющиеся события только в том случае, если он относится к ОДНОМУ ФАЙЛУ. В принятом решении, если два файла будут изменены примерно в одно и то же время, одно из их событий может быть неправильно проигнорировано.

Ответ 14

Решение Фредерика - лучшая вещь, с которой я столкнулся. Однако я обнаружил, что 500 миллисекунд будут слишком медленными. В моем приложении пользователь может выполнить два действия в файле легко в течение 0,5 секунд, поэтому я опустил его до 100 и до сих пор отлично работает. Его С# был немного фубаром (он не конвертировался), поэтому здесь версия VB:

Public LastTimeFileWatcherEventRaised As DateTime

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

LastTimeFileWatcherEventRaised = DateTime.Now

.. handle event here