DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 11.8 Create, Write to, and Read from a File

Problem

You need to create a file—possibly for logging information to or storing temporary information—and then write information to it. You also need to be able to read the information that you wrote to this file.

Solution

To create, write to, and read from a log file, we will use the FileStream and its reader and writer classes. For example, we will create methods to allow construction, reading to, and writing from a log file. To create a log file, you can use the following code:

public FileStream CreateLogFile(string logFileName)
{
    FileStream fileStream = new FileStream(logFileName, 
                                   FileMode.Create, 
                                   FileAccess.ReadWrite, 
                                   FileShare.None);
    return (fileStream);
}

To write text to this file, you can create a StreamWriter object wrapper around the previously created FileStream object (fileStream). You can then use the WriteLine method of the StreamWriter object. The following code writes three lines to the file: a string, followed by an integer, followed by a second string:

public void WriteToLog(FileStream fileStream, string data)
{
    // make sure we can write to this stream
    if(!fileStream.CanWrite)
    {
        // close it and reopen for append
        string fileName = fileStream.Name;
        fileStream.Close( );
        fileStream = new FileStream(fileName,FileMode.Append);
    }
    StreamWriter streamWriter = new StreamWriter(fileStream);
    streamWriter.WriteLine(data);
    streamWriter.Close( ); 
}

Now that the file has been created and data has been written to it, you can read the data from this file. To read text from a file, create a StreamReader object wrapper around the file. If the code had not closed the FileStream object (fileStream), it could use that object in place of the filename used to create the StreamReader. To read the entire file in as a single string, use the ReadToEnd method:

public string ReadAllLog(FileStream fileStream)
{
    if(!fileStream.CanRead)
    {
        // close it and reopen for read
        string fileName = fileStream.Name;
        fileStream.Close( );
        fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read);
    }
    StreamReader streamReader = new StreamReader(fileStream);
    string contents = streamReader.ReadToEnd( );
    streamReader.Close( );
    return contents; 
}

If you need to read the lines in one by one, use the Peek method, as shown in ReadLogPeeking, or the ReadLine method, as shown in ReadLogByLines:

public static void ReadLogPeeking(FileStream fileStream)
{
    if(!fileStream.CanRead)
    {
        // close it and reopen for read
        string fileName = fileStream.Name;
        fileStream.Close( );
        fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read);
    }
    Console.WriteLine("Reading file stream peeking at next line:");
    StreamReader streamReader = new StreamReader(fileStream);
    while (streamReader.Peek( ) != -1)
    {
        Console.WriteLine(streamReader.ReadLine( ));
    }    
    streamReader.Close( );
}

or:

public static void ReadLogByLines(FileStream fileStream)
{
    if(!fileStream.CanRead)
    {
        // close it and reopen for read
        string fileName = fileStream.Name;
        fileStream.Close( );
        fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read);
    }
    Console.WriteLine("Reading file stream as lines:");
    StreamReader streamReader = new StreamReader(fileStream);
    string text = "";
    while ((text = streamReader.ReadLine( )) != null)
    {
        Console.WriteLine(text);
    }
    streamReader.Close( );
}

If you need to read in each character of the file as a byte value, use the Read method, which returns a byte value:

public static void ReadAllLogAsBytes(FileStream fileStream)
{
    if(!fileStream.CanRead)
    {
        // close it and reopen for read
        string fileName = fileStream.Name;
        fileStream.Close( );
        fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read);
    }
    Console.WriteLine("Reading file stream as bytes:");
    StreamReader streamReader = new StreamReader(fileStream);
    while (streamReader.Peek( ) != -1)
    {
        Console.Write(streamReader.Read( ));
    }
    streamReader.Close( );
}

This method displays numeric byte values instead of the characters that they represent. For example, if the log file contained the following text:

This is the first line.
100
This is the third line.

it would be displayed by the ReadAllLogAsBytes method, as follows:

841041051153210511532116104101321021051141151163210810
511010146131049484813108410410511532105115321161041013
211610410511410032108105110101461310

If you need to read in the file by chunks, create and fill a buffer of an arbitrary length based on your performance needs. This buffer can then be displayed or manipulated as needed:

public static void ReadAllBufferedLog (FileStream fileStream)
{
    if(!fileStream.CanRead)
    {
        // close it and reopen for read
        string fileName = fileStream.Name;
        fileStream.Close( );
        fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read);
    }
    Console.WriteLine("Reading file stream as buffers of bytes:");
    StreamReader streamReader = new StreamReader(fileStream);
    while (streamReader.Peek( ) != -1)
    {
        char[] buffer = new char[10];
        int bufferFillSize = streamReader.Read(buffer, 0, 10);
        foreach (char c in buffer)
        {
            Console.Write(c);
        }
        Console.WriteLine(bufferFillSize);
    }
    streamReader.Close( );
}

This method displays the log file's characters in 10-character chunks, followed by the number of characters actually read. For example, if the log file contained the following text:

This is the first line.
100
This is the third line.

it would be displayed by the ReadAllBufferedLog method as follows:

This is th10
e first li10
ne.
100
10
This is th10
e third li10
ne.
     5

Notice that at the end of every tenth character (the buffer is a char array of size 10), the number of characters read in is displayed. During the last read performed, only five characters were left to read from the file. In this case, a 5 is displayed at the end of the text, indicating that the buffer was not completely filled.

The previous code could have been modified to use the ReadBlock method as shown in the ReadAllBufferedLogBlock method instead of the Read method. The output is the same in both cases:

public static void ReadAllBufferedLogBlock(FileStream fileStream)
{
    if(!fileStream.CanRead)
    {
        // close it and reopen for read
        string fileName = fileStream.Name;
        fileStream.Close( );
        fileStream = new FileStream(fileName,FileMode.Open,FileAccess.Read);
    }
    Console.WriteLine("Reading file stream as buffers of bytes using ReadBlock:");
    StreamReader streamReader = new StreamReader(fileStream);
    while (streamReader.Peek( ) != -1)
    {
        char[] buffer = new char[10];
        int bufferFillSize = streamReader.ReadBlock(buffer, 0, 10);
        foreach (char c in buffer)
        {
            Console.Write(c);
        }
        Console.WriteLine(bufferFillSize);
    }
    streamReader.Close( );
}

This displays the following text:

This is th10
e first li10
ne.
100
10
This is th10
e third li10
ne.
     5

Discussion

There are many mechanisms for recording state information about applications, other than creating a file full of the information. One example of this type of mechanism is the Windows Event Log, where informational, security, and error states can be logged during an application's progress. One of the primary reasons for creating a log file is to assist in troubleshooting or to debug your code in the field. If you are shipping code without some sort of debugging mechanism for your support staff (or possibly for you in a small company), we suggest you consider adding some logging support. Any developer who has spent a late night debugging a problem on a QA machine, or worse yet, at a customer site, can tell you the value of a log of the program's actions.

If you are writing character information to a file, the simplest method is to use the Write and WriteLine methods of the StreamWriter class to write data to the file. These two methods are overloaded to handle any of the primitive values (except for the byte data type), as well as character arrays. These methods are also overloaded to handle various formatting techniques discussed in Chapter 1. All of this information is written to the file as character text, not as the underlying primitive type.

If you need to write byte data to a file, consider using the Write and WriteByte methods of the FileStream class. These methods are designed to write byte values to a file. The WriteByte method accepts a single byte value and writes it to the file, after which the pointer to the current position in the file is advanced to the next value after this byte. The Write method accepts an array of bytes that can be written to the file, after which the pointer to the current position in the file is advanced to the next value after this array of bytes. The Write method can also choose a range of bytes in the array in which to write to the file.

The Write method of the BinaryWriter class overloaded similarly to the Write method of the StreamWriter class. The main difference is that the BinaryWriter class's Write method does not allow formatting. This allows the BinaryReader to read the information written by the BinaryWriter as its underlying type, not as a character or a byte. See Recipe 11.18 for an example of the BinaryReader and BinaryWriter classes in action.

Once we have the data written to the file, we can read it back out. The first concern when reading data from a file is not to go past the end of the file. The StreamReader class provides a Peek method that looks—but does not retrieve—the next character in the file. If the end of the file has been reached, a -1 is returned. Likewise, the Read method of this class also returns a -1 if it has reached the end of the file. The Peek and Read methods can be used in the following manner to make sure that you do not go past the end of the file:

StreamReader streamReader = new StreamReader("data.txt");
while (streamReader.Peek( ) != -1)
{
    Console.WriteLine(streamReader.ReadLine( ));
}
streamReader.Close( );

or:

StreamReader streamReader = new StreamReader("data.txt");
string text = "";
while ((text = streamReader.Read( )) != -1)
{
    Console.WriteLine(text);
}
streamReader.Close( );

The main differences between the Read and Peek methods are that the Read method actually retrieves the next character and increments the pointer to the current position in the file by one character, and the Read method is overloaded to return an array of characters instead of just one. If the Read method is used that returns an array buffer of characters and the buffer is larger than the file, the extra elements in the buffer array are set to an empty string.

The StreamReader also contains a method to read an entire line up to and including the newline character. This method is called ReadLine. This method returns a null if it goes past the end of the file. The ReadLine method can be used in the following manner to make sure that you do not go past the end of the file:

StreamReader streamReader = new StreamReader("data.txt");
string text = "";
while ((text = streamReader.ReadLine( )) != null)
{
    Console.WriteLine(text);
}
streamReader.Close( );

If you simply need to read the whole file in at one time, use the ReadToEnd method to read the entire file in to a string. If the current position in the file is moved to a point other than the beginning of the file, the ReadToEnd method returns a string of characters starting at that position in the file and ending at the end of the file.

The FileStream class contains two methods, Read and ReadByte, which read one or more bytes of the file. The Read method reads a byte value from the file and casts that byte to an int before returning the value. If you are explicitly expecting a byte value, consider casting the return type to a byte value:

FileStream fileStream = new FileStream("data.txt", FileMode.Open);
byte retVal = (byte) fileStream.ReadByte( );

However, if retVal is being used to determine whether the end of the file has been reached (i.e., retVal == -1 or retVal == 0xffffffff in hexadecimal), you will run into problems. When the return value of ReadByte is cast to a byte, a -1 is cast to 0xff, which is not equal to -1 but is equal to 255 (the byte data type is not signed). If you are going to cast this return type to a byte value, you cannot use this value to determine whether you are at the end of the file. You must instead rely on the Length Property. The following code block shows the use of the return value of the ReadByte method to determine when we are at the end of the file:

FileStream fileStream = new FileStream("data.txt", FileMode.Open);
int retByte = 0;
while ((retByte = fileStream.ReadByte( )) != -1)
{
    Console.WriteLine((byte)retByte);
}
fileStream.Close( );

This code block shows the use of the Length property to determine when to stop reading the file:

FileStream fileStream = new FileStream("data.txt", FileMode.Open);
long currPosition = 0;
while (currPosition < fileStream.Length)
{
    Console.WriteLine((byte) fileStream.ReadByte( ));
    currPosition++;
}
fileStream.Close( );

The BinaryReader class contains several methods for reading specific primitive types, including character arrays and byte arrays. These methods can be used to read specific data types from a file. Recipe 11.18 contains more on this topic. All of these methods, except for the Read method, indicate that the end of the file has been reached by throwing the EndOfStreamException. The Read method will return a -1 if it is trying to read past the end of the file. This class contains a PeekChar method that is very similar to the Peek method in the StreamReader class. The PeekChar method is used as follows:

FileStream fileStream = new FileStream("data.txt", FileMode.Open);
BinaryReader binaryReader = new BinaryReader(fileStream);
while (binaryReader.PeekChar( ) != -1)
{
    Console.WriteLine(binaryReader.ReadChar( ));
}
binaryReader.Close( );

In this code, the PeekChar method is used to determine when to stop reading values in the file. This will prevent a costly EndOfStreamException from being thrown by the ReadChar method if it tries to read past the end of the file.

See Also

See the "FileStream Class," "StreamReader Class," "StreamWriter Class," "Binary-Reader Class," and "BinaryWriter Class" topics in the MSDN documentation.

    [ Team LiB ] Previous Section Next Section