Chapter 19

File Input Output

Key Skills & Concepts

Image   Writing and Reading Plain Text

Image   Writing and Reading Binary Data

Image   Reading Buffered Binary Data

Image   Randomly Accessing Binary Data


 

This chapter offers a simple introduction to writing and reading file content. The techniques presented vary depending on data format, accessibility, and performance requirements.

Writing and Reading Plain Text

It is likely you will want to read and write human-readable text from many of your applications. Your reasons for doing this could include tasks ranging from logging activity, initializing components of your application from a flat file, and managing simple but persistent data without a database, to creating or even reading spreadsheet data with a delimited format. C# provides the StreamWriter and StreamReader classes from the System.IO namespace to help.

NOTE      

Throughout the discussion you will notice several references to data streams. A stream is like a river that is connected to a data source. Data flows through the stream to the destination. The data may be converted to another format by the time it reaches its destination.

StreamWriter

The StreamWriter class offers methods to write text to a file. Files that are written in this manner can be read with a simple text editor such as Microsoft Notepad. To initialize the StreamWriter object, you can specify the file path with a constructor parameter, as shown next. You may also include a Boolean parameter in one of the constructor overloads to indicate whether you wish to append data to an existing file or overwrite the file contents. Whenever the StreamWriter object is created, if the file in the path specified does not exist, a new file is created.

image

NOTE      

By default, files are written to the same directory as the application executable. This default location is usually one directory above the bin\Debug directory of your project folder. The substring “../” within a file path means move up one directory.

The Write() method of the StreamWriter class outputs text while leaving the file pointer on the same line in the file. The WriteLine() method, on the other hand, outputs text and advances to a new line in the file. This example uses the WriteLine() and Write() methods to output content to a file:

image

When the previous instructions are implemented with a StreamWriter object named sw, the output from these instructions would appear in the file as

I say hello.
You say good-bye.

After writing output, calling the StreamWriter object’s Close() method releases the file so that it can be used by another process:

sw.Close();

Alternatively, and preferably, you can put the StreamWriter object declaration in a using statement. With a using block, you do not have to manually close the StreamWriter object. The StreamWriter instance is closed and the file is automatically released for you when the program exits the code block:

image



Example 19-1 Outputting to a Text File or Spreadsheet


This example demonstrates how to use the StreamWriter class to output comma-separated values to a file that can be read by a simple text editor or by a spreadsheet application such as Microsoft Excel. For this case, budget data is written in comma-separated format to a file named Budget.csv.

image

When running the application, a file named Budget.csv is written in the same directory where the code files of your project are located. This folder is the parent directory to the bin\Debug directory. The file generated contains readable text so it can be opened in Microsoft Notepad or any basic text editor. As well, since the file extension is .csv and the file contents are comma-separated values, you can view the contents in Microsoft Excel or any freeware spreadsheet application (see Figure 19-1).

image


Figure 19-1 Viewing .csv file contents in Microsoft Excel


 

StreamReader

Chances are, in addition to writing text to external files, you will want to be able to read text into your application from external files. The StreamReader class provides methods to do this, and they are easy to use. When creating a StreamReader object, the file location is specified as a string parameter of the constructor.

image

To help, the StreamReader object’s ReadLine() method reads a single line of text and advances the file pointer to the start of the next line. If desired, you can also use the Read() method to read one character at a time. The EndOfStream Boolean property indicates if the end of the file has been reached. This example shows how the reader might iterate through a file and read content one line at a time until the end of the file:

image

When finished reading from a text file, the StreamReader object’s Close() method must be called to release the file so it can be freed up for use in another process if needed:

sr.Close();

Similar to the StreamWriter class, you may consider placing the StreamReader object declaration inside a using statement instead. Once the program leaves the using block, the StreamReader object is closed and the file is released.



Example 19-2 Reading Text from a File


This example reads from the Budget.csv file that was created in Example 19-1. The reading is done line by line until the end of the file and each string is displayed in the console. The purpose of the example is to show that it is easy to load spreadsheet data that is stored in a comma-separated format. The comma-separated data is then split into an array, and each array value is also displayed in the console. To begin, create a new console application and copy the Budget.csv file to the directory where your source files are located in your new console application. Then implement this code:

image

The output shows each line of the file followed by data from the array:

image


 

Try This 19-1 Writing, Reading, and Extracting Text Data

This is an opportunity to see firsthand how easy it is to write and read text data, which can also be viewed using a spreadsheet application.

1. Starting with a new console application, write the following text content to a file called Sales.csv:

    Monthly Sales,Amount
August,23333.25
September,18323.22
October,13344.23

2. After writing, close your StreamWriter object to release the file.

3. Open the file for reading with the StreamReader class.

4. As you read each line, split the contents into an array.

5. Convert the numeric values to decimal objects and keep a cumulative sum.

6. Output the sum of the sales figures for August, September, and October once you calculate this value from the data that is read back into the application.

7. Check to ensure you can open the Sales.csv application with a spreadsheet application such as Microsoft Excel.


 

Writing and Reading Binary Data

Binary data is digitally encoded data. You cannot read this type of content with a simple text editor. However, the binary format is more machine friendly than raw text because it is more easily read, stored, transferred, and modified by your computer system. Also, since binary data is measured in bytes, the binary format offers significant flexibility for direct access to any portion of the data. In addition to these efficiencies gained, the binary format allows you to create your own proprietary file types, which you can optimize for performance and storage space. Let’s now look at how the libraries in the System.IO namespace can help you manage your binary content.

FileMode

Whenever a file is opened for reading or writing, the FileMode enumeration is used as a parameter to describe how the stream will be used. Table 19-1 describes the different FileMode enumerators.

image

Table 19-1 FileMode Enumerators

BinaryWriter

As shown next, a BinaryWriter object is initialized with a FileStream instance that is returned by the File.Open() method. The File.Open() method receives a file path and a FileMode enumerator as parameters. Once initialized, the BinaryWriter class automates the process of converting values of any simple data type to binary format while writing output.

image

Once again, when a BinaryWriter is declared inside a using statement, the program disposes of the object after leaving the code block. If the declaration is not inside a using statement, a call to the Close() method of the BinaryWriter object is required to release the file so that it can be used by another process:

writer.Close();

BinaryReader

For binary input, you can use the BinaryReader class. An object of this class is initialized with a FileStream object that is returned by the File.Open() method:

image

If you know the location and format of data segments in a file, the BinaryReader class offers several methods to read the binary data and convert it to the required type (see Table 19-2).

image

Table 19-2 BinaryReader Methods

In this sample, the ReadDecimal() method reads a binary value and converts it to a decimal object:

decimal balance = reader.ReadDecimal();

When leaving the using block, the BinaryReader object is disposed and the file is released so that it can be used by another process.



Example 19-3 Writing and Reading Binary Data Sequentially


To see how the classes work together with binary content, this example demonstrates how to sequentially write and read binary values of known simple types to and from a file:

image

image

When running the program, a binary file named MyBinaryFile.dat is created, data is stored in it, and then the contents of the file are read, converted back to a useable format, and displayed in the console:

True
A
1.1
2.2
3.3
4
Hello

This example creates the MyBinaryFile.dat file in the same directory as the source files. If you open the file using Notepad, you will notice that the contents of the file are in an unreadable binary format.


 

Try This 19-2 Writing and Reading Binary Data

To get some reassurance that the binary write and read techniques actually work properly, try this exercise.

1. Starting with Example 19-3, modify the code to write the first name “James” and last name “Bond” along with the identification number of 7 in binary format.

2. Adjust the code to read the content back in and to display it in the console window.


 

Reading Buffered Binary Data

When you need to retrieve large amounts of data, either from across a network or from a large file, you may risk overloading the system memory where the reading application resides. To efficiently read large amounts of data, you can retrieve this data in separate blocks. Each time a block is retrieved, it is loaded into a temporary buffer for storage while processing. Often, the buffer is treated as an array of raw bytes, which can be converted to a more useful format by your application. The Read() method of the FileStream class loads raw byte data into the buffer:

int Read(byte[] buffer, int bufferOffset, int totalBytesToRetrieve);

When separating your file into chunks for buffering, you can determine the file size, which is readily available from the FileStream object’s Length property:

int fileStream.Length;

With these structures in mind, a routine that reads buffered data into 4-byte chunks iteratively could look something like the following:

image

image



Example 19-4 Reading Binary Data from a Buffer


This example shows the full application that reads blocks of data into a buffer. To enable this example, a binary file containing integers is created. Segments of this binary file are read into a byte array, which is then converted into integers by the application for display in the console window.

image

image

When you run the program, you can see contents of the binary file in the console window after it is read and converted back to integer format:

1, 2, 3, 4, 5, 6, 7, 2, 1,


 

Randomly Accessing Binary Data

Sometimes you will want to read only a portion of a binary file, especially if the file is large. You can access the start position you need in the file with the Seek() method of the FileStream class, which moves the position of the file pointer. Seek() receives two parameters. The first Seek() parameter sets the offset in bytes for the file pointer. The second Seek() parameter receives the SeekOrigin enumeration as a parameter. The SeekOrigin enumeration sets the file pointer’s initial starting point before it is adjusted with the offset. SeekOrigin’s enumerated values range from SeekOrigin.Begin, which starts the read at the offset from the beginning, to SeekOrigin.Current, which begins reading at the offset from the current file position, to SeekOrigin.End, which starts reading at the negative offset from the end of the file. Your code to randomly access a section in the file could look like the following:

image



Example 19-5 Random Binary Access


This example demonstrates how to adjust the file pointer to directly access and read a specific subsection of a file. In this case, once the required changes are made, the application will begin reading data into a buffer, starting at the third number. The buffer is large enough to contain four integers. To build this current example, begin with the solution for Example 19-4 and replace the ReadBinaryData() method with this version:

image

Note that the starting offset is 8 bytes and that 16 bytes are read. Since each integer is 4 bytes long, this means that the third, fourth, fifth, and sixth integers are read. The output displayed when running the program verifies this:

3, 4, 5, 6,


 

 

Image Chapter 19 Self Test


The following questions are intended to help reinforce your comprehension of the concepts covered in this chapter. The answers can be found in the accompanying online Appendix B, “Answers to the Self Tests.”

1. Whenever you run the code in Example 19-1, the output is always the same. What change can you make so new content is added to the end of the existing file every time the application is run?

2. Write your name in a text editor and then save and close the file. Write a program to read the contents of the file and display it in the console window.

3. Explain in your own words why you receive an error if you revise Example 19-3 by changing the ReadDecimal() method to ReadInt32().

4. Modify Example 19-5 by adding one extra Seek() and Read() method instruction, but leave the existing Seek() and Read() instructions as they are. The final output after making this change should be

    3, 4, 5, 6, 2, 1,