In Hour 10, “Working with Arrays and Collections,” and Hour 13, “Understanding Query Expressions,” you learned how applications could work with data stored in collections and how to query and manipulate that data. Although these are common activities, many applications need to store or retrieve data from files on a disk.
The .NET Framework treats files as a stream of data. A stream is a sequential flow of packets of data, represented as bytes. Data streams have an underlying storage medium, typically called a backing store, which provides a source for the stream. Fortunately, the .NET Framework makes working with files and directories easier by the File
, Directory
, and Path
classes provided by the .NET Framework.
The System.IO
namespace contains all the classes you need to work with both buffered and unbuffered streams. Buffered streams enable the operating system to create its own internal buffer that it uses to read and write data in whatever increments are most efficient.
In this hour, you learn how to work with files, using the File
, Directory
, and Path
classes to explore and manage the file system and for reading and writing files. You also learn how you can use the Stream
class, or any of its derived classes, to perform more complex read and write operations.
You can think of a file as a sequence of bytes having a well-defined name and a persistent backing store. Files are manipulated through directory paths, disk storage, and file and directory names. The .NET Framework provides several classes in the System.IO
namespace that make working with files easy.
A path is a string that provides the location of a file or directory, and can contain either absolute or relative location information. An absolute path fully specifies a location, whereas a relative path specifies a partial location. When using relative paths, the current location is the starting point when locating the file specified.
Every process has a processwide “current location,” which is usually, but not always, the location where the process executable was loaded.
The Path
class provides static methods that perform operations on path strings in a cross-platform manner. Although most Path
class members don’t interact with the file system, they do validate that the specified path string contains valid characters. Table 14.1 shows the commonly used methods.
The Windows operating system includes many “special” folders frequently used by applications. Typically, the operating system sets these folders; however, a user can also explicitly set them when installing a version of Windows. As a result, many might not have the same location or name on any given machine.
These special directories are listed in the Environment.SpecialFolder
enumeration. Some of the common folders are shown in Table 14.2.
The enumeration simply provides a consistent way to reference these folders; to get the actual folder path for a given folder you should use the Environment.GetFolderPath
method. For example, to find the path to the user’s Documents directory, use the following code:
string path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
The DirectoryInfo
and FileInfo
classes both derive from the FileSystemInfo
class, which contains the methods common to file and directory manipulation and can represent either a file or a directory. When a FileSystemInfo
is instantiated, the directory or file information is cached, so you must refresh it using the Refresh
method to ensure current information.
The DirectoryInfo
class contains instance members that provide a number of properties and methods for performing common operations such as copying, moving, creating, and enumerating directories. The commonly used methods and properties of the DirectoryInfo
class are listed in Table 14.3.
Listing 14.1 shows how the DirectoryInfo
class might perform some common operations.
public class DirectoryInfoExample
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
DirectoryInfo directoryInfo = new DirectoryInfo(tempPath);
try
{
if (directoryInfo.Exists)
{
Console.WriteLine("The directory already exists.");
}
else
{
directoryInfo.Create();
Console.WriteLine("The directory was successfully created.");
directoryInfo.Delete();
Console.WriteLine("The directory was deleted.");
}
}
catch (IOException e)
{
Console.WriteLine("An error occurred: {0}", e.Message);
}
}
}
The FileInfo
class contains instance members that provide a number of properties and methods for performing common file operations such as copying, moving, creating, and opening files. The commonly used methods and properties are listed in Table 14.4.
Listing 14.2 shows how the FileInfo
class might perform some common operations.
public class FileInfoExample
{
public static void Main()
{
string tempFile = Path.GetTempFileName();
FileInfo fileInfo = new FileInfo(tempFile);
try
{
if (!fileInfo.Exists)
{
using (StreamWriter writer = fileInfo.CreateText())
{
writer.WriteLine("Line 1");
writer.WriteLine("Line 2");
}
}
fileInfo.CopyTo(Path.GetTempFileName());
fileInfo.Delete();
}
catch (IOException e)
{
Console.WriteLine("An error occurred: {0}", e.Message);
}
}
}
You should be sure to dispose of the stream when you finish using it by calling the Close
method. You can also wrap the streams in a using
statement, which is the preferred way to ensure the stream is closed correctly.
Because reading and writing data, whether it is text or binary, from files is a common task, the File
class provides several methods that make this even more convenient than working directly with streams.
To read or write binary data, you can use the ReadAllBytes
and WriteAllBytes
methods, respectively. These methods open the file, read or write the bytes, and then close the file. The code shown in Listing 14.8 performs the same actions as Listing 14.5 using the ReadAllBytes
and WriteAllBytes
methods.
public class BinaryReaderWriterFile
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exists(tempPath))
{
byte[] data = File.ReadAllBytes(tempPath);
File.WriteAllBytes(tempPath2, data);
}
}
}
Reading and writing text data is just as easy using the ReadAllLines
and ReadAllText
methods for reading and the WriteAllLines
and WriteAllText
methods for writing. The ReadAllLines
method reads all the lines from the file into a string array, where each line is a new element in the array, whereas the ReadAllText
reads all the lines into a single string.
The WriteAllLines
method writes each element of a string array to a file, whereas the WriteAllText
method writes the contents of a string to the file. Both of these create a new file or overwrite the file if it already exists. To append text to an existing file, you can use the AppendAllLines
or AppendAllText
methods. If you need to open a stream, you can use the AppendText
method.
The code shown in Listing 14.9 performs the same actions as Listing 14.7 using the ReadAllLines
and WriteAllLines
methods.
public class TextReaderWriterFile
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exists(tempPath))
{
string[] data = File.ReadAllLines(tempPath);
File.WriteAllLines(tempPath2, data);
}
}
}
The one drawback to using ReadAllLines
, or even ReadAllText
, is that the entire file must first be read into memory. To resolve this issue and return an IEnumerable<string>
collection, you can use the ReadLines
method. Because this method returns an IEnumerable<string>
, you can start to enumerate the returned collection immediately, before the whole collection is returned. The code shown in Listing 14.10 performs the same actions as Listing 14.9 using the File.ReadLines
method.
public class TextReaderWriterFile
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exists(tempPath))
{
File.WriteAllLines(tempPath, File.ReadLines(tempPath2));
}
}
}
In this hour, you learned how to work with streams to read and write text and binary files. Although you focused only on using the FileStream
and StreamWriter
classes, the mechanisms used for reading and writing using FileStream
is essentially the same for any Stream
derived class.
You also learned how the .NET Framework makes working with files, directories, and string paths simple through the File
, Directory
, FileInfo
, DirectoryInfo
, and Path
classes.
Q. What is a stream?
A. A stream is a sequential flow of packets of data represented as bytes. Data streams have an underlying storage medium, typically called a backing store, which provides a source for the stream.
Q. What is the difference between a relative path and an absolute path?
A. An absolute path fully specifies a location, whereas a relative path specifies a partial location. When using relative paths, the current location is the starting point when locating the file specified.
Q. What is the FileSystemInfo
class used for?
A. The FileSystemInfo
class contains methods common to file and directory manipulation and can represent either a file or directory. It is the base class for the DirectoryInfo
and FileInfo
classes.
Q. How is the Directory
class different from the DirectoryInfo
class?
A. The Directory
class provides only static methods, whereas the DirectoryInfo
class provides only instance methods and caches the information retrieved for the specified directory.
Q. What is the difference between a binary file and a text file?
A. A binary file is simply a stream of bytes, whereas a text file is known to contain only text data.
1. How do the Path
class members interact directly with the file system?
2. What method should be used on a FileSystemInfo
instance to update the cached information it contains?
3. What is the difference between the EnumerateDirectories
method on the DirectoryInfo
and Directory
classes?
4. What is the return type of the File.OpenRead
method?
5. What is the return type of the File.OpenText
method?
6. What is the difference between the File.ReadAllLines
method and the File.ReadLines
method?
1. Most Path
class members don’t interact with the file system; they do, however, validate that the specified path string contains valid characters.
2. The FileSystemInfo
class contains a Refresh
method that should be used to update the cached file or directory information.
3. The DirectoryInfo.EnumerateDirectories
returns an IEnumerable<DirectoryInfo>
, whereas Directory.EnumerateDirectories
returns an IEnumerable<string>
.
4. The File.OpenRead
method returns a FileStream
opened to the specified file for reading only.
5. The File.OpenText
method returns a StreamReader
opened to the specified text file.
6. File.ReadAllLines
must read the entire file into memory and returns a string array containing the lines whereas File.ReadLines
returns an IEnumerable<string>
enabling you to start enumerating the collection before the entire file is read.
1. Modify the PhotoCollection
class of the PhotoViewer
project by changing the data type of the path
field to be a DirectoryInfo
. Change the Path
property so that the get
accessor returns the value of the FullName
property and the set
accessor creates a new DirectoryInfo
instance from value
after validating that value
is not null
or an empty string. Also change the constructor so that it uses the set
accessor rather than setting the backing field directly.
2. Add a private method named Update
to the PhotoCollection
class and call it from the constructor right after the Path
property is set. This method should perform the following actions:
a. Clear the collection.
b. If path.Exists
is true
, enumerate over all files in the directory whose extension is “.jpg” and add a new Photo
instance to the collection. This code should be in a try-catch
block that catches a DirectoryNotFoundException
. The catch
handler should contain the following code:
System.Windows.MessageBox.Show("No Such Directory");
3.139.70.101