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 22 the at 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 22 is that the string variable topDir will contain "C:\" plus the name entered by the user. The result of line 24 is that subdir will contain the string in topDir from line 22 and the next-level directory name entered by the user via the ReadLine(). To make the path name correct, backslashes are added to separate the directory names. On line 26, the fileName is read from the user.
The try keyword on line 28 introduces a new construct called a try-catch block. The block begins on line 28 and concludes after the finally clause on line 70. 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 30, 32, 38, and 48 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 66 and output the error message.
The "e" parameter on line 66 is an error object that is created by the CLR when an error occurs. The e.Message property on line 68 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 68. The exception was caught and did not crash from line 30 when the CreateDirectory() method was executed.
If we entered 'd' above and If we comment lines 75, 76, and 87, the program will crash due to the ":" entry on line 78 when the GetDirectories() method is called with an incorrect path format. Since this method is outside of a try-catch block, the exception is unhandled and produces the runtime message below. Notice that the Additional information is identical to the e.Message property output on line 68 as highlighted in the output window above. The Additional information notification may vary across versions of Visual Studio.
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 70-73. 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 the connections are no longer needed which should be done under all circumstances (error or no error).
On line 30 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 30, 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 24.
Lines 32, 38, and 49 contain using statements. These using statements are different than the using directives on lines 3-5 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 (e.g. streamWriter and streamReader) 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 32-36. 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 32 and 49 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 32, a fully qualified path and file name are supplied which will create the file with that name at that location (subDir).
On lines 34 and 35 the WriteLine() method of the streamWriter object is used to write the content to the file. On line 38, a streamReader object is created by calling the OpenText() method of the File class. On lines 43-46 the content of the file created on line 32 is written to the console using the streamReader object. The condition in the while loop on line 43 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 49 has the argument localFile.txt instead of the fully qualified path name and file name supplied on line 32. When only a file name is used as an argument like on line 49, the file is stored in the default location of bin/debug/... The .. represent the current version of .NET core. See the File Explorer view below.
The content of localFile.txt that was written by line 51 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 51. 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 56, 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 63 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 78-81 uses the GetDirectories() method of the Directory class to iteratively assign each directory names to the item string variable. Then, on line 80, 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 94. 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 78 is stored in an object of its parent type (FileInfo) on line 94.
On line 96, the itemType is assigned the default value of File which will be changed if the type is determined to be a directory. On line 98, 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 98 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 98 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 102, the appropriate itemType (file or directory) will be sent to the console.
The use of the FileAttributes enumeration is demonstrated again on line 105. 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 83-86 is much like that for the directories on lines 78-81. 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 98 will be false. Therefore, the default value of itemType will remain File. Notice on line 103 that the FullName and CreationTime attributes are output to the console.
Output FileDirectoryIO Directory Not Deleted
Output FileDirectoryIO Directory Deleted
In the next chapter, we begin working with Windows Forms to produce graphical user interface applications.