Files & Directories
It is common for desktop and enterprise applications to access the local machine file and directory structures. Applications often need to create, read to, write from, and delete files. Directory operations such as creation, deletion, and information processing are also routine. Fortunately, C# provides a rich set of I/O (input/output) classes that support file and directory operations. In this chapter we discuss a few of the more frequently utilized methods and properties of the IO classes.
The I/O classes in C# are supplied to support the conventional programming activity of working with files and directories. In the table below, the I/O classes that we cover in this chapter are listed. A full listing of the types contained in the System.IO namespace can be viewed here.
|Directory||Create, move, delete, etc. directories|
|DirectoryInfo||Obtain information about directories|
|File||Create, write to, read from, delete, etc. files|
|FileInfo||Obtain information about files|
Get started by creating the FileDirectoryIO project and code Program.cs as shown. On line 21 the ampersand character "@" is used to signal the compiler to consider the following string literally. That is, the string does not contain escape characters. So, the "C:\" will be considered literally. The string could also be written without the @ but the backslash would have to be escaped like this: "C:\\". The result of line 21 is that the string variable topDir will contain "C:\" plus the name entered by the user. The result of line 23 is that subdir will contain the the string in topDir from line 21 and the next-level directory name entered by the user. To make the path name correct, backslashes are added to separate the directory names. On line 25, the fileName is read from the user.
The try keyword on line 27 introduces a new construct called a try-catch block. The block begins on line 27 and concludes after the finally clause on line 71. The try-catch provides an effective means to "catch" exceptions (runtime errors) and handle them accordingly. Without error handling, runtime errors produce runtime exceptions which cause the program to crash for the user. See the unhandled exception message that appears when the program crashed due to an array index being out of range. However, the use of try-catch statements enable the programmer to "catch" exceptions as they occur and handle them more gracefully than an abrupt program crash.
Try-catch blocks are normally used to surround statements that are known to produce (a.k.a throw) exceptions. In Program.cs, the statements on lines 29, 31, 37, and 47 which perform directory and file operations could produce exceptions due to an incorrect format of the argument being passed (e.g. the filename) or insufficient access permissions. If errors like that occur, instead of crashing, the program flow will jump to the catch block on line 64 and output the error message.
The "e" parameter on line 64 is an error object that is created by the CLR when an error occurs. The e.Message property on line 66 contains the message text of the exception that was produced by the error. To see this in action, run the program and enter a ":" for the top-level directory name as shown. Notice the statement "The given path's format is not supported." in the output. That is the system-supplied content of the error message output from line 66. The exception was caught and did not crash from line 29 when the CreateDirectory() method was executed.
However, the program did crash due to the ":" entry on line 75 when the GetDirectories() method was called with an incorrect path format. Since this method was outside of a try-catch block, the exception was unhandled and produced the runtime message below. Notice that the "Additional information" is identical to the e.Message property output on line 66 as highlighted in the output window above.
There is one more note to cover about the try-catch process before continuing with the primary topic of the chapter. Notice the finally block from lines 68-71. Finally blocks will run irrespective of an exception being thrown. In other words, the output of the finally block "In the finally block" appears in the program output in the event of an error or if the program runs error free. Finally blocks are commonly used to contain "cleanup" code like closing file and database connections when complete which should be done under all circumstances (error or no error).
On line 29 the CreateDirectory() method of the Directory class is called to create a directory based on the path information supplied by the user. The Directory class is included in the System.IO namespace. Also, the CreateDirectory() method is specified as "static" within the System.IO namespace. This means that an object of the Directory class is not required to call the CreateDirectory() method. Note that no Directory object is instantiated prior to line 29, or anywhere else in the program. The CreateDirectory() method creates a directory (if one does not exist) based on the path supplied in the argument. Review the contents of the subDir variable on line 23.
Lines 31, 37, and 47 contain using statements. These using statements are different than the using directives on lines 1-6 which specify the inclusion of namespaces. The using statements provide a convenient means to ensure the disposal of unmanaged resources. Recall that most constructs in C# are managed by the CLR and garbage collection occurs automatically with managed objects. However, File objects are managed types that use file handles which are unmanaged resources.
Types like the File object that use unmanaged resources must implement the IDisposable interface (recall the interfaces chapter) to ensure the unmanaged resources are released correctly. The IDisposable interface is supplied by .NET as part of the FCL. The using statement is a shorthand syntax that provides the same functionality as enclosing the file operations within a try-catch block. If a try-catch block were used instead of a using statement, the Dispose() method of the IDisposable interface would be called in the finally block to ensure the file handle were released. The using statement takes care of that automatically. The code segment below is the alternative to the using statement on lines 31-35. Notice that if after use, the StreamWriter object is not null, the Dispose() method of the IDisposable interface is called to "clean-up" the no longer needed streamWriter resource (the unmanaged file handle).
When a file is opened for reading and/or writing, a stream is created. Stream is a term commonly applied to file I/O in other languages also. It means a "flow" of data from one point or source to another. On lines 31 and 47 a StreamWriter type object is created. A StreamWriter type object is required for writing to a file. A StreamReader is used for reading. The CreateText() method of the File class is called to create the file and to create a StreamWriter object which is used for writing to the file. On line 31, a fully qualified path and file name are supplied which will create the file with that name at that location (subDir).
On lines 33 and 34 the WriteLine() method of the streamWriter object is used to write the content to the file. On line 37, a streamReader object is created by calling the OpenText() method of the File class. On lines 41-44 the content of the file created on line 31 is written to the console using the streamReader object. The condition in the while loop on line 41 reads each line of the file into the fileString variable and until the end of the file (EOF) is reached at which point the ReadLine() method of the streamReader object will return null and the loop will terminate.
The CreateText() method on line 47 has the argument "localFile.txt" instead of the fully qualified path name and file name supplied on line 31. When only a file name is used as an argument like on line 47, the file is stored in the default location of bin/debug. See the File Explorer view below.
The content of localFile.txt that was written by line 49 is shown below. Using NotePad++ we can see the special characters in the file by selecting View | Show Symbol | Show All Characters. Notice the CRLF (carriage return line feed) at the end of the line in the file. The CRLF were inserted by the WriteLine() method on line 49. On the other hand, the Write() method does not add the CRLF to advance to the next line.
If the user enters the character "d" on line 54, the Delete() method of the Directory class deletes the topDir directory and all files and directories within it are removed. Recall that the Directory class is included in the System.IO namespace and that Delete() is static since an object is not required for its use. The second argument (true) is used to indicate that the directory and all subdirectories and files should also be deleted. If the second argument is not supplied or is false, then the directory will only be deleted if it is empty. On lines 61 the Exists() method of the Directory class is called to determine if the directory exists. If so, the method returns true. If not, the method returns false.
Lines 75-78 uses the GetDirectories() method of the Directory class to iteratively assign each directory names to the item string variable. Then, on line 77, the method DisplayFileDirectoryAttributes() is called. The argument in the method call is a new DirectoryInfo object. That object contains all directory information about the directory name that is stored in the variable item such as CreationTime, LastAccessTime, Parent, etc. The full list of properties and methods of the DirectoryInfo class can be viewed here.
When DisplayFileDirectoryAttributes() is called with the DirectoryInfo object, the object is assigned the local variable name of fileInfo which is in the parameter list on line 88. The method is written to process the information of both directories and files. The fileInfo object is of type FileSystemInfo which is a superclass of both DirectoryInfo and FileInfo. Therefore, the DirectoryInfo object that is passed to the method on line 75 is stored in an object of its parent type (FileInfo) on line 88.
On line 90, the itemType is assigned the default value of "File" which will be changed if the type is determined to be a directory. On line 92, the condition in the if statement is used to determine if the fileInfo object is a directory. If so, the itemType changes from the default of "File" to "Directory". The technique used to ascertain if the object is a directory is a little unusual. It uses an approach known as a bitwise operation. It is not required to under the details of the process for this course. However, let's cover the highlights.
The "&" operator on line 92 performs a bitwise AND operation using the current object (fileInfo) Attributes and the system definition of directory which is found in the Directory member of the FileAttributes enumeration. A simple comparison of fileInfo.Attributes with FileAttributes.Directory cannot be performed since fileInfo.Attributes can contain multiple values such as: Compressed, Directory, Encrypted, and Hidden. The FileAttributes.Directory is only one setting. The full list of members in the FileAttributes enumeration can be observed here.
FileAttributes is an enumeration of possible settings associated with files and directories that is provided as part of the System.IO namespace. It is used for comparison with the existing settings of items (files and directories) of the current item being evaluated. In essence, the condition on line 92 is simply asking if the item is a directory, if so the itemType is assigned "Directory", if not it retains the "File" value. In the output produced from line 96, the appropriate itemType (file or directory) will be sent to the console.
The use of the FileAttributes enumeration is demonstrated again on line 99. This time, it is used to determine if the item is hidden. For demonstration purposes, a hidden directory is included in the topLevelDir and a hidden file is in the nextLevelDir directory. Notice in the File Explorer windows the hidden items are dimmed. To make an item hidden using File Explorer, R-click | Properties | hidden.
The HiddenFile.txt file is hidden below. Also notice that both items are shown as hidden in the output window which is displayed below Program.cs.
The foreach loop to process the files on lines 80-83 is much like that for the directories on lines 75-78. The differences are: GetFiles, subDir, and FileInfo. The GetFiles() method returns files in the subDir directory. A FileInfo type object is created and passed to the DisplayFileDirectoryAttributes() method. In that method, the object will be recognized as a FileInfo object and the condition on line 92 will be false. Therefore, the default value of itemType will remain "File". Notice on line 97 that the FullName and CreationTime attributes are output to the console.
This is the output window displayed when running Program.cs.
In the next chapter, we begin working Windows Forms to produce graphical user interface applications.