Saturday, December 5, 2009

Code4Food #3: Text Editor in 5 minutes – Part II. FileSystemWatcher class.

By the way I installed a rating widget on my blog. So now you can rate my posts if you like them, or if you dislike them you can put a 1 star rate too.

Hi,

Finally I found time to continue my little experiment with the text editor. Here you can read the previous post on text editor.

Today we’re going to talk about:

So here is the to download it :

ViewFile_0.2 sourcecode

 

SaVE FilE

This part actually was relatively easy to do. It was very similar to reading. Using TextWriter was enough to save the file. I still like looking fancy that’s why I used a SaveFileDialog for saving a file.

I indicated that saving would be possible by default in 2 formats without indicating extension (see saveFileDialog.Filter property), text files and C# class files.

private void btnSaveFile_Click(object sender, EventArgs e)
{
	SaveFileDialog saveFileDialog = new SaveFileDialog();
	saveFileDialog.Filter = "Text Files (*.txt)|*.txt|C# files (*.cs)|*.cs|All files (*.*)|*.*";

	saveFileDialog.SupportMultiDottedExtensions = true;

	DialogResult result = saveFileDialog.ShowDialog();

	if (result == DialogResult.OK)
	{
		_fileName = saveFileDialog.FileName;
		
		if (!string.IsNullOrEmpty(_fileName))
			SaveFileContent();
	}
}

private void SaveFileContent()
{
	lblFileName.Text = Path.GetFileName(_fileName);

	try
	{
		// we'll make only a textual file for instance
		TextWriter tw = File.CreateText(_fileName);

		try
		{
			tw.Write(txtFileContent.Text);
		}
		catch (Exception ex)
		{ MessageBox.Show(ex.Message); }
		finally
		{
			tw.Close();

			StartMonitorization();
		}
	}
	catch (UnauthorizedAccessException ex)
	{ MessageBox.Show("Sorry, you lack sufficient privileges."); }
	catch (Exception ex)
	{ MessageBox.Show(ex.Message); }
}
Notifications Sending when Your file is changed

I didn’t actually planned to implement this but I saw a great opportunity. It was already a class there that could permit me to do this in a very easy way.

So here it is:

FileSystemWatcher class – this class is particularly used to “watch” the changes that occur on a particular map or folder or a group of files (for example: textual files).

The most important things in this class are:

Properties

  • string Filter  -  Gets or sets the filter string used to determine what files are monitored in a directory.
  • string Path  -  Gets or sets the path of the directory to watch.

Both of these properties are indicated in constructor: FileSystemWatcher(String path, String filter)
Initializes a new instance of the FileSystemWatcher class, given the specified directory and type of files to monitor.

  • NotifyFilters NotifyFilter – Here you specify what kind of events do you want to watch:
    • FileName - The name of the file.
    • DirectoryName - The name of the directory.
    • Attributes - The attributes of the file or folder.
    • Size - The size of the file or folder.
    • LastWrite - The date the file or folder last had anything written to it.
    • LastAccess - The date the file or folder was last opened.
    • CreationTime - The time the file or folder was created.
    • Security - The security settings of the file or folder.
  • bool IncludeSubdirectories - Gets or sets a value indicating whether subdirectories within the specified path should be monitored.
  • bool EnableRaisingEvents - Gets or sets a value indicating whether the component is enabled.

Events

  • Created  -  Occurs when a file or directory in the specified Path is created.
  • Changed  -  Occurs when a file or directory in the specified Path is changed.
  • Renamed  -  Occurs when a file or directory in the specified Path is renamed.
  • Deleted  -  Occurs when a file or directory in the specified Path is deleted.
  • Error -  Occurs when the internal buffer overflows.

For more information consult MSDN on FileSystemWatcher.

So here is the code I implemented for support of notifications.

First I implemented StartMonitorization method where i initialized my FileSystemWatcher, and wired the events to it. I also specified the name of the file to monitor (it is always the file i load or save), and set the events I want to catch. This was done by setting NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite. This means that I would like to catch the events then the filename is changed or when the file is saved. Then i set EnableRaisingEvents = true for my _fileMonitor. If this property is set to false, events are not generated.

Then I implemented the event handlers. First of all I got to say that I made a single handler for Created and Changed event, and another two for Deleted and Renamed events.

The workflow was easy: when an event occurs I display a message box and ask if user wants to reload the file in case of Created, Renamed, Changed events, or remove the file from text editor if i get the Deleted event. If user answers OK, then I execute the needed method.

 

Problems I’ve met

  1. Now the first problem I saw was that my methods were not executed because I try to modify the UI thread from watcher thread. That is not working that way. We need to use BeginInvoke method and to create a delegate in UI thread, and to pass him the names of the methods to execute. The difference is that this delegate is on UI thread, and he is allowed to execute the methods from the same thread. So I created LoadContentCallback delegate.
  2. Second problem that I had was that the events were generated twice. To solve this problem I created 3 boolean fields to match the events. When the event is generated I set this boolean to true. Second time I set it to false, without actually executing the logic. With the Deleted event I had no problems because i dispose the _fileMonitor once user deletes file from text editor.
  3. Changed event handler handled 2 events Created and Changed, so I was wondering how am I gonna determine which event is actually handled. But the arguments of the event FileSystemEventArgs helped me because there is a property WatcherChangeTypes e.ChangeType which indicate what kind of change was made. From here it was easy because I knew what boolean to reset.
  4. To determine the new file name when Renamed event occurs was super easy thanks to RenamedEventArgs class which contains the following info, that did helped me:
    • FullPath - Gets the new fully qualifed path of the affected file or directory.
    • Name  -  Gets the new name of the affected file or directory.
    • OldName  -  Gets the old name of the affected file or directory.
    • OldFullPath - Gets the previous fully qualified path of the affected file or directory.

 

private FileSystemWatcher _fileMonitor;

private bool _wasChanged;
private bool _wasRenamed;
private bool _wasCreated;

public delegate void LoadContentCallback();

private void StartMonitorization()
{
	_fileMonitor = new FileSystemWatcher(Path.GetDirectoryName(_fileName), Path.GetFileName(_fileName))
		{
			NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
			IncludeSubdirectories = false
		};

	_fileMonitor.Changed += FileMonitor_OnChanged;
	_fileMonitor.Created += FileMonitor_OnChanged;
	_fileMonitor.Deleted += FileMonitor_OnDeleted;
	_fileMonitor.Renamed += FileMonitor_OnRenamed;

	_fileMonitor.EnableRaisingEvents = true;
}

private void FileMonitor_OnRenamed(object sender, RenamedEventArgs e)
{
	if (!_wasRenamed)
	{
		DialogResult result = MessageBox.Show("The file " + e.OldName + " was renamed. \r\n" +
											"Do you want to load the new file ?", "File was renamed", MessageBoxButtons.OKCancel);

		_wasRenamed = true;

		if (result == DialogResult.OK)
		{
			_fileName = e.FullPath;
			this.BeginInvoke(new LoadContentCallback(LoadFileContent));
		}
	}
	else
	{
		_wasRenamed = false;
	}
}

private void FileMonitor_OnDeleted(object sender, FileSystemEventArgs e)
{
	DialogResult result = MessageBox.Show("The file " + e.Name + " was deleted. \r\n" + "Do you want to remove it from text editor ?", "File was deleted",
MessageBoxButtons.YesNo);

	if (result == DialogResult.Yes)
	{
		_fileMonitor.Dispose();

		this.BeginInvoke(new LoadContentCallback(ResetUiContent));
	}

}

private void FileMonitor_OnChanged(object sender, FileSystemEventArgs e)
{
	if ((!_wasCreated && e.ChangeType == WatcherChangeTypes.Created) ||
		(!_wasChanged && e.ChangeType == WatcherChangeTypes.Changed))
	{
		DialogResult result = MessageBox.Show("The content of the file " + e.Name + " was changed. \r\n" + "Do you want to reload it ?", "File was changed",
MessageBoxButtons.OKCancel);

		if (e.ChangeType == WatcherChangeTypes.Changed)
		{
			_wasChanged = true;
		}
		else
		{
			_wasCreated = true;
		}

		if (result == DialogResult.OK)
		{
			this.BeginInvoke(new LoadContentCallback(LoadFileContent));
		}
	}
	else
	{
		if (e.ChangeType == WatcherChangeTypes.Changed && _wasChanged)
		{
			_wasChanged = false;
		}
		else if (e.ChangeType == WatcherChangeTypes.Created && _wasCreated)
		{
			_wasCreated = false;
		}
	}
}

private void ResetUiContent()
{
	txtFileContent.Text = string.Empty;
	lblFileName.Text = string.Empty;
	_fileName = string.Empty;
}
PUTTING Keyboard shortcuts on your buttons

This is I think really the easiest part. First you wire up your form with KeyDown event, make a event handler (in my case it was ViewFile_KeyDown handler). Then you need to set your form KeyPreview property on true, because otherwise it would not be able to catch keyboard events.

Than you just indicate what kind of combination will execute your action. For example I wanted my app to react at       Ctrl + S as a save action shortcut. In event handler I just indicate that I want it to react on Control + S for saving and Control + O for loading/opening the file.

this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ViewFile_KeyDown);
this.KeyPreview = true;

private void ViewFile_KeyDown(object sender, KeyEventArgs e)
{
	if (e.Modifiers == Keys.Control)
	{
		switch (e.KeyCode)
		{
			case Keys.S:
				btnSaveFile_Click(btnSaveFile, null);
				break;
			case Keys.O:
				btnLoadFile_Click(btnSaveFile, null);
				break;
		}
	}
}
New Features coming…

Now I think about new features that could be done on top of what already exists:

I also think about migrating this app on WPF to provide better visual experience. I think I will do it in the next 2 releases.

The Entire Sourcecode

Here you can see entire sourcecode file in collapsed way.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;

// Author : Poperecinii Timur
namespace ViewFile
{
    public partial class ViewFile : Form
    {
        private string _fileName;
        private FileSystemWatcher _fileMonitor;

        private bool _wasChanged;
        private bool _wasRenamed;
        private bool _wasCreated;

        public delegate void LoadContentCallback();

        public ViewFile()
        {
            InitObjects();

            InitializeComponent();
        }

        private void InitObjects()
        {
            _fileName = string.Empty;
        }

        private void btnLoadFile_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();

            ofd.CheckFileExists = true;
            ofd.Multiselect = false;

            DialogResult result = ofd.ShowDialog();

            if (result == DialogResult.OK)
            {
                _fileName = ofd.FileName;

                if (_fileName != string.Empty)
                    LoadFileContent();
            }
        }

        private void LoadFileContent()
        {
            lblFileName.Text = Path.GetFileName(_fileName);

            try
            {
                TextReader tr = new StreamReader(_fileName);
                try
                { txtFileContent.Text = tr.ReadToEnd(); }
                catch (Exception ex)
                { MessageBox.Show(ex.Message); }
                finally
                {
                    tr.Close();

                    StartMonitorization();
                }
            }
            catch (FileNotFoundException ex)
            { MessageBox.Show("Sorry, the file does not exist."); }
            catch (UnauthorizedAccessException ex)
            { MessageBox.Show("Sorry, you lack sufficient privileges."); }
            catch (Exception ex)
            { MessageBox.Show(ex.Message); }
        }

        private void StartMonitorization()
        {
            _fileMonitor = new FileSystemWatcher(Path.GetDirectoryName(_fileName), Path.GetFileName(_fileName))
                               {
                                   NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
                                   IncludeSubdirectories = false
                               };

            _fileMonitor.Changed += FileMonitor_OnChanged;
            _fileMonitor.Created += FileMonitor_OnChanged;
            _fileMonitor.Deleted += FileMonitor_OnDeleted;
            _fileMonitor.Renamed += FileMonitor_OnRenamed;

            _fileMonitor.EnableRaisingEvents = true;
        }

        private void FileMonitor_OnRenamed(object sender, RenamedEventArgs e)
        {
            if (!_wasRenamed)
            {
                DialogResult result = MessageBox.Show("The file " + e.OldName + " was renamed. \r\n" +
                                                    "Do you want to load the new file ?", "File was renamed", MessageBoxButtons.OKCancel);

                _wasRenamed = true;

                if (result == DialogResult.OK)
                {
                    _fileName = e.FullPath;
                    this.BeginInvoke(new LoadContentCallback(LoadFileContent));
                }
            }
            else
            {
                _wasRenamed = false;
            }

        }

        private void FileMonitor_OnDeleted(object sender, FileSystemEventArgs e)
        {
            DialogResult result = MessageBox.Show("The file " + e.Name + " was deleted. \r\n" +
                                                  "Do you want to remove it from text editor ?", "File was deleted",
                                                  MessageBoxButtons.YesNo);

            if (result == DialogResult.Yes)
            {
                _fileMonitor.Dispose();

                this.BeginInvoke(new LoadContentCallback(ResetUiContent));
            }

        }

        private void FileMonitor_OnChanged(object sender, FileSystemEventArgs e)
        {
            if ((!_wasCreated && e.ChangeType == WatcherChangeTypes.Created) ||
                (!_wasChanged && e.ChangeType == WatcherChangeTypes.Changed))
            {
                DialogResult result = MessageBox.Show("The content of the file " + e.Name + " was changed. \r\n" +
                                                      "Do you want to reload it ?", "File was changed",
                                                      MessageBoxButtons.OKCancel);

                if (e.ChangeType == WatcherChangeTypes.Changed)
                {
                    _wasChanged = true;
                }
                else
                {
                    _wasCreated = true;
                }

                if (result == DialogResult.OK)
                {
                    this.BeginInvoke(new LoadContentCallback(LoadFileContent));
                }
            }
            else
            {
                if (e.ChangeType == WatcherChangeTypes.Changed && _wasChanged)
                {
                    _wasChanged = false;
                }
                else if (e.ChangeType == WatcherChangeTypes.Created && _wasCreated)
                {
                    _wasCreated = false;
                }
            }
        }

        private void ResetUiContent()
        {
            txtFileContent.Text = string.Empty;
            lblFileName.Text = string.Empty;
            _fileName = string.Empty;
        }



        private void btnSaveFile_Click(object sender, EventArgs e)
        {
            SaveFileDialog saveFileDialog = new SaveFileDialog();
            saveFileDialog.Filter = "Text Files (*.txt)|*.txt|C# files (*.cs)|*.cs|All files (*.*)|*.*";

            saveFileDialog.SupportMultiDottedExtensions = true;

            DialogResult result = saveFileDialog.ShowDialog();

            if (result == DialogResult.OK)
            {
                _fileName = saveFileDialog.FileName;
                
                if (!string.IsNullOrEmpty(_fileName))
                    SaveFileContent();
            }
        }

        private void SaveFileContent()
        {
            lblFileName.Text = Path.GetFileName(_fileName);

            try
            {
                // we'll make only a textual file for instance
                TextWriter tw = File.CreateText(_fileName);

                try
                {
                    tw.Write(txtFileContent.Text);
                }
                catch (Exception ex)
                { MessageBox.Show(ex.Message); }
                finally
                {
                    tw.Close();

                    StartMonitorization();
                }
            }
            catch (UnauthorizedAccessException ex)
            { MessageBox.Show("Sorry, you lack sufficient privileges."); }
            catch (Exception ex)
            { MessageBox.Show(ex.Message); }
        }

        private void ViewFile_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Modifiers == Keys.Control)
            {
                switch (e.KeyCode)
                {
                    case Keys.S:
                        btnSaveFile_Click(btnSaveFile, null);
                        break;
                    case Keys.O:
                        btnLoadFile_Click(btnSaveFile, null);
                        break;
                }
            }
        }
    }
}