“Correct” Windows File Behaviour
Vector 12:1, page 129
Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 129
Introduction¶
I spent a few days earlier this year writing an odd function to handle file open and close requests in a Windows application that I was writing. The behaviour I had to implement is not quite as straightforward as it seems and the function that I used to implement it is a little weird. It should raise a ‘Disgusted of Cheltenham’ letter if nothing else.
One of the things that can really irk a user of an application is odd behaviour in the area of the file management interface. By odd I mean non-standard. If you are using ten applications under Windows, and nine of them behave in one way but the tenth behaves in another, then in the tenth you will make mistakes. The area where these mistakes can be most costly is that of file handling. For instance, when you close most applications you are asked if you wish to save any work that you have done since last saving. When I am using such an application I tend not to save my work explicitly before leaving, as I know that the application will prompt me to do so if I might need to. Inevitably when I am using an application without this helpful behaviour I still just double-click the system icon and lose all my unsaved work — bummer.
In order not to saddle my users with the same problem I spent some time working out exactly what my application should do in this situation. As a model I took Windows Write. It is supplied with Windows and might be considered to a certain extent definitive.
The interface to the file management process consists of five menu items under the main ‘File’ menu. They are ‘New’, ‘Open’, ‘Save’, ‘Save As’ and ‘Exit’. ‘Exit’ is considered part of the file management process for the reason given above.
New¶
The current data is cleared and the name is changed to ‘(untitled)’ or ‘Documentn’ or some such. However if the current data is unsaved the user is prompted to save it. If he chooses to save then the ‘Save’ process (see below) is performed before clearing the existing data.
Open¶
If the user has unsaved data he is prompted to save it as above. He then selects a file from disk and the contents are loaded into the system. The application file name is set to the name of the loaded file.
Save¶
The current data is saved under the current file name. If the name is ‘(untitled)’, or whatever the system uses to represent a new file, then the user is asked to choose a name as in ‘Save As’ below.
Save As¶
The user is prompted for a new file name. If the current file name was not generated by the system as a result of the ‘New’ option then it is given as the default. If the name chosen is that of an existing file then the user is warned that this is the case and is asked whether he wishes to overwrite this file or not. If he answers in the affirmative then the process continues as ‘Save’ above. If he answers ‘No’ then he is given the opportunity to select another file name.
Exit¶
If the current data is unsaved then the user is prompted as in ‘New’ and ‘Open’ above. The application then terminates although strictly this is not part of the file management process.
In each of the above processes, if the user cancels when asked a question or asked to select a file (for ‘Open’ or ‘Save As’) either by pressing the Cancel button or by pressing Escape, then the whole process is terminated.
These five options share a great deal of behaviour and in the case of ‘Save’ and ‘Save As’ each may be a subroutine of the other depending on circumstance. I found it easier to think about the problem by dividing the processes into the individual actions they perform and the questions that they ask. These are, in no particular order:
Questions¶
Has the current data changed since the last save, is the current file ‘dirty’?
Does the user wish to save the changed data?
Is the current data associated with a file name or is it ‘(untitled)’?
Does the name chosen in the ‘Save As’ process represent an existing file?
Does the user wish to overwrite the existing file?
Actions¶
Clear the data
Open a file
Get a file name for ‘Open’
Get a file name for ‘Save’
Save the data to a named file
Each of these actions or questions can occur in two ways. Either it is specified initially by the user selecting a menu option, or it is made necessary by the answer to a question or result of an action. For example "Get a file name for ‘Save’" can occur as a result of the user selecting "Save As" or because the "Is current data named?" returned "No".
The extra actions and questions that result from a particular condition do not depend on what actions need to be performed subsequently, so it is possible to think of the process as a form of an execution stack and this was how I implemented it. I decided that the behaviour was sufficiently horrible that I wanted to restrict it to one function. The function has a number of blocks corresponding to the actions and questions above. Each block consists of a label naming the block, some block specific code and some code to manage the transfer between blocks.
The way that the application developer specifies the set of blocks to execute in a call of the function, is in the argument to the function. This is a set of block names which when executed resolves to the line numbers of the block labels. They are stored in a variable path, to the end of which we append the line number of a special ‘Ok’ block, and we branch to path which takes us to the first block specified. At the beginning of each block the first element of path is removed (as it is no longer needed) and at the end of a block the new value of path is branched to. If, as a result of executing the block-specific code, it becomes necessary to execute more blocks, then the labels are simply added to the head of path. In this way when the extra blocks have been dealt with execution will continue as if nothing had happened. In the case of the user cancelling the operation then a special ‘Cancel’ block is executed which changes the result to 0 and adds 0 to the head of path, causing the function to terminate at the end of the block. If all specified blocks are executed without the user cancelling the operation then the ‘Ok’ block is executed which sets the result to 1 and adds 0 to the head of path in the same way as the ‘Cancel’ block.
As a consequence of each question, and depending on the success or failure of each action, the execution path is changed to include all necessary new behaviour. In this way the argument to the management function need only contain the minimum set of actions that the function should perform and the normal Windows file management behaviour can be obtained by specifying the following simple values of path.
| New | 'Dirty,NewFile' |
| Open | 'Dirty,OpenName,OpenFile' |
| Save | 'Named,SaveFile' |
| Save As | 'SaveName,Exists,SaveFile' |
| Exit | 'Dirty' |
However, if the user had selected the option to open a file when the current data was unsaved and the file untitled, and furthermore he had chosen to save his data under the name of an existing file and declined to overwrite it before selecting a different name, then the execution path would be:
Blocks¶
Dirty¶
If the file is dirty execute the block AskSave
AskSave¶
Use a "Yes, No, Cancel" dialogue box to ask whether the user wishes to save the current changes. If the answer is yes then execute the blocks Named and SaveFile. If the user cancelled the dialogue box execute the Cancel block.
Named¶
If the file name represents a new file then execute the blocks SaveName and Exists.
SaveName¶
Use the standard file dialogue box to get a file name under which to save the file. If the user cancels the dialogue box then execute the Cancel block.
Exists¶
If the file name corresponds to an existing file execute the AskReplace block.
AskReplace¶
Use the "Yes, No, Cancel" dialogue box to ask whether to overwrite the file or not. If the user specifies that the file should not be overwritten then execute the blocks SaveName and Exists to allow the user to choose another name. If the user cancels the dialogue then execute the Cancel block. Otherwise, continue execution at the next block in the stack (which will be SaveFile).
SaveFile¶
Save the current data to a file using the current name or the name specified by the SaveName block if it has been executed. If an error occurs in the operation then inform the user and execute the Cancel block.
OpenName¶
If the function was called with a left argument then use that as the new name. Else use the standard file dialogue to get the name of a file to open. If the dialogue is cancelled then execute the Cancel block.
OpenFile¶
Get the data from the named file into the application. If an error occurs in the operation then inform the user and execute the Cancel block.
NewFile¶
Clear the current data from the application then continue executing the next block in line.
Cancel¶
Set the result of the function to 0 then leave the function.
Ok¶
Set the result of the function to 1 then leave the function.
Listing¶
∇ r←newname File path;name;res
[1] name←∆file
[2] →path←(⍎path),Ok
[3]
[4] Dirty:path←1↓path
[5] path←(Is_File_Dirty/AskSave),path
[6] →path
[7]
[8] AskSave:path←1↓path
[9] res←Win_YNC name'This data has changed.' '' 'Save current changes?'
[10] path←((⎕io+res)⊃⍬(Named SaveFile)Cancel),path
[11] →path
[12]
[13] Named:path←1↓path
[14] path←((~Is_File_Named name)/SaveName Exists),path
[15] →path
[16]
[17] SaveName:path←1↓path
[18] res name←Get_Save_File_Name
[19] path←res↓Cancel,path
[20] →path
[21]
[22] Exists:path←1↓path
[23] path←((Does_File_Exist name)/AskReplace),path
[24] →path
[25]
[26] AskReplace:path←1↓path
[27] res←Win_YNC name'File already exists.' '' 'Replace existing file?'
[28] path←(res⊃(SaveName Exists)⍬ Cancel),path
[29] →path
[30]
[31] SaveFile:path←1↓path
[32] :if Save_Data_To_File name
[33] ∆file←name
[34] :else
[35] path←Cancel,path
[36] :endif
[37] →path
[38]
[39] OpenName:path←1↓path
[40] :if 2=⎕NC'newname'
[41] res name←1 newname
[42] :else
[43] res name←Get_Open_File_Name
[44] :endIf
[45] path←res↓Cancel,path
[46] →path
[47]
[48] OpenFile:path←1↓path
[49] :if Get_Data_From_File name
[50] ∆file←name
[51] :else
[52] path←Cancel,path
[53] :endif
[54] →path
[55]
[56] NewFile:path←1↓path
[57] Clear_Data
[58] →path
[59]
[60] Cancel:path←1↓path
[61] r←0
[62] path←0,path
[63] →path
[64]
[65] Ok:path←1↓path
[66] r←1
[67] path←0,path
[68] →path
∇
Arguments and Effects¶
path is a text vector indicating the tasks to perform. It is a set of label names separated by spaces or commas such that when executed within this function it will resolve to a vector of line numbers. During the execution of the function, and depending on circumstances, various line numbers may be catenated to the beginning of this list. It is in effect an execution stack.
∆file is global and is the name of the current file. It may be changed by the function.
The result r is 0 if the user cancelled at any stage, otherwise 1.
If the new name is known (if for example it came from a list of most recently used files) then it is passed as the left argument newname.
Sub-Functions¶
The following functions are used in the function File with the described effects.
| Is_File_Dirty | returns 1 if the application data has changed since the last save, else 0. |
| Win_YNC | shows the standard Yes, No, Cancel message box and returns 1 0 or 2 respectively. |
| Is_File_Named | returns 1 if its argument represents a file name, else 0. |
| Get_Save_File_Name | using the standard file dialogue box asks the user for a file name and returns the chosen name. The first element of the result is 0 if the user cancelled the transaction, else 1. |
| Does_File_Name_Exist | returns 1 if its argument refers to an existing file, else 0. |
| Save_Data_To_File | saves the application data to the file named by its argument and returns 1 for success or 0 for failure. It handles the error dialogue on failure. |
| Get_Open_File_Name | using the standard file dialogue box asks the user for a file name and returns the chosen name. The first element of the result is 0 if the user cancelled the transaction, else 1. |
| Get_Data_From_File | loads the file named by its argument into the application and returns 1 for success or 0 for failure. It handles the error dialogue on failure. |
| Clear_Data | clears the current data from the application and sets ∆file to represent a new file. |
Many of these (such as Does_File_Name_Exist) are quite generic; the majority are completely application-dependent, and there is the occasional borderline case such as the code in the AskSave block where it might be necessary to have more specific wording in the message. The next challenge is to make a Causeway object out of this interesting collection of code so that it can safely and easily be used from any application that must guarantee to provide the accepted file management interface.