Introduction

In this article, we are going to develop a file watcher application. This application will listen for file changes on a given directory.
We will add controls to check the activity done on .txt file types.
The same concept can be also employed on different file types.
We will be using C# and WPF technologies.
We will also use Visual Studio 2019 version but any version can work.
The application we are going to create will look like this
Sample Application
We have the following sections:
Files
The files section will be the root directory that we will be watching or listening for changes.
Activity
On the activity section, we will be logging changes to files
Editor
This is a simple text editor that we will be using to make our text file changes.
We will also save the location in the application settings for easy retrieval.
Here are the steps that we will follow
User Interface
Our user interface will consist of the following code snippnet
  1. <Window x:Class="FileWatcherApp.MainWindow"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6. xmlns:local="clr-namespace:FileWatcherApp"
  7. mc:Ignorable="d"
  8. Title="MainWindow" Height="450" Width="800" Closing="Window_Closing">
  9. <Grid>
  10. <Grid Margin="10">
  11. <Grid.RowDefinitions>
  12. <RowDefinition Height="30"/>
  13. <RowDefinition Height="*"/>
  14. </Grid.RowDefinitions>
  15. <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Grid.Row="0">
  16. <TextBlock Margin="5" Text="Location"></TextBlock>
  17. <TextBox Width="300" Margin="5" Name="txtDirectory"></TextBox>
  18. <Button Name="btnBrowse" Width="90" Content="Browse..." Margin="5" Click="btnBrowse_Click"></Button>
  19. <Button Name="btnListen" Width="90" Content="Start Watching" Margin="5" Click="btnListen_Click"></Button>
  20. </StackPanel>
  21. <Grid Grid.Row="1">
  22. <Grid.ColumnDefinitions>
  23. <ColumnDefinition Width="*"></ColumnDefinition>
  24. <ColumnDefinition Width="*"></ColumnDefinition>
  25. </Grid.ColumnDefinitions>
  26. <Grid Grid.Column="0" Name="FilesGrid">
  27. <Grid.RowDefinitions>
  28. <RowDefinition Height="*"></RowDefinition>
  29. <RowDefinition Height="*"></RowDefinition>
  30. </Grid.RowDefinitions>
  31. <GroupBox x:Name="groupBox" Grid.Row="0" Header="Files" MinHeight="100" HorizontalAlignment="Left" VerticalAlignment="Top" Width="{Binding ActualWidth, ElementName=FilesGrid, Mode=OneWay}">
  32. <TreeView Name="treeFiles" SelectedItemChanged="treeFiles_SelectedItemChanged"></TreeView>
  33. </GroupBox>
  34. <GroupBox x:Name="groupBoxEditor" Grid.Row="1" MinHeight="100" Header="Editor" HorizontalAlignment="Left" VerticalAlignment="Top" Width="{Binding ActualWidth, ElementName=FilesGrid, Mode=OneWay}">
  35. <TextBox x:Name="txtEditor" TextChanged="txtEditor_TextChanged"></TextBox>
  36. </GroupBox>
  37. </Grid>
  38. <Grid Grid.Column="1" Name="ActivityGrid">
  39. <GroupBox x:Name="groupBox1"Header="Activity" MinHeight="100"HorizontalAlignment="Left" VerticalAlignment="Top"Width="{Binding ActualWidth, ElementName=FilesGrid, Mode=OneWay}">
  40. <ListView x:Name="lstResults" ></ListView>
  41. </GroupBox>
  42. </Grid>
  43. </Grid>
  44. </Grid>
  45. </Grid>
  46. </Window>
Our backend code will look like this:
  1. using FileWatcherApp.Properties;
  2. using System;
  3. using System.IO;
  4. using System.Text.RegularExpressions;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Forms;
  8. using System.Windows.Threading;
  9. namespace FileWatcherApp
  10. {
  11. /// <summary>
  12. /// Interaction logic for MainWindow.xaml
  13. /// </summary>
  14. public partial class MainWindow : Window
  15. {
  16. FileSystemWatcher watcher;
  17. static readonly object locker = new object();
  18. private Timer timer = new Timer();
  19. private bool isWatching;
  20. private bool canChange;
  21. private string filePath = string.Empty;
  22. DateTime lastRead = DateTime.MinValue;
  23. public MainWindow()
  24. {
  25. InitializeComponent();
  26. if (!string.IsNullOrEmpty(Settings.Default.PathSetting))
  27. {
  28. txtDirectory.Text = Settings.Default.PathSetting;//Get Saved Path
  29. ListDirectory(treeFiles, txtDirectory.Text);
  30. }
  31. }
  32. private void btnBrowse_Click(object sender, RoutedEventArgs e)
  33. {
  34. var dialog = new FolderBrowserDialog();
  35. if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
  36. {
  37. txtDirectory.Text = dialog.SelectedPath;
  38. }
  39. ListDirectory(treeFiles, txtDirectory.Text);
  40. }
  41. //This method will list files to our treeview
  42. private void ListDirectory(System.Windows.Controls.TreeView treeView, string path)
  43. {
  44. try
  45. {
  46. treeView.Items.Clear();
  47. var rootDirectoryInfo = new DirectoryInfo(path);
  48. treeView.Items.Add(CreateDirectoryItems(rootDirectoryInfo));
  49. }
  50. catch (Exception ex)
  51. {
  52. AppendListViewcalls(ex.Message);
  53. }
  54. }
  55. private static TreeViewItem CreateDirectoryItems(DirectoryInfo directoryInfo)
  56. {
  57. var directoryItem = new TreeViewItem { Header = directoryInfo.Name };
  58. foreach (var directory in directoryInfo.GetDirectories())
  59. directoryItem.Items.Add(CreateDirectoryItems(directory));
  60. foreach (var file in directoryInfo.GetFiles())
  61. directoryItem.Items.Add(new TreeViewItem { Header = file.Name, Tag = file.FullName });
  62. return directoryItem;
  63. }
  64. private void btnListen_Click(object sender, RoutedEventArgs e)
  65. {
  66. //We want to check whether the filewatche is on or not and display usefull signal to the user either to start or stop
  67. if (isWatching)
  68. {
  69. btnListen.Content = "Start Watching";
  70. stopWatching();
  71. }
  72. else
  73. {
  74. btnListen.Content = "Stop Watching";
  75. startWatching();
  76. }
  77. }
  78. private void startWatching()
  79. {
  80. if (!isDirectoryValid(txtDirectory.Text))
  81. {
  82. AppendListViewcalls(DateTime.Now + " - Watch Directory Invalid");
  83. return;
  84. }
  85. isWatching = true;
  86. timer.Enabled = true;
  87. timer.Start();
  88. timer.Interval = 500;
  89. AppendListViewcalls(DateTime.Now + " - Watcher Started");
  90. watcher = new FileSystemWatcher();
  91. watcher.Path = txtDirectory.Text;
  92. watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite
  93. | NotifyFilters.FileName | NotifyFilters.DirectoryName;
  94. watcher.Filter = "*.*";
  95. watcher.Created += new FileSystemEventHandler(OnChanged);
  96. watcher.Renamed += new RenamedEventHandler(OnChanged);
  97. watcher.Changed += new FileSystemEventHandler(OnChanged);
  98. watcher.EnableRaisingEvents = true;
  99. }
  100. private void stopWatching()
  101. {
  102. isWatching = false;
  103. timer.Enabled = false;
  104. timer.Stop();
  105. AppendListViewcalls(DateTime.Now + " - Watcher Stopped");
  106. }
  107. private bool isDirectoryValid(string path)
  108. {
  109. if (Directory.Exists(path))
  110. {
  111. return true;
  112. }
  113. else
  114. {
  115. return false;
  116. }
  117. }
  118. protected void OnChanged(object source, FileSystemEventArgs e)
  119. {
  120. //Specify what to do when a file is changed, created, or deleted
  121. //filter file types
  122. if (Regex.IsMatch(System.IO.Path.GetExtension(e.FullPath), @"\.txt", RegexOptions.IgnoreCase))
  123. {
  124. try
  125. {
  126. while (IsFileLocked(e.FullPath))
  127. {
  128. System.Threading.Thread.Sleep(100);
  129. }
  130. lock (locker)
  131. {
  132. //Process file
  133. //Do further activities
  134. DateTime lastWriteTime = File.GetLastWriteTime(e.FullPath);
  135. if (lastWriteTime != lastRead)
  136. {
  137. AppendListViewcalls("File: \"" + e.FullPath + "\"- " + DateTime.Now + " - Processed the changes successfully");
  138. lastRead = lastWriteTime;
  139. }
  140. }
  141. }
  142. catch (FileNotFoundException)
  143. {
  144. //Stop processing
  145. }
  146. catch (Exception ex)
  147. {
  148. AppendListViewcalls("File: \"" + e.FullPath + "\" ERROR processing file (" + ex.Message + ")");
  149. }
  150. }
  151. else
  152. AppendListViewcalls("File: \"" + e.FullPath + "\" has been ignored");
  153. }
  154. private static bool IsFileLocked(string file)
  155. {
  156. FileStream stream = null;
  157. try
  158. {
  159. stream = new FileInfo(file).Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None);
  160. }
  161. catch (FileNotFoundException err)
  162. {
  163. throw err;
  164. }
  165. catch (IOException)
  166. {
  167. //the file is unavailable because it is:
  168. //still being written to
  169. //or being processed by another thread
  170. //or does not exist (has already been processed)
  171. return true;
  172. }
  173. finally
  174. {
  175. if (stream != null)
  176. stream.Close();
  177. }
  178. //file is not locked
  179. return false;
  180. }
  181. public void AppendListViewcalls(string input)
  182. {
  183. this.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(delegate ()
  184. {
  185. this.lstResults.Items.Add(input);
  186. }));
  187. }
  188. private void treeFiles_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
  189. {
  190. try
  191. {
  192. var item = (TreeViewItem)e.NewValue;
  193. filePath = item.Tag.ToString();
  194. //Check changes for .txt files only
  195. if (Regex.IsMatch(System.IO.Path.GetExtension(filePath), @"\.txt", RegexOptions.IgnoreCase))
  196. {
  197. canChange = false;
  198. txtEditor.Clear();
  199. string contents = File.ReadAllText(filePath);
  200. txtEditor.Text = contents;
  201. canChange = true;
  202. }
  203. }
  204. catch (Exception ex)
  205. {
  206. AppendListViewcalls(ex.Message);
  207. }
  208. }
  209. private void txtEditor_TextChanged(object sender, TextChangedEventArgs e)
  210. {
  211. try
  212. {
  213. if (canChange)
  214. {
  215. System.IO.File.WriteAllText(filePath, txtEditor.Text);
  216. }
  217. }
  218. catch (Exception ex)
  219. {
  220. AppendListViewcalls(ex.Message);
  221. }
  222. }
  223. private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
  224. {
  225. //Save Path on closure
  226. Settings.Default.PathSetting = txtDirectory.Text;
  227. Settings.Default.Save();
  228. }
  229. }
  230. }
If you encounter a namespace reference error on the System.Windows.Forms namespace kindly add the reference as depicted in the picture below.
Windows Namespace Reference
Our project is complete and when run it should display a user interface like this and we are free to test our implementation.
Final output
While our start listening button is displaying we can try to change the text in the editor as much as we want and we can see no activity is logged.

However, when we start listening and make changes to any selected file we can see that the activities are being logged on the activity section.
Complete Application
This concludes our file watcher application.