Использование FileSystemWatcher с несколькими файлами

Я хочу использовать FileSystemWatcher для отслеживания перемещаемых файлов в каталоге и его подкаталогах. И затем я хочу запустить некоторый код, когда все файлы были перемещены. Но я не знаю как. Мой код как есть будет срабатывать каждый раз при перемещении файла, и если пользователь перемещает несколько файлов одновременно, я хочу, чтобы он срабатывал только один раз для всех файлов. Итак, в основном я хочу создать список, и как только перемещение всех файлов будет выполнено, я хочу что-то сделать с этим списком...

Вот код:

class Monitor
{
    private List<string> _filePaths;  
    public void CreateWatcher(string path)
    {
        FileSystemWatcher watcher = new FileSystemWatcher();

        watcher.Filter = "*.*";

        watcher.Created += new
        FileSystemEventHandler(watcher_FileCreated);

        watcher.Path = path;
        watcher.IncludeSubdirectories = true;

        watcher.EnableRaisingEvents = true;
    }

    void watcher_FileCreated(object sender, FileSystemEventArgs e)
    {
        _filePaths.Add(e.FullPath);
        Console.WriteLine("Files have been created or moved!");
    }

}

ОБНОВЛЕНИЕ: попытка использовать код Криса, но он не работает (см. мой комментарий к ответу Криса):

class Monitor
    {
        private List<string> _filePaths;
        private Timer _notificationTimer;
        private FileSystemWatcher _fsw;
        public Monitor(string path)
        {
            _notificationTimer = new Timer();
            _notificationTimer.Elapsed += notificationTimer_Elapsed;
            // CooldownSeconds is the number of seconds the Timer is 'extended' each time a file is added.
            // I found it convenient to put this value in an app config file.
            int CooldownSeconds = 1;
            _notificationTimer.Interval = CooldownSeconds * 1000;

            _fsw = new FileSystemWatcher();
            _fsw.Path = path;
            _fsw.IncludeSubdirectories = true;
            _fsw.EnableRaisingEvents = true;

            // Set up the particulars of your FileSystemWatcher.
            _fsw.Created += fsw_Created;
        }

        private void notificationTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            //
            // Do what you want to do with your List of files.
            //
            Console.Write("Done");
            // Stop the timer and wait for the next batch of files.            
            _notificationTimer.Stop();
            // Clear your file List.
            _filePaths = new List<string>();
        }


        // Fires when a file is created.
        private void fsw_Created(object sender, FileSystemEventArgs e)
        {
            // Add to our List of files.
            _filePaths.Add(e.Name);

            // 'Reset' timer.
            _notificationTimer.Stop();
            _notificationTimer.Start();
        }


    }

ОБНОВЛЕНИЕ 2:

Попробовал это в соответствии с ответом Андерса:

public class FileListEventArgs : EventArgs
{
    public List<string> FileList { get; set; }
}

public class Monitor
{
    private List<string> filePaths;
    private ReaderWriterLockSlim rwlock;
    private Timer processTimer;
    public event EventHandler FileListCreated;


    public void OnFileListCreated(FileListEventArgs e)
    {
        if (FileListCreated != null)
            FileListCreated(this, e);
    }

    public Monitor(string path)
    {
        filePaths = new List<string>();

        rwlock = new ReaderWriterLockSlim();

        FileSystemWatcher watcher = new FileSystemWatcher();
        watcher.Filter = "*.*";
        watcher.Created += watcher_FileCreated;

        watcher.Path = path;
        watcher.IncludeSubdirectories = true;
        watcher.EnableRaisingEvents = true;
    }

    private void ProcessQueue()
    {
        List<string> list = new List<string>();
        try
        {
            Console.WriteLine("Processing queue, " + filePaths.Count + " files created:");
            rwlock.EnterReadLock();

        }
        finally
        {
            if (processTimer != null)
            {
                processTimer.Stop();
                processTimer.Dispose();
                processTimer = null;
                OnFileListCreated(new FileListEventArgs { FileList = filePaths });
                filePaths.Clear();
            }
            rwlock.ExitReadLock();
        }
    }

    void watcher_FileCreated(object sender, FileSystemEventArgs e)
    {
        try
        {
            rwlock.EnterWriteLock();
            filePaths.Add(e.FullPath);

            if (processTimer == null)
            {
                // First file, start timer.
                processTimer = new Timer(2000);
                processTimer.Elapsed += (o, ee) => ProcessQueue();
                processTimer.Start();
            }
            else
            {
                // Subsequent file, reset timer. 
                processTimer.Stop();
                processTimer.Start();
            }

        }
        finally
        {
            rwlock.ExitWriteLock();
        }
    }

Мне пришлось переместить триггер события в оператор finally, и это работает. Я не знаю, есть ли какая-то причина, по которой я не хотел бы этого делать?


person Anders    schedule 04.08.2011    source источник
comment
Будете ли вы получать события из каталога, в котором файл удаляется/добавляется, а также для перемещенных файлов? Если это так, возможно, вы могли бы просто игнорировать (или фильтровать) события файла и слушать только события каталога? Изменить: вы, вероятно, также получаете несколько событий добавления из каталога, если добавлено несколько файлов, верно? :/   -  person Anders Forsgren    schedule 04.08.2011
comment
Я не совсем уверен, что вы спрашиваете здесь. Вы хотите заполнить список в момент начала кода, а затем подождать, пока все не будет перемещено? Или вы будете пополнять список постепенно?   -  person Chris Surfleet    schedule 04.08.2011
comment
@Chris: ну, скажем, пользователь выбирает несколько файлов и перемещает их в другой подкаталог. В этом случае событие будет запускаться для каждого файла (которое будет запускать как событие удаления, так и событие создания, и я использую событие создания для обнаружения перемещенных файлов). Таким образом, несмотря на то, что каждый из файлов, которые пользователь перемещает за одну операцию, вызовет событие, я хочу обработать событие только после того, как все файлы будут перемещены.   -  person Anders    schedule 04.08.2011
comment
@Anders: ну, я не уверен, что полностью понимаю ваш вопрос, я новичок в FileSystemWatcher, но да, я предполагаю, что вы говорите правильно, что в обоих должно быть несколько триггеров, поэтому я не уверен, как это решить...   -  person Anders    schedule 04.08.2011
comment
Ах хорошо. Похоже, ответ Джея - правильный путь. В любом случае, я всегда находил файловую систему немного ненадежной!   -  person Chris Surfleet    schedule 04.08.2011
comment
@ Крис, я согласен! Я всегда думал, что с FSW должно быть легче работать, чем это есть на самом деле. Я нашел эту статью (и ее вторую часть) в CodeProject полезной.   -  person Jay Riggs    schedule 04.08.2011


Ответы (4)


Как говорит Джей: таймер, вероятно, единственный способ «группировать» события. Блокировка может быть излишней, но мне не нравится идея изменения коллекции в многопоточной ситуации (я думаю, что события из fswatcher вызываются в потоках из пула).

  public class Monitor : IDisposable
  {
     private List<string> filePaths;
     private ReaderWriterLockSlim rwlock;
     private Timer processTimer;
     private string watchedPath;
     private FileSystemWatcher watcher;

     public Monitor(string watchedPath)
     {
        filePaths = new List<string>();

        rwlock = new ReaderWriterLockSlim();

        this.watchedPath = watchedPath;
        InitFileSystemWatcher();
     }

     private void InitFileSystemWatcher()
     {
        watcher = new FileSystemWatcher();
        watcher.Filter = "*.*";
        watcher.Created += Watcher_FileCreated;
        watcher.Error += Watcher_Error;
        watcher.Path = watchedPath;
        watcher.IncludeSubdirectories = true;
        watcher.EnableRaisingEvents = true;
     }

     private void Watcher_Error(object sender, ErrorEventArgs e)
     {
        // Watcher crashed. Re-init.
        InitFileSystemWatcher();
     }

     private void Watcher_FileCreated(object sender, FileSystemEventArgs e)
     {
        try
        {
           rwlock.EnterWriteLock();
           filePaths.Add(e.FullPath);

           if (processTimer == null)
           {
              // First file, start timer.
              processTimer = new Timer(2000);
              processTimer.Elapsed += ProcessQueue;
              processTimer.Start();
           }
           else
           {
              // Subsequent file, reset timer.
              processTimer.Stop();
              processTimer.Start();
           }

        }
        finally
        {
           rwlock.ExitWriteLock();
        }
     }

     private void ProcessQueue(object sender, ElapsedEventArgs args)
     {
        try
        {
           Console.WriteLine("Processing queue, " + filePaths.Count + " files created:");
           rwlock.EnterReadLock();
           foreach (string filePath in filePaths)
           {
              Console.WriteLine(filePath);
           }
           filePaths.Clear();
        }
        finally
        {
           if (processTimer != null)
           {
              processTimer.Stop();
              processTimer.Dispose();
              processTimer = null;
           }
           rwlock.ExitReadLock();
        }
     }

     protected virtual void Dispose(bool disposing)
     {
        if (disposing)
        {
           if (rwlock != null)
           {
              rwlock.Dispose();
              rwlock = null;
           }
           if (watcher != null)
           {
              watcher.EnableRaisingEvents = false;
              watcher.Dispose();
              watcher = null;
           }
        }
     }

     public void Dispose()
     {
        Dispose(true);
        GC.SuppressFinalize(this);
     }

  }     

Не забудьте установить размер буфера в вашем fswatcher И реализовать «воскрешение» fswatcher, если он получит ошибку (т. е. привязать событие ошибки к методу, который воссоздает наблюдатель).

Изменить: обратите внимание, что таймер в этом примере - это System.Timers.Timer, а не System.Threading.Timer.

Изменить: теперь содержит обработку ошибок для наблюдателя, логику удаления.

person Anders Forsgren    schedule 04.08.2011
comment
Хорошо, спасибо, похоже, это почти работает, но я не уверен, что происходит со всеми блокировками чтения и прочим. Он попадает в ProcessQueue в первый раз со всеми файлами, перечисленными в списке путей к файлам. Но затем он начинает входить в какой-то странный цикл, прыгая туда-сюда по коду. Что случилось? Опять же, кажется, что у него есть правильная информация в первый раз, когда он туда попадает, но потом он начинает ходить туда-сюда... Не понимаю, почему... - person Anders; 04.08.2011
comment
Я обновил образец кода (пропустил запуск и остановку) и протестировал его. Кажется, теперь работает нормально. Блокировка просто существует, поэтому файлы не добавляются в очередь во время обработки очереди. События fswatcher, происходящие во время обработки последнего пакета, просто ждут и добавляются к следующему пакету. - person Anders Forsgren; 04.08.2011
comment
Спасибо, теперь вроде работает нормально! Всего пара вопросов, если вы не возражаете: во-первых, как мне пересоздать наблюдателя в инструкции try? Где? И что здесь происходит: processTimer.Elapsed += (o, ee) => ProcessQueue(); Спасибо! - person Anders; 04.08.2011
comment
Я заменил сбивающий с толку (o,ee) на нормальный (эквивалентный) делегат. Я добавил код для повторной инициализации наблюдателя после сбоя и некоторый код для правильного удаления наблюдателя. - person Anders Forsgren; 05.08.2011
comment
Ок, отлично. Еще одна вещь: когда я пытаюсь вызвать событие после того, как список завершен, чтобы я мог вернуть его в winform, кажется, что ProcessQueue вызывается несколько раз (по одному для каждого файла?), тогда как если у меня нет событие, вызванное там (только foreach в вашем примере), то, похоже, оно не вызывается более одного раза... Вот что я заменил foreach: var fileListEventArgs = new FileListEventArgs {FileList = filePaths}; OnFileListCreated(fileListEventArgs); Есть идеи, почему? - person Anders; 05.08.2011
comment
Исправление, дело было не в количестве файлов, кажется, что оно вызывалось несколько раз больше... Таким образом, пользовательское событие запускается много раз, а не один раз... - person Anders; 05.08.2011
comment
Без понятия, я не могу это воспроизвести. Я получаю один вызов обработчика событий, если добавляю событие, как вы описываете. Дважды проверьте, не подписались ли вы на событие более одного раза. Если вы подпишетесь на FileListCreated дважды, вы каждый раз будете вызывать обработчик событий дважды. - person Anders Forsgren; 05.08.2011
comment
Нет, на самом деле это похоже на бесконечный цикл, мне нужно остановить отладчик, чтобы остановить вызовы... Но я заставил его работать, если я переместил очистку списка и триггер события в оператор finally. Это неправильно по какой-то причине? В любом случае вроде нормально работает! Смотрите мое обновление. - person Anders; 05.08.2011
comment
Решил, что это ответ сейчас. У меня есть дополнительный вопрос, если вы думаете, что можете помочь и с этим: stackoverflow.com/questions/6950071/ - person Anders; 05.08.2011

Мне пришлось сделать то же самое. Используйте System.Timers.Timer в своем классе Monitor и закодируйте его событие Elapsed для обработки списка файлов и очистки списка. Когда первый элемент будет добавлен в ваш список файлов через события FSW, запустите таймер. Когда последующие элементы добавляются в список, «сбросьте» таймер, остановив и перезапустив его.

Что-то вроде этого:

class Monitor
{
    FileSystemWatcher _fsw;
    Timer _notificationTimer;
    List<string> _filePaths = new List<string>();

    public Monitor() {
        _notificationTimer = new Timer();
        _notificationTimer.Elapsed += notificationTimer_Elapsed;
        // CooldownSeconds is the number of seconds the Timer is 'extended' each time a file is added.
        // I found it convenient to put this value in an app config file.
        _notificationTimer.Interval = CooldownSeconds * 1000;

        _fsw = new FileSystemWatcher();
        // Set up the particulars of your FileSystemWatcher.
        _fsw.Created += fsw_Created;
    }

    private void notificationTimer_Elapsed(object sender, ElapsedEventArgs e) {
        //
        // Do what you want to do with your List of files.
        //

        // Stop the timer and wait for the next batch of files.            
        _notificationTimer.Stop();
        // Clear your file List.
        _filePaths = new List<string>();
    }


    // Fires when a file is created.
    private void fsw_Created(object sender, FileSystemEventArgs e) {
        // Add to our List of files.
        _filePaths.Add(e.Name);

        // 'Reset' timer.
        _notificationTimer.Stop();
        _notificationTimer.Start();
    }
}
person Jay Riggs    schedule 04.08.2011
comment
Джей, спасибо, но я не уверен. Я могу вас неправильно понять, но, возможно, я недостаточно объяснил ситуацию. Я не понимаю, как этот код когда-либо будет вызываться. Потому что происходит то, что приложение будет запущено только один раз, а затем будет отслеживать все, что происходит в каталоге. Таким образом, единственное, что может привести к чему-либо, — это запуск события Created. Я не могу запустить приложение и вручную вызвать метод, запускающий таймер. Это будет означать, что код будет работать только в течение первых нескольких секунд после запуска приложения? Или я вас неправильно понимаю? - person Anders; 04.08.2011
comment
@Anders Таймер остановлен по умолчанию и запускается, когда срабатывает событие FSW Created. Когда срабатывает событие Timer Elapsed, таймер останавливается сам. Я добавлю, что мой код постоянно работает в службе Windows. Когда служба запускается, приложение запускается и продолжает работать, пока служба не остановится. - person Jay Riggs; 04.08.2011
comment
Хорошо, я, должно быть, делаю что-то не так, потому что я попробовал это, немного изменив для работы с остальной частью моего кода, но это не работает. Если я перемещаю один файл, код срабатывает. Но только один раз. В следующий раз, когда я перемещаю файл, код не срабатывает. И когда я пытаюсь переместить несколько, он никогда не достигает прошедшего события. Смотрите обновление! - person Anders; 04.08.2011

Rx — дроссельная заслонка — облегчает эту работу. Вот тестируемое, многоразовое решение.

public class Files
{
     public static FileSystemWatcher WatchForChanges(string path, string filter, Action triggeredAction)
            {
                var monitor = new FileSystemWatcher(path, filter);

                //monitor.NotifyFilter = NotifyFilters.FileName;
                monitor.Changed += (o, e) => triggeredAction.Invoke();
                monitor.Created += (o, e) => triggeredAction.Invoke();
                monitor.Renamed += (o, e) => triggeredAction.Invoke();
                monitor.EnableRaisingEvents = true;

                return monitor;
            }
}

Позволяет объединить события в единую последовательность

  public IObservable<Unit> OnUpdate(string path, string pattern)
        {
            return Observable.Create<Unit>(o =>
            {
                var watcher = Files.WatchForChanges(path, pattern, () => o.OnNext(new Unit()));

                return watcher;
            });
        }

затем, наконец, использование

OnUpdate(path, "*.*").Throttle(Timespan.FromSeconds(10)).Subscribe(this, _ => DoWork())

Вы, очевидно, можете играть со скоростью дроссельной заслонки в соответствии с вашими потребностями.

person Weq    schedule 21.10.2014

Кроме того, установите размер буфера больше, чем по умолчанию, чтобы избежать переполнения буфера. Это происходит, когда в исходный каталог попадает более 25 файлов (в моем тесте). Если отброшено 200 файлов, обработчик событий вызывается только для нескольких файлов, а не для всех.

_watcher.InternalBufferSize = 65536; //Максимальный размер буфера

person HirenP    schedule 31.05.2017