Title: Safely manage documents in C#
This is a pretty involved example that shows how to safely manage documents. The pieces are all simple but there are a lot of tightly integrated pieces.
This example lets you create, edit, open, and save files in text and Rich Text formats. It provides New, Open, Save, Save As, and Exit commands. The main purpose of the example is to coordinate all of those commands so the document is always safe.
For example, if you open a file, make changes, and then try to open another file, the program says there are unsaved changes asks if you want to save them. If you click Yes, the program either saves the file with its current file name or prompts you for a file name if it has not been saved before.
The example also updates the program's title bar to display the name of the currently loaded file (if it has a name--new files have no names until they are saved) and displays an asterisk if the loaded document has unsaved changes.
The program uses two key properties to ensure that changes to the loaded document are not lost and to update the program's title bar: FileName and DocumentChanged. The following code shows these two properties.
// The document's file name.
private string _FileName = "";
private string FileName
{
get { return _FileName; }
set
{
if (value == _FileName) return;
_FileName = value;
SetFormCaption();
}
}
// True if the document was changed since opening/creation.
private bool _DocumentChanged = false;
private bool DocumentChanged
{
get { return _DocumentChanged; }
set
{
if (value == _DocumentChanged) return;
_DocumentChanged = value;
SetFormCaption();
}
}
These properties use backing variables _FileName and _DocumentChanged to store their values. Each property's getter simply returns the value of the backing variable.
Each property's setter determines whether its value is changing and exits if it is not. It then saves the new value and calls the SetFormCaption method described next to update the form's title bar.
Note that the RichTextBox control has a Modified property that also tracks whether the document has been modified much as the DocumentChanged property does. The program uses its own property instead of the control's Modified property so that property can call SetFormCaption.
The following code shows the SetFormCaption method.
// Set the form's caption to indicate the file
// name and whether there are unsaved changes.
private void SetFormCaption()
{
string caption = "howto_save_as ";
// If there are changes, add an asterisk.
if (DocumentChanged) caption += "* ";
else caption += " ";
// Add the file name without its path.
if (FileName == "") caption += "[ ]";
else caption += "[" + Path.GetFileName(FileName) + "]";
// Display the result.
this.Text = caption;
}
This method simply composes the form's caption using the document's current file name (if it has one) and adding an asterisk if the document is modified.
That takes care of updating the form's caption. Ensuring the document's safety is a bit more involved but the pieces are still fairly simple. The key is the IsDocumentSafe method shown in the following code.
// Return true if it is safe to discard the current document.
private bool IsDocumentSafe()
{
// If there are no current changes to the document, it's safe.
if (!DocumentChanged) return true;
// See if the user wants to save the changes.
switch (MessageBox.Show(
"There are unsaved changes. Do you want to save the document?",
"Save Changes?", MessageBoxButtons.YesNoCancel))
{
case DialogResult.Yes:
// Save the changes.
SaveDocument();
// If the document still has unsaved changes,
// then we didn't save so the document is not safe.
return (!DocumentChanged);
case DialogResult.No:
// It's safe to lose the current changes.
return true;
default:
// Cancel. It's not safe to lose the changes.
return false;
}
}
This method returns true if it is safe to discard the current document.
First the method checks the DocumentChanged property and, if there are no unsaved changes, the method returns true. This happens if there have been no changes since the document was last loaded, saved, or created.
If there are unsaved changes, the method warns the user and asks if it should save the changes.
If the user clicks Yes, the program calls SaveDocument to try to save the changes. This might fail (you'll see why when you look at the SaveDocument method) so IsDocumentSafe returns true only if there are no unsaved changes after the call to SaveDocument returns.
If the user clicks No, then the user doesn't want to save the current changes so IsDocumentSafe returns true to indicate that the program can discard the changes.
If the user clicks Cancel, then the user decided not to do whatever made the program call IsDocumentSafe. This might have been creating a new document, opening an existing document, or exiting the program. (You'll see those calls to IsDocumentSafe shortly.) In that case, IsDocumentSafe returns false to tell the calling code that it is not safe to discard the current changes.
There are two ways the user might try to directly save the current document: by using the File menu's Save or Save As commands. The following code shows those menus' event handlers.
// Save using the current document name.
private void mnuFileSave_Click(object sender, EventArgs e)
{
SaveDocument();
}
// Save the document with a new name.
private void mnuFileSaveAs_Click(object sender, EventArgs e)
{
SaveDocumentAs();
}
These event handlers simply call the SaveDocument and SaveDocumentAs methods respectively.
The following code shows the SaveDocumentAs method.
// Attempt to save the document with a new file name.
private void SaveDocumentAs()
{
// Let the user pick the file in which to save.
if (sfdDocument.ShowDialog() == DialogResult.OK)
{
// Save using the selected file name.
SaveDocumentAs(sfdDocument.FileName);
}
}
This method displays a SaveFileDialog to the user to let the user select the file in which to save the document. If the user picks a file and clicks Save, the code calls an overloaded version of SaveDocumentAs that takes a file name as a parameter, passing that method the file name selected by the user. The following code shows the overloaded version of SaveDocumentAs.
// Save the document with the indicated name.
private void SaveDocumentAs(string file_name)
{
// Save with the indicated name.
try
{
// Save the file.
if (Path.GetExtension(file_name).ToLower() == ".txt")
{
// Save as a text file.
File.WriteAllText(file_name, rchDocument.Text);
}
else
{
// Save as an RTF file.
rchDocument.SaveFile(file_name);
}
// Update the document's file name.
FileName = file_name;
// There are no unsaved changed now.
DocumentChanged = false;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
This version of SaveDocumentAs is the only piece of code that actually saves a file. First it checks the file name's extension. If the extension is .txt, the code uses the File class's WriteAllText method to save the current document's text into the indicated file. If the extension is not .txt, the code uses the RichTextBox control's SaveFile method to save the control's contents in Rich Text format.
If the code successfully saves the file, it updates the FileName and DocumentChanged properties.
The other method that saves files is the following SaveDocument method.
// Attempt to save the document.
// If we don't have a file name, treat this as Save As.
private void SaveDocument()
{
// See if we have a file name.
if (FileName == "")
{
// We have no file name. Treat as Save As.
SaveDocumentAs();
}
else
{
// Save with the current name.
SaveDocumentAs(FileName);
}
}
This method checks the FileName property. If the program does not have a file name for the current document (because it created a new file and hasn't saved it yet), then the program calls SaveDocumentAs to let the user select the file in which to save the document. If the program does have a file name for the document, the method calls SaveDocumentAs passing it the current file name to make the program save the document in that file.
Those are the trickiest parts of the program. The rest of the code just calls those methods when appropriate.
The following code shows how the program creates a new file when the user selects the File menu's New command.
// Create a new document.
private void mnuFileNew_Click(object sender, EventArgs e)
{
// Make sure it's safe to discard the current document.
if (!IsDocumentSafe()) return;
// Clear the document.
rchDocument.Clear();
// There are no changes.
DocumentChanged = false;
// We have no file name yet.
FileName = "";
}
This code checks IsDocumentSafe and returns if it is not safe to discard the current changes. If it is safe to continue, the method clears the RichTextBox, sets DocumentChanged to false, and clears the file name.
The following code shows how the program opens a file when the user selects the File menu's Open command.
// Open an existing file.
private void mnuFileOpen_Click(object sender, EventArgs e)
{
// Make sure it's safe to discard the current document.
if (!IsDocumentSafe()) return;
// Let the user pick the file.
if (ofdDocument.ShowDialog() == DialogResult.OK)
{
// Open the file.
OpenFile(ofdDocument.FileName);
}
}
Like the code used to create a new document, this code checks IsDocumentSafe to see if it is safe to proceed. If it is safe, the code displays an OpenFileDialog to let the user pick the file. If the user picks a file and clicks OK, the code calls the OpenFile method passing it the name of the selected file. The following code shows the OpenFile method.
// Open the indicated file.
private void OpenFile(string file_name)
{
try
{
// Load the file.
if (Path.GetExtension(file_name).ToLower() == ".txt")
{
// Read as a text file.
rchDocument.Text = File.ReadAllText(file_name);
}
else
{
// Read as an RTF file.
rchDocument.LoadFile(file_name);
}
// There are no changes.
DocumentChanged = false;
// Save the file's name.
FileName = file_name;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The OpenFile method checks the file name's extension and tries to open either a text file or a Rich Text file accordingly. If the code successfully opens the file, it updates the DocumentChanged and FileName properties.
The following code shows the event handlers that execute when the user selects the File menu's Save, Save As, and Exit commands.
// Save using the current document name.
private void mnuFileSave_Click(object sender, EventArgs e)
{
SaveDocument();
}
// Save the document with a new name.
private void mnuFileSaveAs_Click(object sender, EventArgs e)
{
SaveDocumentAs();
}
// Exit. Just try to close.
private void mnuFileExit_Click(object sender, EventArgs e)
{
Close();
}
The Save and Save As commands call the SaveDocument and SaveDocumentAs methods. The Exit menu command simply tries to close the form. When the form tries to close, the following FormClosing event handler executes.
// If it's not safe to close, cancel the close.
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
e.Cancel = !IsDocumentSafe();
}
The FormClosing event handler just calls IsDocumentSafe and, if it is not safe to discard the current document, sets e.Cancel to true to make the form stay open.
The last two pieces of code are the RichTextBox control's TextChanged event handler and the form's Load event handler.
// Flag the document as changed.
private void rchDocument_TextChanged(object sender, EventArgs e)
{
DocumentChanged = true;
}
// Initially we have a new unchanged document.
private void Form1_Load(object sender, EventArgs e)
{
mnuFileNew_Click(null, null);
}
The TextChanged event handler sets DocumentChanged to true. The form's Load event handler calls the mnuFileNew_Click event handler to start a new document just as if the user had clicked the File menu's New command.
That's all there is to it! Simple isn't it? ;-) There are a lot of pieces because there are a lot of possible sequences of events, but individually each of the pieces is relatively straightforward.
A natural addition to this program would be to add toolbar buttons to let the user create, open, and save documents. You could also add an MRU (Most Recently Used) list.
Download the example to experiment with it and to see additional details.
|