(This is part 3 of the Paint-by-Numbers series)
There are four issues I want to deal with in this post: New, Open, Save (As), and Exit. I’ll be doing these in order, but all of them depend on knowing whether or not the application is dirty, so I’ll deal with that problem first. Opening and saving files isn’t a particularly hard problem once you know how to deal with streams, but knowing when to notify users about potential data loss when opening files (or creating new ones, or closing an application) involves some logic, and that’s where understanding “dirtiness” comes into play.
First, however, I’m going to deal with a problem that Bill McC brought up with my last post (see the comments attached to my last post for details). I was essentially abusing List Of() to be a stack, when Stack Of() already exists. This illustrates why it’s really important to (a) have a solid plan that you either stick to or re-examine thoroughly if you decide to change course and (b) make sure that you get someone to review your code. None of the code that I write for these blogs gets reviewed by anyone else — it’s just stuff that I find time to do in the cracks of my schedule – but of course for production code in Visual Studio, every line gets scrutinized deeply by a senior developer (or two, when we get closer to shipping) for just this reason. In my case, my original plan was to append the undo units to a list, which would be fine, but I changed my mind halfway through the coding of undo/redo to inserting them at the front instead without careful consideration of the performance hit that would create.
Changing this to use stacks instead of lists was very trivial:
– I replaced all “List Of(…)“ with “Stack Of(…)”
– I replaced all “Insert(0,…)” with “Push(…)”
– I replaced all “Remove(0,…)” with “Pop(…)”
– I replaced all “Item(0)” with “Peek()”
Functionality-wise, that’s all that needs to be done and then everything will work the way it did before, albeit in a more performant manner, but I also made the following changes:
– I right-clicked on the UndoList and RedoList variables and used the “rename” command to change them to “UndoStack” and “RedoStack.” This is not a functional change, but I like my variable names to match my types.
– I eliminated an unnecessary temporary variable in the Undo and Redo commands by nesting the calls to the stack, so that now Undo is:
If CanUndo() Then
DoActions(UndoStack.Peek(), True) ‘ Do all the actions inverted (erase becomes draw and vice-versa)
RedoStack.Push(UndoStack.Pop()) ‘ Now it’s a redo
and similarly for Redo:
If CanRedo() Then
DoActions(RedoStack.Peek(), False) ‘ Do all the actions properly (erase is erase, draw is draw)
UndoStack.Push(RedoStack.Pop()) ‘ Now it’s an undo
(Keen eyes will note that I now do the Actions before moving the action item… that’s not strictly necessary, but is more aesthetically pleasing to me.)
Those changes took about three minutes total and result in a much more performant and elegant application.
Now, on to the main topic for today – loading and saving.
The Dirty Bit
Most applications have something called a “dirty bit” which gets set when an action happens in the document, and if you try to close the application while it’s dirty, you get prompted to save. There are expedient ways to implement this, and smart ways which take a little more work:
(1) Do nothing. That is, whenever the user saves, you always overwrite the file regardless of whether or not anything has changed, and you always prompt the user to save when closing the application regardless of the applications state. I don’t know about you, but applications like this always annoy the heck out of me.
(2) Create a Boolean value which gets set whenever any action happens, and gets cleared once a save is committed – check this value to see whether or not a save is needed. This is better, and a lot of applications do this. However, if you support undo/redo, then the simple act of doing an undo followed by a redo after a save will result in a “dirty” app, even though the document is identical to the copy on disk.
(3) Compare the top of the undo stack with a known “last action,” and use that as the “dirty” flag. Thus, an undo followed by a redo gets you back to the same state, with no unnecessary prompt to save. It involves slightly more logic, but it gives the truest indication as to whether or not anything has really changed.
I’m going to implement option (3). To do this, first I need to cache the last pen stroke before the save:
Dim LastSavedAction As Stack(Of GridAction) = Nothing
and then check the value of that against the top of the Undo stack (or against Nothing if the UndoStack is empty):
Private Function IsDirty() As Boolean
If Me.UndoStack.Count = 0 Then
Return LastSavedAction IsNot Nothing
Return LastSavedAction IsNot Me.UndoStack.Peek()
If the document is not dirty, then the top of the undo stack will have to be whatever we cached it to be at the last save; otherwise, it’s dirty. (When starting up, the Undo stack will be empty and the LastSavedAction will be Nothing, so that combination should indicate that we’re not dirty as well.)
We’ll add a helper to disable the Save menu item:
Private Sub UpdateSaveMenuItem()
Me.SaveToolStripMenuItem.Enabled = IsDirty()
which we can call anytime something interesting happens – at the end of the handlers for FormLoad, MouseUp, Undo, Redo, and of course New, Open, Save and Save As. I’ll go ahead & call the UpdteSaveMenuItem helper function from the first four of those – I’ll do the others later when I write those routines.
Opening up the form editor, I’ll drop-down the “File” menu and double-click on “New” – this will create the appropriate handler for me. Now, all New needs to really do is just reset everything to the way it was when the application was started, but first it needs to check to see if the user really wants to discard any changes since the last save. In my resource editor (right-click “Project,” choose “Properties,” and then navigate to the Resources tab), I’ll add a string called MSG_SaveFirst which says “Do you want to save first?” and I’ll add a helper called ContinueOperation to prompt the user with this string (Cancel will cancel the operation, Yes will save the puzzle before proceeding, and No will just proceed with the operation). Then, the methods look like:
Private Function ContinueOperation() As Boolean
If IsDirty() Then
Dim result As MsgBoxResult = MsgBox(My.Resources.MSG_SaveFirst, MsgBoxStyle.YesNoCancel)
If result = MsgBoxResult.Cancel Then
Return False ‘ User decided not to create a new document after all
ElseIf result = MsgBoxResult.Yes Then
If String.IsNullOrEmpty(saveFileName) Then
Private Sub NewToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
If Not ContinueOperation() Then
Return ‘User elected to Cancel
‘ Now, reset everything to its pristine state:
For i As Integer = 0 To Rows – 1
For j As Integer = 0 To Columns – 1
Grid(i, j) = 0