Creating A File System Watcher Application

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
  • Create the C# WPF application project

    Create New Project Dialog

  • Give the project a name of your choice

    Project Name Dialog

  • Press create and once we are going to add the following code.
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.   
  10. namespace FileWatcherApp  
  11. {  
  12.     /// <summary>  
  13.     /// Interaction logic for MainWindow.xaml  
  14.     /// </summary>  
  15.     public partial class MainWindow : Window  
  16.     {  
  17.         FileSystemWatcher watcher;  
  18.         static readonly object locker = new object();  
  19.         private Timer timer = new Timer();  
  20.         private bool isWatching;  
  21.         private bool canChange;  
  22.         private string filePath = string.Empty;  
  23.         DateTime lastRead = DateTime.MinValue;  
  24.   
  25.   
  26.         public MainWindow()  
  27.         {  
  28.             InitializeComponent();  
  29.              
  30.             if (!string.IsNullOrEmpty(Settings.Default.PathSetting))  
  31.             {  
  32.                 txtDirectory.Text = Settings.Default.PathSetting;//Get Saved Path  
  33.                 ListDirectory(treeFiles, txtDirectory.Text);  
  34.             }  
  35.         }  
  36.   
  37.         private void btnBrowse_Click(object sender, RoutedEventArgs e)  
  38.         {  
  39.   
  40.             var dialog = new FolderBrowserDialog();  
  41.   
  42.             if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)  
  43.             {  
  44.                 txtDirectory.Text = dialog.SelectedPath;  
  45.             }  
  46.             ListDirectory(treeFiles, txtDirectory.Text);  
  47.   
  48.         }  
  49.   
  50.         //This method will list files to our treeview  
  51.         private void ListDirectory(System.Windows.Controls.TreeView treeView, string path)  
  52.         {  
  53.             try  
  54.             {  
  55.                 treeView.Items.Clear();  
  56.                 var rootDirectoryInfo = new DirectoryInfo(path);  
  57.                 treeView.Items.Add(CreateDirectoryItems(rootDirectoryInfo));  
  58.             }  
  59.             catch (Exception ex)  
  60.             {  
  61.                 AppendListViewcalls(ex.Message);  
  62.             }  
  63.   
  64.         }  
  65.   
  66.         private static TreeViewItem CreateDirectoryItems(DirectoryInfo directoryInfo)  
  67.         {  
  68.             var directoryItem = new TreeViewItem { Header = directoryInfo.Name };  
  69.             foreach (var directory in directoryInfo.GetDirectories())  
  70.                 directoryItem.Items.Add(CreateDirectoryItems(directory));  
  71.   
  72.             foreach (var file in directoryInfo.GetFiles())  
  73.                 directoryItem.Items.Add(new TreeViewItem { Header = file.Name, Tag = file.FullName });  
  74.   
  75.             return directoryItem;  
  76.   
  77.         }  
  78.         private void btnListen_Click(object sender, RoutedEventArgs e)  
  79.         {  
  80.             //We want to check whether the filewatche is on or not and display usefull signal to the user either to start or stop  
  81.             if (isWatching)  
  82.             {  
  83.                 btnListen.Content = "Start Watching";  
  84.                 stopWatching();  
  85.             }  
  86.             else  
  87.             {  
  88.                 btnListen.Content = "Stop Watching";  
  89.                 startWatching();  
  90.             }  
  91.   
  92.         }  
  93.   
  94.         private void startWatching()  
  95.         {  
  96.             if (!isDirectoryValid(txtDirectory.Text))  
  97.             {  
  98.                 AppendListViewcalls(DateTime.Now + " - Watch Directory Invalid");  
  99.                 return;  
  100.             }  
  101.             isWatching = true;  
  102.             timer.Enabled = true;  
  103.             timer.Start();  
  104.             timer.Interval = 500;  
  105.             AppendListViewcalls(DateTime.Now + " - Watcher Started");  
  106.   
  107.             watcher = new FileSystemWatcher();  
  108.             watcher.Path = txtDirectory.Text;  
  109.             watcher.NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite  
  110.                | NotifyFilters.FileName | NotifyFilters.DirectoryName;  
  111.             watcher.Filter = "*.*";  
  112.             watcher.Created += new FileSystemEventHandler(OnChanged);  
  113.             watcher.Renamed += new RenamedEventHandler(OnChanged);  
  114.             watcher.Changed += new FileSystemEventHandler(OnChanged);  
  115.             watcher.EnableRaisingEvents = true;  
  116.         }  
  117.         private void stopWatching()  
  118.         {  
  119.             isWatching = false;  
  120.             timer.Enabled = false;  
  121.             timer.Stop();  
  122.             AppendListViewcalls(DateTime.Now + " - Watcher Stopped");  
  123.         }  
  124.   
  125.         private bool isDirectoryValid(string path)  
  126.         {  
  127.             if (Directory.Exists(path))  
  128.             {  
  129.                 return true;  
  130.             }  
  131.             else  
  132.             {  
  133.                 return false;  
  134.             }  
  135.         }  
  136.         protected void OnChanged(object source, FileSystemEventArgs e)  
  137.         {  
  138.             //Specify what to do when a file is changed, created, or deleted  
  139.   
  140.             //filter file types  
  141.             if (Regex.IsMatch(System.IO.Path.GetExtension(e.FullPath), @"\.txt", RegexOptions.IgnoreCase))  
  142.             {  
  143.                 try  
  144.                 {  
  145.                     while (IsFileLocked(e.FullPath))  
  146.                     {  
  147.                         System.Threading.Thread.Sleep(100);  
  148.                     }  
  149.   
  150.                     lock (locker)  
  151.                     {  
  152.                         //Process file  
  153.                         //Do further activities  
  154.                         DateTime lastWriteTime = File.GetLastWriteTime(e.FullPath);  
  155.                         if (lastWriteTime != lastRead)  
  156.                         {  
  157.                             AppendListViewcalls("File: \"" + e.FullPath + "\"- " + DateTime.Now + " - Processed the changes successfully");  
  158.                             lastRead = lastWriteTime;  
  159.                         }  
  160.                          
  161.                     }  
  162.                 }  
  163.                 catch (FileNotFoundException)  
  164.                 {  
  165.                     //Stop processing  
  166.                 }  
  167.                 catch (Exception ex)  
  168.                 {  
  169.                     AppendListViewcalls("File: \"" + e.FullPath + "\" ERROR processing file (" + ex.Message + ")");  
  170.                 }  
  171.             }  
  172.   
  173.             else  
  174.                 AppendListViewcalls("File: \"" + e.FullPath + "\" has been ignored");  
  175.   
  176.   
  177.         }  
  178.   
  179.         private static bool IsFileLocked(string file)  
  180.         {  
  181.             FileStream stream = null;  
  182.   
  183.             try  
  184.             {  
  185.                 stream = new FileInfo(file).Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None);  
  186.             }  
  187.             catch (FileNotFoundException err)  
  188.             {  
  189.                 throw err;  
  190.             }  
  191.             catch (IOException)  
  192.             {  
  193.                 //the file is unavailable because it is:  
  194.                 //still being written to  
  195.                 //or being processed by another thread  
  196.                 //or does not exist (has already been processed)  
  197.                 return true;  
  198.             }  
  199.             finally  
  200.             {  
  201.                 if (stream != null)  
  202.                     stream.Close();  
  203.             }  
  204.   
  205.             //file is not locked  
  206.             return false;  
  207.         }  
  208.   
  209.   
  210.         public void AppendListViewcalls(string input)  
  211.         {  
  212.             this.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(delegate ()  
  213.             {  
  214.                 this.lstResults.Items.Add(input);  
  215.   
  216.             }));  
  217.   
  218.         }  
  219.   
  220.         private void treeFiles_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)  
  221.         {  
  222.             try  
  223.             {  
  224.                 var item = (TreeViewItem)e.NewValue;  
  225.                  filePath = item.Tag.ToString();  
  226.                   
  227.                 //Check changes for .txt files only  
  228.                 if (Regex.IsMatch(System.IO.Path.GetExtension(filePath), @"\.txt", RegexOptions.IgnoreCase))  
  229.                 {  
  230.                     canChange = false;  
  231.                     txtEditor.Clear();  
  232.                     string contents = File.ReadAllText(filePath);  
  233.                     txtEditor.Text = contents;  
  234.                     canChange = true;  
  235.                 }  
  236.             }  
  237.             catch (Exception ex)  
  238.             {  
  239.                 AppendListViewcalls(ex.Message);  
  240.             }  
  241.              
  242.         }  
  243.   
  244.         private void txtEditor_TextChanged(object sender, TextChangedEventArgs e)  
  245.         {  
  246.             try  
  247.             {  
  248.                 if (canChange)  
  249.                 {  
  250.                     System.IO.File.WriteAllText(filePath, txtEditor.Text);  
  251.                 }  
  252.                  
  253.             }  
  254.             catch (Exception ex)  
  255.             {  
  256.                 AppendListViewcalls(ex.Message);  
  257.             }  
  258.         }  
  259.   
  260.         private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)  
  261.         {  
  262.             //Save Path on closure  
  263.             Settings.Default.PathSetting = txtDirectory.Text;  
  264.             Settings.Default.Save();  
  265.         }  
  266.     }  

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.