FileSystemWatcher:忽略对文件的第一次更改

FileSystemWatcher: Ignore first change to the file

我在 C# 中使用 FileSystemWatcher class 来跟踪用户在修改文件后保存文件时的更改。

FileSystemWatcher watcher = new FileSystemWatcher()
{
    Path = DIR_NAME,
    NotifyFilter = NotifyFilters.LastWrite | 
                   NotifyFilters.CreationTime | 
                   NotifyFilters.LastAccess,
    Filter = "*.pdf",
    IncludeSubdirectories = true,
    EnableRaisingEvents = true
};

watcher.Changed += OnChanged;

但是,我要跟踪的文件是以编程方式创建的,如下所示:

FileStream stream = FileUtil.CreateNewFile(filePath); // Creates a file
file.WriteFileToStream(stream); // Writes into the file

理想情况下,我的代码应该 运行 如下:

  1. 程序将创建一个文件并向其中写入一些内容。
  2. 用户打开文件,修改并保存。
  3. 此时,它应该触发OnChanged,即我希望我的OnChanged处理程序代码仅在真实用户修改并保存时执行。

但是,只要以编程方式写入文件,即在以下行中,它就会被触发:

file.WriteFileToStream(stream);

从技术上讲,这是正确的行为,因为它正在跟踪文件中的更改。但是,我的业务案例不允许在最初创建和写入文件时执行 OnChanged 处理程序代码。

我的问题是,在第一次以编程方式创建和写入文件时,是否有跳过 OnChanged 调用的解决方法?

注:

  1. 应用程序架构要求 FileSystemWatcher 在我的应用程序启动时进行初始化。所以我无法在创建文件后注册它。
  2. 这是一个多用户应用程序,其中多个用户将同时写入文件,因此我无法在创建文件之前禁用观察器并在创建文件后启用它:

watcher.EnableRaisingEvents = false; 
CreateFile();
watcher.EnableRaisingEvents = true;

这是一个棘手的情况,除了检查文件是否被锁定(假设它有读锁或写锁)之外,您无能为力:

public  bool IsFileLocked(string fileName)
{

   try
   {
      using (var stream = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.None))
      {
         return false;
      }   
   }
   catch (IOException)
   {
      //the file is unavailable
      return true;
   }
}

前提是当你获得更新时,你检查文件是否被锁定,如果没有那么你可以宽松地假设它已被关闭。

不过需要考虑的一些事项:

  1. 当检查文件是否被锁定、清除所有内容,然后发现其他进程已锁定文件时,可能存在竞争条件。
  2. 我用过 FileAccess.Read,所以这不会在只读文件上失败。
  3. 由于各种原因,事件可能会从 FileSystemWatcher 中删除,并且在某些情况下不能总是依赖它;阅读文档以了解可以删除事件的原因以及如何修复它们。在这些情况下,您可以通过长时间的民意调查和一些逻辑来挑选掉队的人(取决于您的情况)。

我在 OnChanged 处理程序中添加了以下代码,它似乎按预期工作。

private void OnChanged(object source, FileSystemEventArgs file)
{
    if (file.ChangeType == WatcherChangeTypes.Created)
        return;

    FileInfo fileInfo = new FileInfo(file.FullPath);
    if (fileInfo.CreationTime == fileInfo.LastWriteTime)
        return; 

    // Handle the Changed event
    ...
}

但是,我不确定我是否遗漏了一些会导致它崩溃的东西。有什么想法吗?

方法一:

  1. 创建文件并将其保存在未被监视的目录中。
  2. 将文件移动到正在观看的目录中。

方法二:

创建文件后,使用 OnCreated() 事件处理程序将文件名添加到 filesToIgnore 列表。

OnChanged 事件处理程序中,检查 filesToIgnore 列表是否包含文件名。如果是,则将其从列表中删除(以便下次处理)并从处理程序中删除 return。

private List<string> filesToIgnore = new List<string>();

private void OnCreated(object source, FileSystemEventArgs file)
{
   filesToIgnore.Add(file.Name);
}

private void OnChanged(object source, FileSystemEventArgs file)
{
    if(filesToIgnore.Contains(file.Name))
    {
         filesToIgnore.Remove(file.Name);
         return; 
    }

    // Code to execute when user saves the file
}

此方法假定 OnCreated() 将始终在 OnChanged().

之前触发

下面的扩展方法只允许处理来自用户操作的事件:

public static void OnChangedByUser(this FileSystemWatcher fsw,
    FileSystemEventHandler handler)
{
    const int TOLERANCE_MSEC = 100;
    object locker = new object();
    string fileName = null;
    Stopwatch stopwatch = new Stopwatch();
    fsw.Created += OnFileCreated;
    fsw.Changed += OnFileChanged;
    fsw.Disposed += OnDisposed;
    void OnFileCreated(object sender, FileSystemEventArgs e)
    {
        lock (locker)
        {
            fileName = e.Name;
            stopwatch.Restart();
        }
    }
    void OnFileChanged(object sender, FileSystemEventArgs e)
    {
        lock (locker)
        {
            if (e.Name == fileName && stopwatch.ElapsedMilliseconds < TOLERANCE_MSEC)
            {
                return; // Ignore this event
            }
        }
        handler.Invoke(sender, e);
    }
    void OnDisposed(object sender, EventArgs e)
    {
        fsw.Created -= OnFileCreated;
        fsw.Changed -= OnFileChanged;
        fsw.Disposed -= OnDisposed;
    }
}

用法示例:

var fsw = new FileSystemWatcher(DIR_NAME);
fsw.OnChangedByUser(File_ChangedByUser);
fsw.EnableRaisingEvents = true;

private static void File_ChangedByUser(object sender, FileSystemEventArgs e)
{
    // Handle the event
}