Wednesday, May 13, 2009

FileSystemWatcher Done Right.

For some reason the FileSystemWatcher (aka ChangeNotify) in Windows is just not right. If you search for this in on the internet, you'll find tons of issues with people attempting to use it and running into problems. A couple of the top issues:

* The FileSystemWatcher calls multiple times on a single change.
* It does not call when certain programs change the file.
* If my process changes the file, the FileSystemWatcher calls me. How do I know I was the only one who changed it (and someone else didn't slip in after I called Close()).

I built the following class to encapsulate all of these issues, and to do file watching correctly.

The only things you need to know:
* Create a FileWatcher on a file on the disk. If it changes by an external process, you will get called once per change.
* If this process needs to write the file, call the FileWatcher's CloseFileInThisProcess() method when ready to close the file. This will ensure you don't get called when you change the file.

This is written for WPF. The only reason that matters is because of the threading issues. It can easily be tweaked for WinForms.

There is an example usage following the code.



///
/// This filewatcher works around the 'issues'
/// with Window's file watcher .NET FileSystemWatcher
/// or Win32::ChangeNotify). This code is written for
/// WPF, but can be tweaked to work with Winforms as well.
///
/// It solves these two main problems:
/// * The FileSystemWatcher calls multiple times on
/// a single change.
/// * If my process changes the file, the
/// FileSystemWatcher calls me.
///
/// It solves the former by using a 100 ms timer to
/// collapse multiple calls into a single call. It
/// solves the latter by storing the file size and
/// time when this process writes a file, and
/// comparing this to the values when notified by
/// FileSystemWatcher.
///
/// Usage is straightforward, except that you must
/// call CloseFileInThisProcess when you are closing
/// the file that this watcher is watching. It will
/// carefully close the file in such a way that it
/// can later tell if the change was by this process
/// or another.
///
///

public class FileWatcher : FileSystemWatcher
{
public delegate void FileChangedHandler(string filepath);
public FileWatcher(string filepath, FileChangedHandler handler) :
base(System.IO.Path.GetDirectoryName(filepath),
System.IO.Path.GetFileName(filepath))
{
FilePath = filepath;
Handler = handler;
NotifyFilter =
NotifyFilters.FileName |
NotifyFilters.Attributes |
NotifyFilters.LastAccess |
NotifyFilters.LastWrite |
NotifyFilters.Security |
NotifyFilters.Size;
Changed += new FileSystemEventHandler(delegate(object sender, FileSystemEventArgs e)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(
new VoidDelegate(this.FileChanged));
});
UpdateFileInfo();
Timer = new Timer(100);
Timer.AutoReset = false;
Timer.Elapsed += new ElapsedEventHandler(delegate(object sender, ElapsedEventArgs e)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke(
new VoidDelegate(this.TimerElapsed));
});
EnableRaisingEvents = true;
}

///
/// This only works with StreamWriters. You may need
/// to make your own version for whatever writer you are
/// using.
///
/// How this works: It stores the file size before
/// calling close. Then, after close it grabs the write
/// time. There is a minute corner case here: If
/// another process gets in and writes the file after
/// the close, but before the GetLastWriteTime call, and the
/// file is the same length, we will not detect the change.
/// If that is critical, one could do a checksum on the file....
///

public void CloseFileInThisProcess(StreamWriter writer)
{
writer.Flush();
LastFileLength = writer.BaseStream.Length;
writer.Close();
LastWriteTime = File.GetLastWriteTime(FilePath);
}


void UpdateFileInfo()
{
var fileInfo = new FileInfo(FilePath);
LastWriteTime = fileInfo.LastWriteTime;
LastFileLength = fileInfo.Length;
}

public delegate void VoidDelegate();

void FileChanged()
{
var fileInfo = new FileInfo(FilePath);
if (LastWriteTime != fileInfo.LastWriteTime || LastFileLength != fileInfo.Length)
if (!Timer.Enabled)
Timer.Start();
}

void TimerElapsed()
{
UpdateFileInfo();
Handler(FilePath);
}

string FilePath;
FileChangedHandler Handler;
DateTime LastWriteTime;
long LastFileLength;
Timer Timer;
}




Here is an example usage:



public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();

// Set up the file watcher given a filename and a handler.
Watcher = new FileWatcher(@"c:\temp\test.txt", FileChanged);
}
FileWatcher Watcher;
void FileChanged(string filename)
{
// For now just display a message when the file changed.
MessageBox.Show("Got here");
}

// Here is an example of writing the file.
protected override void OnMouseDown(MouseButtonEventArgs e)
{
var writer = new StreamWriter(@"c:\temp\test.txt");
try
{
writer.Write("Hello, world, number " + I++);
}
finally
{
Watcher.CloseFileInThisProcess(writer);
}
}
int I = 0;
}