software_writer
software_writer

Reputation: 4468

FileSystemWatcher: Ignore first change to the file

I am using FileSystemWatcher class in C# to track the changes on a file whenever the user saves the file after modifying it.

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

watcher.Changed += OnChanged;

However, the file that I want to track is getting created programmatically, as follows:

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

Ideally, my code is supposed to run as follows:

  1. The program will create a file and write some content into it.
  2. User opens the file, modifies it and saves.
  3. At this point, it should trigger OnChanged, i.e. I want my OnChanged handler code to execute only when a real user modifies it and saves it.

However, it's getting triggered whenever the file is getting written into programmatically, i.e. on the following line:

file.WriteFileToStream(stream);

Which is, technically, correct behavior, as it's tracking the change in the file. However, my business case doesn't allow the OnChanged handler code to be executed when the file is initially created and written into.

My question is, is there a workaround to skip the OnChanged call when the file is created and written into first time programmatically?

Note:

  1. The application architecture requires that the FileSystemWatcher is initialized when my application starts. So I cannot register it after the file is created.
  2. This is a multi-user application, where multiple users will be writing into the files simultaneously, so I cannot disable the watcher before creating the file and enable it after its created:

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

Upvotes: 2

Views: 2087

Answers (4)

TheGeneral
TheGeneral

Reputation: 81523

This is a sticky situation and there is not much you can do about it apart from checking if the file is locked (assuming it has a read or write lock):

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;
   }
}

The premise is when you get an update, you check if the file is locked, if it isn't then you can loosely assume it's been closed.

Some things to consider though:

  1. There could be a race condition when checking the file is locked, getting the all clear, and then finding out some other process has locked the file.
  2. I have used FileAccess.Read, so this doesn't fail on read only files.
  3. Events can be dropped from the FileSystemWatcher for various reasons, and it can't always be relied on in certain situation; read the documentation for the reasons why events can be dropped and how to fix them. In these cases, you could possible get away with a long poll and some logic to pick up any stragglers (depending on your situation).

Upvotes: 0

software_writer
software_writer

Reputation: 4468

Approach One:

  1. Create and save the file in a directory that is not being watched.
  2. Move the file into the directory being watched.

Approach Two:

When a file is created, use OnCreated() event handler to add the file name to a filesToIgnore list.

In the OnChanged event handler, check if the filesToIgnore list contains the file name. If it does, remove it from the list (to process it next time) and return from the handler.

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
}

This approach assumes that OnCreated() will be always triggered before OnChanged().

Upvotes: 1

Theodor Zoulias
Theodor Zoulias

Reputation: 43728

The extension method bellow allows handling events from user's actions only:

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;
    }
}

Usage example:

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
}

Upvotes: 0

software_writer
software_writer

Reputation: 4468

I added the following code in OnChanged handler, and it seems to be working as expected.

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
    ...
}

However, I am not sure if I am missing something that will cause this to break. Any thoughts?

Upvotes: 0

Related Questions