文件和文件夾不存在的時候,FileSystemWatcher 監聽不到文件的改變?如果遞歸地監聽就可以了

  • 2020 年 2 月 10 日
  • 筆記

文件和文件夾不存在的時候,FileSystemWatcher 監聽不到文件的改變?如果遞歸地監聽就可以了

2018-12-20 02:05

當你需要監視文件或文件夾的改變的時候,使用 FileSystemWatcher 便可以完成。不過,FileSystemWatcher 對文件夾的監視要求文件夾必須存在,否則會產生錯誤「無效路徑」。

那麼,如果文件或文件夾不存在的時候可以怎麼監視文件的改變呢?更麻煩的是如果頂層很多級文件夾都不存在,怎麼能監視呢?本文將告訴你方法。

本文的代碼適用於 .NET Framework 和 .NET Core,同時不需要任何第三方依賴。


方法一:創建文件夾(在逃避問題,但也不失為一種解決思路)

如果文件夾不存在,把它創建出來就可以監視了嘛!這其實是在逃避問題。不過我把它寫出來是因為如果我不說,可能有些小夥伴原本簡單的問題就會變得複雜化。

public void Watch(string file)  {      path = Path.GetFullPath(file);      var directory = Path.GetDirectoryName(path);      var file = Path.GetFileName(path);        // 如果文件夾不存在,則創建文件夾。      if (!Directory.Exists(directory))      {          Directory.CreateDirectory(directory);      }        // 監視 directory 文件夾下的 file 文件的改變      var watcher = new FileSystemWatcher(directory, file)      {          EnableRaisingEvents = true,          NotifyFilter = NotifyFilters.LastWrite,      };      watcher.Changed += FinalFile_Changed;      // 使用 watcher 做其他的事情。  }    private void FinalFile_Changed(object sender, FileSystemEventArgs e)  {      // 當文件改變的時候,這裡的代碼會執行。  }

以上代碼的含義是:

  1. 將文件路徑取出來,分為文件夾部分和文件部分;
  2. 判斷文件夾是否存在,如果不存在,則創建文件夾;
  3. 監視文件夾中此文件的改變。

需要說明的是,FileSystemWatcher 原本是監視文件夾的,第一個參數是監視的文件夾的路徑,而第二個參數是監視文件或文件夾的過濾通配符。

不過,官方文檔的說明是:

To watch a specific file, set the Filter property to the file name. For example, to watch for changes in the file MyDoc.txt, set the Filter property to 「MyDoc.txt」.

如果你需要監聽一個特定的文件,那麼直接將後面的過濾器設定為文件名,那麼就會直接監視到對應的文件。

如果你的業務當中,反正始終都是要創建這個文件的,那麼一開始創建了這個文件夾就能避免不少的麻煩。這也是我把這個方法放到這裡作為首選方法的原因。雖然實際上這是在逃避問題,但真的是一個好方法。

方法二:遞歸監視文件夾

這種方法適用於如果文件或者文件夾不存在時,你不能創建這個文件夾的情況。也許是你的業務需要,也許因為你正在寫庫,庫作為最為通用的業務,不希望改變用戶的環境。

這時,我們可以考慮的思路是 —— 遞歸地監視文件或文件夾

例如,我們有這樣的文件夾結構:

C:abx.txt

希望監聽 x.txt 的改變。

那麼,如果 b 文件夾不存在,就監聽 a 文件夾,如果 a 文件夾也不存在,那麼就監聽 C: 驅動器。實際上,我們不需要再去考慮 C: 驅動器也不存在的情況了(當你真的遇到的時候,考慮業務上規避吧……)。

代碼實現

既然需要遞歸監視,那麼我們需要查找第一次監視的時候,需要到哪一層。

這裡,我們可以用一個 while 循環來進行,一層一層查找文件夾。直到能夠找到一層,文件夾存在而子文件夾不存在的情況。這時我們便能夠監視子文件夾的創建了。

我寫了一個函數,用於返回這時存在的那個文件夾,和不存在的那個子文件夾或者文件。

當然有特殊情況,就是文件直接就已經存在的情況下,也是返迴文件所在的文件夾和此文件名的。

private (string directory, string file) FindWatchableLevel()  {      var path = _file.FullName;        // 如果文件存在,就返迴文件所在的文件夾和文件本身。      if (File.Exists(path))      {          return (Path.GetDirectoryName(path), Path.GetFileName(path));      }        // 如果文件不存在,但文件夾存在,也是返迴文件夾和文件本身。      // 這一點在下面的第一層循環中體現。        // 對於每一層循環。      while (true)      {          var directory = Path.GetDirectoryName(path);          var file = Path.GetFileName(path);            // 檢查文件夾是否存在,只要文件夾存在,那麼就可以返回。          if (Directory.Exists(directory))          {              return (directory, file);          }            // 如果連文件夾都不存在,那麼就需要查找上一層文件夾。          path = directory;      }  }

接下來,根據得到的文件夾和文件,判斷其存在與否,決定是監視這個文件的改變,還是監視文件/文件夾結構的改變。如果文件/文件夾的結構改變,那麼就需要重新調用這個方法再查找應該監視的文件夾了。

public void Watch()  {      var (directory, file) = FindWatchableLevel();      if (File.Exists(_file.FullName))      {          // 如果文件存在,說明這是最終的文件。          // 注意使用 File.Exists 判斷已存在的同名文件夾時會返回 false。          _watcher = new FileSystemWatcher(directory, file)          {              EnableRaisingEvents = true,              NotifyFilter = NotifyFilters.LastWrite,          };          _watcher.Changed += FinalFile_Changed;          _watcher.Deleted += FileOrDirectory_CreatedOrDeleted;      }      else      {          // 注意這裡的 file 可能是文件也可能是文件夾。          _watcher = new FileSystemWatcher(directory, file)          {              EnableRaisingEvents = true,          };          _watcher.Created += FileOrDirectory_CreatedOrDeleted;          _watcher.Renamed += FileOrDirectory_CreatedOrDeleted;          _watcher.Deleted += FileOrDirectory_CreatedOrDeleted;      }  }

我們通過 File.Exists(_file.FullName) 來判斷最終的文件是否存在,用以區分是在監視最終的文件改變,還是監視文件夾結構的改變。

private void FileOrDirectory_CreatedOrDeleted(object sender, FileSystemEventArgs e)  {      // 在文件/文件夾結構發生改變的時候,重新監視。      _watcher?.Dispose();      Watch();  }    private void FinalFile_Changed(object sender, FileSystemEventArgs e)  {      // 這裡就是最終文件改變的地方了。  }

完整的代碼和使用方法

由於代碼還是有一點點多。如果放到你原有的業務當中,對你的業務代碼確實是一種污染。所以我封裝了一個類 FileWatcher。它不需要依賴任何就可以使用,你可以將它拷貝到你的項目當中。

代碼可以閱讀本文文末,或者前往 gist 查看:FileWatcher that helps you to watch a single file change even if the file or it』s owner folders does not exists.

使用方法與 FileSystemWatcher 類似,但是更簡單:

_watcher = new FileWatcher(@"C:UserswalterlvDesktopdemo.txt");  _watcher.Changed += OnFileChanged;  _watcher.Watch();
private void OnFileChanged(object sender, EventArgs e)  {      // 最純粹的文件改變事件,僅在文件的內容真的改變的時候觸發。  }

此方法的特點,優勢和不足

實際上,FileSystemWatcher 的監視也是有一些空洞的。如果你只是監視一級文件夾而不是遞歸監視子文件夾(通過設置 IncludeSubdirectories 屬性來指定),那麼就會存在一些情況是監視不到的。然而如果你真的遞歸監視子文件夾,又會監聽到大量的事件需要過濾。

那麼此方法可以支持和不支持的情況有哪些呢?

依然假設監視的文件是:C:abx.txt

支持這些情況:

  1. 一開始文件 x.txt 不存在,而後創建。
  2. 一開始 bx.txt 不存在,而後依次創建。
  3. 從 y.txt 文件重命名到 x.txt。
  4. 一開始文件 x.txt 存在,而後刪除,再然後重新創建。

不支持這些情況:

  1. 一開始文件存在,但你直接刪除了 a 或者 b 文件夾,而不是先刪除了 x.txt。
  2. 一開始文件存在,但直接將 bx.txt 連文件帶文件夾一起移走,然後刪除文件或文件夾。
  3. 一開始 bx.txt 都不存在,但現在保持文件夾結構連文件帶文件夾一起移入到 a 文件夾中。

當然,也有一些意外的發現:

  1. 一開始文件存在,但直接將 bx.txt 連文件帶文件夾一起移走,這時依然能監聽到 x.txt 文件的改變,但它已經不在原來的目錄了。

附所有源碼

如果看不到,請訪問:FileWatcher that helps you to watch a single file change even if the file or it』s owner folers does not exists.

參考資料
本文會經常更新,請閱讀原文: https://blog.walterlv.com/post/watch-file-change-even-the-file-or-

本作品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發佈,但務必保留文章署名 呂毅 (包含鏈接: https://blog.walterlv.com ),不得用於商業目的,基於本文修改後的作品務必以相同的許可發佈。如有任何疑問,請 與我聯繫 ([email protected])