Introduction
How irritated you will be when you are working on an application for a long time, and the application crashes. You forget to do File->Save or ‘Ctrl + S’, and data is completely lost. The efforts you have put in have gone in vain. Maybe next time, you’ll be more cautious and keep pressing ‘ctrl + s’ after a certain interval of time. Maybe you can follow this a few times, and forget to do this thereafter.
An application crash can happen for different reasons such as power went off, a bug in the application that might bring down the application or malicious virus. You can take care of the power or virus, but not the bug till you get a patch/new version.
Background
There are applications having a feature of Autosave wherein the work is saved at a certain interval. If an application crashes, a user is prompted for loading the autosaved work. Microsoft Word has this built in and can be configured as shown below. It can be accessed through the office button in the left top corner -> Word Options -> Save tab.
Another example is Microsoft Outlook and Gmail saving emails after a certain interval.
I am going to write a simple paint application that saves the design at a certain interval. It’s developed using WPF and C#. The application is just to understand the concept of Autosave and doesn’t include the advanced features present in Microsoft Word. However, it can be extended to create such a feature. I’ll try to keep it simple.
Autosave Paint Application
List of features in the application is as follows:
- New – Clears the design
- Save – Saves the working design to the specified location
- Open – Opens a selected Ink serialized format (*.ink) file
- Exit – Exits the application
- Autosave – Saves the design at a specified interval to a hidden location in the background
- Recovery – Loads the last auto saved file during startup after application restarted followed by application crash
Let’s start and create the application.
Code
- Open Visual Studio -> New Project -> Other Project Types -> Visual Studio Solutions -> Blank Solution. Give it a name as “
AutosaveAndRecovery
”. - Add a new WPF project “
AutosavePaint
”. - Replace MainWindow.xaml and MainWindow.xaml.cs with below XAML and .cs file.
MainWindow.xaml
<Window x:Class="AutosavePaint.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Menu HorizontalAlignment="Left" Height="38" VerticalAlignment="Top"
Width="517" RenderTransformOrigin="0.32,-0.079">
<MenuItem Header="File">
<MenuItem Command="ApplicationCommands.New">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.New"
Executed="New"
CanExecute="CanNew"/>
</MenuItem.CommandBindings>
</MenuItem>
<MenuItem Header="Open" Command="ApplicationCommands.Open"
HorizontalAlignment="Left" Width="157.507"
RenderTransformOrigin="0.502,0.502" Height="26" Margin="0,0,-13.17,0">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.Open"
Executed="Open"
CanExecute="CanOpen"/>
</MenuItem.CommandBindings>
</MenuItem>
<MenuItem Header="Save" Margin="0,0,12,0" Command="ApplicationCommands.Save"
Height="25" RenderTransformOrigin="0.552,0.482">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.Save"
Executed="Save"
CanExecute="CanSave"/>
</MenuItem.CommandBindings>
</MenuItem>
<MenuItem Header="Exit" Command="ApplicationCommands.Close">
<MenuItem.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close"
Executed="Exit"
CanExecute="CanExit"/>
</MenuItem.CommandBindings>
</MenuItem>
</MenuItem>
</Menu>
<InkCanvas Name="inkCanvas" HorizontalAlignment="Left" Height="229"
Margin="10,43,0,0" VerticalAlignment="Top" Width="497" Background="#FF9C9898"/>
<StatusBar x:Name="statusBar" Height="33" Margin="10,277,10,0"
VerticalAlignment="Top" Background="#FFD4CFCF">
<StatusBarItem x:Name="statusBarItem" Content="StatusBarItem"
Height="33" VerticalAlignment="Top"/>
</StatusBar>
</Grid>
</Window>
MainWindow.xaml.cs
We have structured the application without functionality. Run the application and ensure it's launched fine. I have used InkCanvas
for drawing. Move the mouse over the central gray area.
Now, we will add the features mentioned above one by one.
1. New: Clears the Design
Add the below code to clear the canvas and on a press of New, design should be cleared.
public void New(object sender, ExecutedRoutedEventArgs e)
{
inkCanvas.Strokes.Clear();
statusBarItem.Content = "Ready";
}
2. Save: Saves the Working Design to the Specified Location
Add the below code to save a design to a file.
public void Save(object sender, ExecutedRoutedEventArgs e)
{
var saveFileDialog = new SaveFileDialog();
saveFileDialog.Filter = "Ink Serialized Format (*.isf)|*.isf|";
if (saveFileDialog.ShowDialog() == true)
{
Save(saveFileDialog.FileName);
}
}
private bool Save(string fileName)
{
FileStream fs = null;
try
{
fs = File.Open(fileName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (fs != null)
fs.Close();
}
return true;
}
The design will be saved in a file with extension *.ink. The design can be saved in bitmap format as well. I haven’t included it in order to keep code simple and the main intention of this article is to demonstrate Autosave and recovery. Here is an excellent article on Paint
application using InkCanvas
by Sacha Barber.
3. Open: Opens a Selected Ink Serialized Format (*.ink) File
Add the below code to open a *.ink file and resolve the references.
public void Open(object sender, ExecutedRoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.CheckFileExists = true;
openFileDialog.Filter = "Ink Serialized Format (*.isf)|*.isf|" +
"All files (*.*)|*.*";
if (openFileDialog.ShowDialog(this) == true)
{
string fileName = openFileDialog.FileName;
if (!fileName.ToLower().EndsWith(".isf"))
{
MessageBox.Show("The requested file is not a Ink Serialized Format
file\r\n\r\nplease retry", Title);
}
else
{
if (Open(fileName))
{
statusBarItem.Content = "Loaded";
}
else
{
statusBarItem.Content = "An error occured while opening the file";
}
}
}
}
private bool Open(string fileName)
{
FileStream fs = null;
try
{
this.inkCanvas.Strokes.Clear();
fs = new FileStream(fileName, FileMode.Open);
inkCanvas.Strokes = new System.Windows.Ink.StrokeCollection(fs);
}
catch (Exception)
{
return false;
}
finally
{
if (fs != null)
fs.Close();
}
return true;
}
4. Exit: Exit the Application
Add the below code to exit the application:
public void Exit(object sender, ExecutedRoutedEventArgs e)
{
this.Close();
}
5. Autosave: Saves the Design at a Specified Interval to a Hidden Location in the Background
In order to implement Autosave, we need to consider the below things.
- Location – Location of autosaved files. We’ll hide it from the user.
- Background operation - Save operation has to be performed in the background in the worker thread so that application remains responsive.
- Autosave interval – This determines the interval between two consecutive save operation.
Let’s define the location first. I have used the executing assembly location and created Autosave directory in it. Also, the directory needs to be hidden. Two helper methods, MakeDirectoryHidden(…)
and MakeFileHidden(…)
have been added. Refer to the downloaded code for implementation.
private readonly string _backupFilePath;
private string _backupFile;
private const string BACKUP_FILE_NAME = "PaintAppBackup.bk";
public MainWindow()
{
InitializeComponent();
statusBarItem.Content = "Ready";
_backupFilePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) +
"\\" + BACKUP_DIRECTORY;
_backupFile = _backupFilePath + "\\" + BACKUP_FILE_NAME;
if (!Directory.Exists(_backupFilePath))
{
Directory.CreateDirectory(_backupFilePath);
MakeDirectoryHidden(_backupFilePath);
}
}
Run the application and observe that Autosave directory will be created in the ..\\ AutosaveAndRecovery\AutosavePaint\bin\Debug\Autosave if the application is running in the debug mode.
In order to save the design in the background, I have used BackgroundWorker
.
Two methods have been added to achieve it.
private void OnSaveAsyncCompletion(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
statusBarItem.Content = "An error occured while saving the design";
DeleteBackupFile();
}
else
{
statusBarItem.Content = "Saved";
MakeFileHidden(_backupFile);
}
}
Now, the last thing is to call SaveAync(…)
periodically at a certain interval. I have chosen 20 seconds as the saving interval. In order to call it periodically, I have used DispatcherTimer
. The DispatcherTimer
calls below method periodically.
void PeriodicSave(object o, EventArgs e)
{
statusBarItem.Content = "Saving";
if (!Directory.Exists(_backupFilePath))
{
Directory.CreateDirectory(_backupFilePath);
MakeDirectoryHidden(_backupFilePath);
}
DeleteBackupFile();
SaveAsync(_backupFile);
}
In order to start/stop the Dispatcher
, Window’s Loaded
and Closing
events have been added.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_dispatcherTimer = new DispatcherTimer();
_dispatcherTimer.Interval = TimeSpan.FromMilliseconds(AUTOSAVE_INTERVAL_SECONDS * 1000);
_dispatcherTimer.Tick += PeriodicSave;
_dispatcherTimer.Start();
statusBarItem.Content = "Ready";
}
private void Window_Closing(object sender, CancelEventArgs e)
{
if (_dispatcherTimer != null)
{
_dispatcherTimer.Stop();
_dispatcherTimer = null;
}
}
Run the application and observe an error occurred while saving the design after 20 seconds. The exception raised is a cross-thread exception. We are trying to access the UI component (InkCanvas
) from a background thread. To overcome marshal UI access call to UI thread as shown below.
private bool Save(string fileName)
{
FileStream fs = null;
try
{
fs = File.Open(fileName, FileMode.Create);
ExecuteOnUIThread(() => inkCanvas.Strokes.Save(fs)); <<ç=
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (fs != null)
fs.Close();
}
return true;
}
private void ExecuteOnUIThread(Action action)
{
var dispatcher = Application.Current.Dispatcher;
if (dispatcher != null)
{
dispatcher.Invoke(action);
}
else
{
action();
}
}
Run the application, sketch something and after 20 seconds, the file will be saved.
6. Recovery: Load the Last Autosaved File During Startup After Application Restarted Followed by an Application Crash
This is the last feature and autosave is useless if we can’t bring the last autosaved file back. The below flowchart depicts the recovery mechanism.
A method CheckAndLoadBackupFile(…)
does the recovery. It’s called in the loaded
event as soon as the application starts. Also, DeleteBackupFile(…)
is called in the closing
event of the application.
Application after design being auto-saved.
Conclusion
Autosave and recovery are nice features to have in an application that makes users' life comfortable. I have just given an introduction on how to implement it. It can be extended with advanced features. It can be used to save designs, sketches or forms data into a file/database. Any application providing options for Open/Save is a prime candidate for this kind of application.
Comments and suggestions are Welcome!
References