Tracking the contents of a single file in .NET

In C#/.NET, when you want to track a directory for changes you use FileSystemWatcher. Sometimes, you only need to track a particular file. I created a class FileWatcher for this reason.

This class minimizes IO usage by reading lazily and memoizing the result, which means the read is performed only when you request the watcher.Content value. When the file changes, it sets a flag to indicate that a re-read is necessary, and then raises the ContentChanged and PropertyChanged events. Thus, if the new Content is not requested after the file has changed. the actual file read is never performed.

If you create multiple FileWatchers pointing to the same file, the internal watcher is reused and additional IO reads will be avoided, making instance creation cheap. Instance destruction is also cheap, thread-safe, and leak-free.

To quickly test it, you can create a console application as follows:

static void Main(string[] args)
{
    var watcher = new FileWatcher("myfile.txt");
    watcher.ContentChanged += (s, e) =>
    {
        Console.WriteLine(watcher.Content);
    };

    Console.ReadLine();
}

Note that if the file does not exist, has been deleted, has been renamed, or any other error has occurred, the returned content will be an empty string.

You can copy the following class to your project:


/// <summary>
/// Tracks the content of a single file.
/// </summary>
public sealed class FileWatcher : IDisposable, INotifyPropertyChanged
{
private class Watcher : IDisposable
{
private readonly List<FileWatcher> listeners;
private readonly FileSystemWatcher fileSystemWatcher;
private readonly string filePath;
private bool isLatestValue;
private string value;
public Watcher(FileWatcher initialListener)
{
filePath = initialListener.filePath;
listeners = new List<FileWatcher> { initialListener };
fileSystemWatcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(filePath),
Filter = Path.GetFileName(filePath),
EnableRaisingEvents = true
};
fileSystemWatcher.Created += (s, e) => Update();
fileSystemWatcher.Changed += (s, e) => Update();
fileSystemWatcher.Deleted += (s, e) => Update();
fileSystemWatcher.Renamed += (s, e) => Update();
fileSystemWatcher.Error += (s, e) => Update();
}
public string Value
{
get
{
lock (this)
{
if (!isLatestValue)
{
value = TryReadFile();
isLatestValue = true;
}
return value;
}
}
private set => this.value = value;
}
public int Count => listeners.Count;
public void AddListener(FileWatcher listener)
{
listeners.Add(listener);
}
public void RemoveListener(FileWatcher listener)
{
listeners.Remove(listener);
}
public void Dispose()
{
fileSystemWatcher.Dispose();
}
private void Update()
{
try
{
fileSystemWatcher.EnableRaisingEvents = false;
isLatestValue = false;
foreach (var listener in listeners)
{
listener.NotifyChanged();
}
}
finally
{
fileSystemWatcher.EnableRaisingEvents = true;
}
}
private string TryReadFile()
{
try
{
using (var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
catch
{
return "";
}
}
}
private static readonly Dictionary<string, Watcher> Watchers
= new Dictionary<string, Watcher>(StringComparer.OrdinalIgnoreCase);
private readonly string filePath;
private bool disposed;
public FileWatcher(string filePath)
{
filePath = Path.GetFullPath(filePath ?? throw new ArgumentNullException(nameof(filePath)));
this.filePath = filePath;
lock (Watchers)
{
if (Watchers.ContainsKey(filePath))
{
Watchers[filePath].AddListener(this);
}
else
{
Watchers[filePath] = new Watcher(this);
}
}
}
~FileWatcher()
{
Dispose(false);
}
public string Content
{
get
{
lock (Watchers)
{
if (disposed)
{
throw new ObjectDisposedException(nameof(FileWatcher));
}
return Watchers[filePath].Value;
}
}
}
public event EventHandler ContentChanged;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyChanged()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Content)));
ContentChanged?.Invoke(this, EventArgs.Empty);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
lock (Watchers)
{
if (disposed)
{
return;
}
disposed = true;
var watcher = Watchers[filePath];
watcher.RemoveListener(this);
if (watcher.Count == 0)
{
watcher.Dispose();
Watchers.Remove(filePath);
}
}
}
}

view raw

FileWatcher.cs

hosted with ❤ by GitHub

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s