Reading And Writing To Files


Reading and writing to files is in principle very simple; however, it is not done through the DirectoryInfo or FileInfo objects. Instead, using the .NET Framework 2.0, you can now do it through the File object. Later in this chapter, you see how to accomplish this through the use of a number of other classes that represent a generic concept called a stream.

Before the .NET Framework 2.0, it took a bit of wrangling to read and write to files. It was possible using the available classes from the framework, but it wasn't really that straightforward. The .NET Framework2.0 has expanded upon the File class to make it as simple as just one line of code to read or write to a file.

Reading a File

For an example of reading a file, create a Windows Form application that contains a regular text box, a button, and a multi-line text box. In the end, your form should appear something like Figure 34-6.

image from book
Figure 34-6

The idea of this form is that the end user will enter in the path of a specific file in the first text box and click the Read button. From here, the application will read the specified file and display the file's contents in the multi-lined text box. This is illustrated in the following code example:

 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.IO; namespace ReadingFiles { partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { textBox2.Text = File.ReadAll(textBox1.Text); } } } 

In building this example, the first step is to add the using statement to bring in the System.IO namespace. From there, simply use the button1_Click event for the Send button on the form to populate the text box with what comes back from the file. You can now get at the file's contents by using the File.ReadAll method. Now with the .NET Framework 2.0, you can read files with a single statement. The ReadAll method opens the specified file, reads the contents, and then closes the file. The return value of the ReadAll method is a string containing the entire contents of the file specified. The end result would be something similar to what is shown in Figure 34-7.

image from book
Figure 34-7

The File.ReadAll signature shown in the preceding example is of the following construction:

File.ReadAll(FilePath);

The other option is to also specify the encoding of the file being read:

File.ReadAll(FilePath, Encoding);

Using this signature allows you to specify the encoding to use when opening and reading the contents of the file. So this means that you could do something like the following:

 File.ReadAll(textBox1.Text, Encoding.ASCII); 

Some of the other options for opening and working with files include using the ReadAllBytes and the ReadAllLines methods. The ReadAllBytes method allows you to open up a binary file and read the contents into a byte array. The ReadAll method shown earlier gives you the entire contents of the specified file in a single string instance. This might not be something that you are interested in. You might instead be interested in working with what comes back from the file in a line-by-line fashion. In this case, you will want to use the ReadAllLines method because it will allow for this kind of functionality.

Writing to a File

Besides making reading from files an extremely simple process under the .NET Framework 2.0 umbrella, writing to files is just as easy. Just as the Base Class Library (BCL) gives you the ReadAll, ReadAllLines, and ReadAllBytes methods to read files in a couple of different ways, to write to files the BCL gives you the WriteAll, WriteAllBytes, and WriteAllLines methods.

For an example of how to write to a file, use the same Windows Form application, but instead, use the multi-line text box in the form to input data into a file. The code for the button1_Click event handler should appear as shown here:

 private void button1_Click(object sender, EventArgs e) { File.WriteAll(textBox1.Text, textBox2.Text); } 

Build and start the form, type C:\Testing.txt in the first text box, type some random content in the second text box, and then click the button. Nothing will happen visually, but if you look in your root C drive, you will see the Testing.txt file with the content you specified.

The WriteAll method went to the specified location, created a new text file, and provided the file the specified contents before saving and closing the file. Not bad for just one line of code!

If you run the application again, and specify the same file (Testing.txt) but with some new content, pressing the button again will cause the application to go out and perform the same task again. This time though, as it is important to note, the new content is not added to the previous content you specified, but instead, the new content completely overrides the previous content. In fact, the WriteAll, WriteAllBytes, and WriteAllLines all override any previous files, so you have to be careful when using these methods.

The WriteAll method in the previous example is using the following signature:

File.WriteAll(FilePath, Content)

You can also specify the encoding of the new file:

File.WriteAll(FilePath, Content, Encoding)

The WriteAllBytes method allows you to write content to a file using a byte array and the WriteAllLines method allows you to write a string array to a file. An example of this is illustrated in the following event handler:

 private void button1_Click(object sender, EventArgs e) { string[] movies =  {"Grease", "Close Encounters of the Third Kind", "The Day After Tomorrow"}; File.WriteAllLines("C:\Testing.txt", movies); } 

Now clicking the button for such an application will give you a Testing.txt file with the following contents:

Grease Close Encounters of the Third Kind  The Day After Tomorrow

The WriteAllLines method writes out the string array with each array item taking its own line in the file.

Because of the fact that data may not only be written to disk, but to other places as well (such as named pipes or to memory), it is also important to understand how to deal with File I/O in .NET using streams as a means of moving file contents around. This is shown in the following section.

Streams

The idea of a stream has been around for a very long time. A stream is an object used to transfer data. The data can be transferred in one of two directions:

  • If the data is being transferred from some outside source into your program, it is called reading from the stream.

  • If the data is being transferred from your program to some outside source, it is called writing to the stream.

Very often, the outside source will be a file, but that is not necessarily the case. Other possibilities include

  • Reading or writing data on the network using some network protocol, where the intention is for this data to be picked up by or sent from another computer

  • Reading or writing to a named pipe

  • Reading or writing to an area of memory

Of these examples, Microsoft has supplied a .NET base class for writing to or reading from memory, System.IO.MemoryStream. System.Net.Sockets.NetworkStream handles network data. There are no base stream classes for writing to or reading from pipes, but there is a generic stream class, System.IO.Stream, from which you would inherit if you wanted to write such a class. Stream does not make any assumptions about the nature of the external data source.

The outside source might even be a variable within your own code. This might sound paradoxical, but the technique of using streams to transmit data between variables can be a useful trick for converting data between data types. The C language used something like this to convert between integer data types and strings or to format strings using a function, sprintf.

The advantage of having a separate object for the transfer of data, rather than using the FileInfo or DirectoryInfo classes to do this, is that by separating the concept of transferring data from the particular data source, it makes it easier to swap data sources. Stream objects themselves contain a lot of generic code that concerns the movement of data between outside sources and variables in your code, and by keeping this code separate from any concept of a particular data source, you make it easier for this code to be reused (through inheritance) in different circumstances. For example, the StringReader and StringWriter classes are part of the same inheritance tree as two classes that you will be using later on to read and write text files, StreamReader and StreamWriter. The classes will almost certainly share a substantial amount of code behind the scenes.

Figure 34-8 illustrates the actual hierarchy of stream-related classes in the System.IO namespace.

image from book
Figure 34-8

As far as reading and writing files is concerned, the classes that concern us most are:

  • FileStream — This class is intended for reading and writing binary data in a binary file. However, you can also use it to read from or write to any file.

  • StreamReader and StreamWriter — These classes are designed specifically for reading from and writing to text files.

You might also find the BinaryReader and BinaryWriter classes useful, although they are not used in the examples here. These classes do not actually implement streams themselves, but they are able to provide wrappers around other stream objects. BinaryReader and BinaryWriter provide extra formatting of binary data, which allows you to directly read or write the contents of C# variables to the relevant stream. Think of the BinaryReader and BinaryWriter as sitting between the stream and your code, providing extra formatting (see Figure 34-9).

image from book
Figure 34-9

The difference between using these classes and directly using the underlying stream objects is that a basic stream works in bytes. For example, suppose as part of the process of saving some document you want to write the contents of a variable of type long to a binary file. Each long occupies 8 bytes, and if you used an ordinary binary stream you would have to explicitly write each of those 8 bytes of memory. In C# code that would mean you'd have to perform some bitwise operations to extract each of those 8 bytes from the long value. Using a BinaryWriter instance, you can encapsulate the entire operation in an overload of the BinaryWriter.Write() method that takes a long as a parameter, and which will place those 8 bytes into the stream (and hence if the stream is directed to a file, into the file). A corresponding BinaryReader.Read() method will extract 8 bytes from the stream and recover the value of the long.

For more information on the BinaryReader and BinaryWriter classes refer to the SDK documentation.

Buffered Streams

For performance reasons, when you read or write to a file, the output is buffered. This means that if your program asks for the next 2 bytes of a file stream, and the stream passes the request on to Windows, then Windows will not go through the trouble of connecting to the file system and then locating and reading the file off the disk, just to get 2 bytes. Instead, Windows will retrieve a large block of the file in one go, and store this block in an area of memory known as a buffer. Subsequent requests for data from the stream will be satisfied from the buffer until the buffer runs out, at which point Windows grabs another block of data from the file. Writing to files works in the same way. For files this is done automatically by the operating system, but you might have to write a stream class to read from some other device that isn't buffered. If so, you can derive your class from BufferedStream, which implements a buffer itself.(Note, however, that BufferedStream is not designed for the situation in which an application frequently alternates between reading and writing data.)

Reading and Writing to Binary Files Using FileStream

Reading and writing to binary files can be done using the FileStream class (note that if you are working with the .NET Framework 1.x, this will most likely be the case).

The FileStream class

A FileStream instance is used to read or write data to or from a file. In order to construct a FileStream, you need four pieces of information:

  • The file you want to access.

  • The mode, which indicates how you want to open the file. For example, are you intending to create a new file or open an existing file? And if you are opening an existing file, should any write operations be interpreted as overwriting the contents of the file or appending to the file?

  • The access, which indicates how you want access to file. For example, do you want to read or write to the file or do both?

  • The share access, which specifies whether you want exclusive access to the file. Or, are you willing for other streams to be able to access this file simultaneously? If so, should other streams have access to read the file, to write to it, or to do both?

The first of these pieces of information is usually represented by a string that contains the full pathname of the file, and this chapter only considers those constructors that require a string here. Besides those constructors, however, there are some additional ones that take an old Windows-API-style Windows handle to a file instead. The remaining three pieces of information are represented by three .NET enumerations called FileMode, FileAccess, and FileShare. The values of these enumerations are listed in the following table; they should be self-explanatory.

Enumeration

Values

FileMode

Append, Create, CreateNew, Open, OpenOrCreate, or Truncate

FileAccess

Read, ReadWrite, or Write

FileShare

Inheritable, None, Read, ReadWrite, or Write

Note that in the case of FileMode, exceptions can be thrown if you request a mode that is inconsistent with the existing status of the file. Append, Open, and Truncate will throw an exception if the file does not already exist, and CreateNew will throw an exception if it does. Create and OpenOrCreate will cope with either scenario, but Create will delete any existing file to replace it with a new, initially empty, one. The FileAccess and FileShare enumerations are bitwise flags, so values can be combined with the C# bitwise OR operator, |.

There are a large number of constructors for the FileStream. The three simplest ones work as follows:

 // creates file with read-write access and allows other streams read access FileStream fs = new FileStream(@"C:\C# Projects\Project.doc",  FileMode.Create); // as above, but we only get write access to the file FileStream fs2 = new FileStream(@"C:\C# Projects\Project2.doc",  FileMode.Create, FileAccess.Write); // as above but other streams don't get  // fs3 is open FileStream fs3 = new FileStream(@"C:\C# Projects\Project3.doc",  FileMode.Create, FileAccess.Write, FileShare.None); 

As this code reveals, the overloads of these constructors have the effect of providing default values of FileAccess.ReadWrite and FileShare.Read to the third and fourth parameters. It is also possible to create a file stream from a FileInfo instance in various ways:

 FileInfo myFile4 = new FileInfo(@"C:\C# Projects\Project4.doc"); FileStream fs4 = myFile4.OpenRead(); FileInfo myFile5= new FileInfo(@"C:\C# Projects\Project5doc"); FileStream fs5 = myFile5.OpenWrite(); FileInfo myFile6= new FileInfo(@"C:\C# Projects\Project6doc"); FileStream fs6 = myFile6.Open(FileMode.Append, FileAccess.Write,  FileShare.None); FileInfo myFile7 = new FileInfo(@"C:\C# Projects\Project7.doc"); FileStream fs7 = myFile7.Create(); 

FileInfo.OpenRead() supplies a stream that gives you read-only access to an existing file, whereas FileInfo.OpenWrite() gives you read-write access. FileInfo.Open() allows you to specify the mode, access, and file share parameters explicitly.

Of course, after you've finished with a stream, you should close it:

 fs.Close(); 

Closing the stream frees up the resources associated with it, and allows other applications to set up streams to the same file. In between opening and closing the stream, you'll want to read data from it and/or write data to it. FileStream implements a number of methods to do this.

ReadByte() is the simplest way of reading data. It grabs one byte from the stream, and casts the result to an int having a value between 0 and 255. If you have reached the end of the stream, it returns -1:

 int NextByte = fs.ReadByte(); 

If you prefer to read a number of bytes at a time, you can call the Read() method, which reads a specified number of bytes into an array. Read() returns the number of bytes actually read — if this value is zero, you know you're at the end of the stream. Here's an example where you read into a byte array called ByteArray:

 int nBytesRead = fs.Read(ByteArray, 0, nBytes); 

The second parameter to Read() is an offset, which you can use to request that the Read operation starts populating the array at some element other than the first, and the third parameter is the number of bytes to read into the array.

If you want to write data to a file, two parallel methods are available, WriteByte() and Write(). WriteByte() writes a single byte to the stream:

 byte NextByte = 100;  fs.WriteByte(NextByte); 

Write(), on the other hand, writes out an array of bytes. For instance, if you initialized the ByteArray mentioned before with some values, you could use the following code to write out the first nBytes of the array:

 fs.Write(ByteArray, 0, nBytes); 

As with Read(), the second parameter allows you to start writing from some point other than the beginning of the array. Both WriteByte() and Write() return void.

In addition to these methods, FileStream implements various other methods and properties to do with bookkeeping tasks such as determining how many bytes are in the stream, locking the stream, or flushing the buffer. These other methods aren't usually required for basic reading and writing, and if you need them, full details are in the SDK documentation.

Example: BinaryFileReader

The use of the FileStream class is illustrated by writing an example, BinaryFileReader, which reads in and displays any file. Create the project in Visual Studio 2005 as a Windows application. It has one menu item, which brings up a standard OpenFileDialog asking what file to read in, then displays the file as binary code. As you are reading in binary files, you need to be able to display non-printable characters. You will do this is by displaying each byte of the file individually, showing 16 bytes on each line of a multi-line text box. If the byte represents a printable ASCII character, you'll display that character; otherwise, you'll display the value of the byte in a hexadecimal format. In either case, you pad out the displayed text with spaces so that each byte displayed occupies four columns so the bytes line up nicely under each other.

Figure 34-10 shows what the BinaryFileReader application looks like when viewing a text file (because BinaryFileReader can view any file, it's quite possible to use it on text files as well as binary ones). In this case, the application has read in a basic ASP.NET page (.aspx).

image from book
Figure 34-10

Clearly, this format is more suited to looking at the values of individual bytes rather than displaying text! Later in this chapter, you develop an example that is specifically designed to read text files; then you will be able to see what this file really says. On the other hand, the advantage of this example is that you can look at the contents of any file.

This example won't demonstrate writing to files because you don't want to get bogged down in the complexities of trying to translate the contents of a text box like the one shown in Figure 34-10 into a binary stream! You see how to write to files later on when you develop an example that can read or write, but only to text files.

Here's at the code used to get these results. First, you need to make sure that you have brought in the System.IO namespace through the use of the using statement:

 using System.IO; 

Next, you add a couple of fields to the main form class — one representing the file dialog and a string that gives the path of the file currently being viewed:

partial class Form1 : Form { private OpenFileDialog chooseOpenFileDialog = new OpenFileDialog(); private string chosenFile; 

You also need to add some standard Windows Forms code to deal with the handlers for the menu and the file dialog:

public Form1() {    InitializeComponent(); menuFileOpen.Click += new EventHandler(OnFileOpen); chooseOpenFileDialog.FileOk += new  CancelEventHandler(OnOpenFileDialogOK); } void OnFileOpen(object Sender, EventArgs e) { chooseOpenFileDialog.ShowDialog(); } void OnOpenFileDialogOK(object Sender, CancelEventArgs e) { chosenFile = chooseOpenFileDialog.FileName; this.Text = Path.GetFileName(chosenFile); DisplayFile(); } 

As this code demonstrates, when the user clicks OK to select a file in the file dialog, you call the DisplayFile() method, which does the work of reading in the selected file:

 void DisplayFile() { int nCols = 16; FileStream inStream = new FileStream(chosenFile, FileMode.Open,  FileAccess.Read); long nBytesToRead = inStream.Length; if (nBytesToRead > 65536/4)  nBytesToRead = 65536/4; int nLines = (int)(nBytesToRead/nCols) + 1; string [] lines = new string[nLines]; int nBytesRead = 0; for (int i=0 ; i<nLines ; i++) { StringBuilder nextLine = new StringBuilder(); nextLine.Capacity = 4*nCols; for (int j = 0 ; j<nCols ; j++) { int nextByte = inStream.ReadByte(); nBytesRead++; if (nextByte < 0 || nBytesRead > 65536) break; char nextChar = (char)nextByte; if (nextChar < 16) nextLine.Append(" x0" + string.Format("{0,1:X}", (int)nextChar)); else if (char.IsLetterOrDigit(nextChar) || char.IsPunctuation(nextChar)) nextLine.Append("  " + nextChar + " "); else nextLine.Append(" x" + string.Format("{0,2:X}", (int)nextChar)); } lines[i] = nextLine.ToString(); } inStream.Close(); this.textBoxContents.Lines = lines; } 

There's quite a lot going on in this method, so here's a breakdown. You instantiate a FileStream object for the selected file, which specifies that you want to open an existing file for reading. You then work out how many bytes there are to read in and how many lines should be displayed. The number of bytes will normally be the number of bytes in the file. However, text boxes can display only a maximum of 65,536 characters and with the chosen display format, you are displaying four characters for every byte in the file, so you will need to cap the number of bytes shown in the text box if the selected file is longer than 65,536/4 = 16,384 bytes.

Note

If you want to display longer files in this sort of environment, you might want to look up the RichTextBox class in the System.Windows.Forms namespace. RichTextBox is similar to a text box, but has many more advanced formatting facilities and does not have a limit on how much text it can display. TextBox is used here to keep the example simple and focused on the process of reading in files.

The bulk of the method is given over to two nested for loops that construct each line of text to be displayed. You use a StringBuilder class to construct each line for performance reasons: You are appending suitable text for each byte to the string that represents each line 16 times. If on each occasion you allocate a new string and take a copy of the half-constructed line, you are not only going to be spending a lot of time allocating strings, but will be wasting a lot of memory on the heap. Notice that the definition of printable characters is anything that is a letter, digit, or punctuation, as indicated by the relevant static System.Char methods. You've excluded any character with a value less than 16 from the printable list, however, which means you'll trap the carriage return (13) and line feed (10) as binary characters (a multi-line text box isn't able to display these characters properly if they occur individually within a line).

Furthermore, using the Properties Window, you changed the Font property for the text box to a fixed width font. In this case, you chose Courier New 9pt regular, and also set the text box to have vertical and horizontal scroll bars.

Upon completion, you close the stream and set the contents of the text box to the array of strings that you've built up.

Reading and Writing to Text Files

Theoretically, it's perfectly possible to use the FileStream class to read in and display text files. You have, after all, just done that. The format in which the NewFile.aspx file is displayed in the preceding example isn't particularly user-friendly, but that has nothing to do with any intrinsic problem with the FileStream class, only with how you chose to display the results in the text box.

Having said that, if you know that a particular file contains text, you will usually find it more convenient to read and write it using the StreamReader and StreamWriter classes instead of the FileStream class. That's because these classes work at a slightly higher level and are specifically geared to reading and writing text. The methods that they implement are able to automatically detect where convenient points to stop reading text are, based on the contents of the stream. In particular:

  • These classes implement methods to read or write one line of text at a time, StreamReader .ReadLine() and StreamWriter.WriteLine(). In the case of reading, this means that the stream will automatically figure out for you where the next carriage return is and stop reading at that point. In the case of writing, it means that the stream will automatically append the carriage return-line feed combination to the text that it writes out.

  • By using the StreamReader and StreamWriter classes you don't need to worry about the encoding (the text format) used in the file. Possible encodings include ASCII (1 byte for each character), or any or the Unicode-based formats, UNICODE, UTF7, UTF8, and UTF32. Text files on Windows 9x systems are always in ASCII, because Windows 9x doesn't support Unicode, but Windows NT, 2000, XP, and 2003 all do support Unicode, and so text files might theoretically contain Unicode, UTF7, UTF8, or UTF32 data instead of ASCII data. The convention is that if the file is in ASCII format, it will simply contain the text. If it is in any Unicode format, this will be indicated by the first two or three bytes of the file, which are set to particular combinations of values to indicate the format used in the file.

These bytes are known as the byte code markers. When you open a file using any of the standard Windows applications, such as Notepad or WordPad, you don't need to worry about this because these applications are aware of the different encoding methods and will automatically read the file correctly. This is also the case for the StreamReader class, which will correctly read in a file in any of these formats, while the StreamWriter class is capable of formatting the text it writes out using whatever encoding technique you request. On the other hand, if you wanted to read in and display a text file using the FileStream class, you would have to handle all this yourself.

The StreamReader class

StreamReader is used to read text files. Constructing a StreamReader is in some ways easier than constructing a FileStream instance, because some of the FileStream options are not required when using StreamReader. In particular, the mode and access types are not relevant to StreamReader, because the only thing you can do with a StreamReader is read! Furthermore, there is no direct option to specify the sharing permissions. However, there are a couple of new options:

  • You need to specify what to do about the different encoding methods. You can instruct the StreamReader to examine the byte code markers in the beginning of the file to determine the encoding method, or you can simply tell the StreamReader to assume that the file uses a specified encoding method.

  • Instead of supplying a file name to be read from, you can supply a reference to another stream.

This last option deserves a bit more discussion, because it illustrates another advantage of basing the model for reading and writing data on the concept of streams. Because the StreamReader works at a relatively high level, you might find it useful if you are in the situation in which you have another stream that is there to read data from some other source, but you would like to use the facilities provided by StreamReader to process that other stream as if it contained text. You can do so by simply passing the output from this stream to a StreamReader. In this way, StreamReader can be used to read and process data from any data source — not only files. This is essentially the situation discussed earlier with regard to the BinaryReader class. However, in this book you will only use StreamReader to connect directly to files.

The result of these possibilities is that StreamReader has a large number of constructors. Not only that, but there are a couple of FileInfo methods that return StreamReader references too: OpenText() and CreateText(). The following just illustrates some of the constructors.

The simplest constructor takes just a file name. This StreamReader will examine the byte order marks to determine the encoding:

 StreamReader sr = new StreamReader(@"C:\My Documents\ReadMe.txt"); 

Alternatively, if you prefer to specify that UTF8 encoding should be assumed:

 StreamReader sr = new StreamReader(@"C:\My Documents\ReadMe.txt",  Encoding.UTF8); 

You specify the encoding by using one of several properties on a class, System.Text.Encoding. This class is an abstract base class, from which a number of classes are derived and which implements methods that actually perform the text encoding. Each property returns an instance of the appropriate class, and the possible properties you can use here are as follows:

  • ASCII

  • Unicode

  • UTF7

  • UTF8

  • UTF32

  • BigEndianUnicode

The following example demonstrates hooking up a StreamReader to a FileStream. The advantage of this is that you can specify whether to create the file and the share permissions, which you cannot do if you directly attach a StreamReader to the file:

 FileStream fs = new FileStream(@"C:\My Documents\ReadMe.txt",  FileMode.Open, FileAccess.Read, FileShare.None); StreamReader sr = new StreamReader(fs);  

For this example, you specify that the StreamReader will look for byte code markers to determine the encoding method used, as it will do in the following examples, in which the StreamReader is obtained from a FileInfo instance:

 FileInfo myFile = new FileInfo(@"C:\My Documents\ReadMe.txt"); StreamReader sr = myFile.OpenText(); 

Just as with a FileStream, you should always close a StreamReader after use. Failure to do so will result in the file remaining locked to other processes (unless you used a FileStream to construct the StreamReader and specified FileShare.ShareReadWrite):

 sr.Close(); 

Now that you've gone to the trouble of instantiating a StreamReader, you can do something with it. As with the FileStream, you'll simply see the various ways to read data, and the other, less commonly used StreamReader methods are left to the SDK documentation.

Possibly the easiest method to use is ReadLine(), which keeps reading until it gets to the end of a line. It does not include the carriage return–line feed combination that marks the end of the line in the returned string:

 string nextLine = sr.ReadLine(); 

Alternatively, you can grab the entire remainder of the file (or strictly, the remainder of the stream) in one string:

 string restOfStream = sr.ReadToEnd(); 

You can read a single character:

 int nextChar = sr.Read(); 

This overload of Read() casts the returned character to an int. This is so that it has the option of returning a value of -1 if the end of the stream has been reached.

Finally, you can read a given number of characters into an array, with an offset:

 // to read 100 characters in. int nChars = 100; char [] charArray = new char[nChars];  int nCharsRead = sr.Read(charArray, 0, nChars); 

nCharsRead will be less than nChars if you have requested to read more characters than are left in the file.

The StreamWriter class

This works in basically the same way as the StreamReader, except that you can only use StreamWriter to write to a file (or to another stream). Possibilities for constructing a StreamWriter include:

 StreamWriter sw = new StreamWriter(@"C:\My Documents\ReadMe.txt"); 

This will use UTF8 Encoding, which is regarded by .NET as the default encoding method. If you want, you can specify an alternative encoding:

 StreamWriter sw = new StreamWriter(@"C:\My Documents\ReadMe.txt", true, Encoding.ASCII); 

In this constructor, the second parameter is a Boolean that indicates whether the file should be opened for appending. There is, oddly, no constructor that takes only a file name and an encoding class.

Of course, you may want to hook up StreamWriter to a file stream to give you more control over the options for opening the file:

 FileStream fs = new FileStream(@"C:\My Documents\ReadMe.txt",  FileMode.CreateNew, FileAccess.Write, FileShare.Read); StreamWriter sw = new StreamWriter(fs);  

FileStream does not implement any methods that return a StreamWriter class.

Alternatively, if you want to create a new file and start writing data to it, you'll find this sequence useful:

 FileInfo myFile = new FileInfo(@"C:\My Documents\NewFile.txt");  StreamWriter sw = myFile.CreateText(); 

Just as with all other stream classes it is important to close a StreamWriter class when you have finished with it:

 sw.Close(); 

Writing to the stream is done using any of four overloads of StreamWriter.Write(). The simplest writes out a string and appends it with a carriage return–line feed combination:

 string nextLine = "Groovy Line";  sw.Write(nextLine); 

It is also possible to write out a single character:

 char nextChar = 'a';  sw.Write(nextChar); 

An array of characters is also possible:

 char [] charArray = new char[100]; // initialize these characters sw.Write(charArray); 

It is even possible to write out a portion of an array of characters:

 int nCharsToWrite = 50; int startAtLocation = 25; char [] charArray = new char[100]; // initialize these characters sw.Write(charArray, startAtLocation, nCharsToWrite); 

Example: ReadWriteText

The ReadWriteText example displays the use of the StreamReader and StreamWriter classes. It is similar to the earlier ReadBinaryFile example, but it assumes the file to be read in is a text file and displays it as such. It is also capable of saving the file (with any modifications you've made to the text in the text box). It will save any file in Unicode format.

The screenshot in Figure 34-11 shows ReadWriteText displaying the same NewFile.aspx file that you used earlier. This time, however, you are able to read the contents a bit more easily!

image from book Figure 34-11

We won't go over the details of adding the event handlers for the Open File dialog box, because they are basically the same as with the earlier BinaryFileReader example. As with that example, opening a new file causes the DisplayFile() method to be called. The only real difference between this example and the previous one is the implementation of DisplayFile as well as that you now have the option to save a file. This is represented by another menu option, Save. The handler for this option calls another method you've added to the code, SaveFile(). (Note that the new file always overwrites the original file; this example does not have an option to write to a different file.)

You'll look at SaveFile() first, because that is the simplest function. You simply write each line of the text box, in turn, to a StreamWriter stream, relying on the StreamReader.WriteLine() method to append the trailing carriage return and line feed at the end of each line:

 void SaveFile() { StreamWriter sw = new StreamWriter(chosenFile, false, Encoding.Unicode); foreach (string line in textBoxContents.Lines) sw.WriteLine(line); sw.Close(); } 

chosenFile is a string field of the main form, which contains the name of the file you have read in (just as for the previous example). Notice that you specify Unicode encoding when you open the stream. If you'd wanted to write files in some other format, you'd simply need to change the value of this parameter. The second parameter to this constructor would be set to true if you wanted to append to a file, but you don't in this case. The encoding must be set at construction time for a StreamWriter. It is subsequently available as a read-only property, Encoding.

Now you examine how files are read in. The process of reading in is complicated by the fact that you don't know until you've read in the file how many lines it is going to contain (in other words, how many (char)13(char)10 sequences are in the file) because char(13)char(10) is the carriage return–line feed combination that occurs at the end of a line). You solve this problem by initially reading the file into an instance of the StringCollection class, which is in the System.Collections.Specialized namespace. This class is designed to hold a set of strings that can be dynamically expanded. It implements two methods that you will be interested in: Add(), which adds a string to the collection, and CopyTo(), which copies the string collection into a normal array (a System.Array instance). Each element of the StringCollection object will hold one line of the file.

The DisplayFile() method calls another method, ReadFileIntoStringCollection(), which actually reads in the file. After doing this, you now know how many lines there are, so you are in a position to copy the StringCollection into a normal, fixed-size array and feed this array into the text box. Because only the references to the strings that are copied when you actually make the copy, not the strings themselves, the process is reasonably efficient:

 void DisplayFile() { StringCollection linesCollection = ReadFileIntoStringCollection(); string [] linesArray = new string[linesCollection.Count]; linesCollection.CopyTo(linesArray, 0); this.textBoxContents.Lines = linesArray; } 

The second parameter of StringCollection.CopyTo() indicates the index within the destination array of where you want the collection to start.

Now you examine the ReadFileIntoStringCollection() method. You use a StreamReader to read in each line. The main complication here is the need to count the characters read in to make sure you don't exceed the capacity of the text box:

 StringCollection ReadFileIntoStringCollection() { const int MaxBytes = 65536; StreamReader sr = new StreamReader(chosenFile); StringCollection result = new StringCollection(); int nBytesRead = 0; string nextLine; while ( (nextLine = sr.ReadLine()) != null) { nBytesRead += nextLine.Length; if (nBytesRead > MaxBytes) break; result.Add(nextLine); } sr.Close(); return result; } 

That completes the code for this example.

If you run ReadWriteText, read in the NewFile.aspx file, and then save it, the file will be in Unicode format. You wouldn't be able to tell this from any of the usual Windows applications: Notepad, WordPad, and even the ReadWriteText example, will still read the file in and display it correctly under Windows NT/2000/XP/2003, although because Windows 9x doesn't support Unicode, applications like Notepad won't be able to understand the Unicode file on those platforms. (If you download the example from the Wrox Press Web site at www.wrox.com, you can try this!) However, if you try to display the file again using the earlier BinaryFileReader example, you can see the difference immediately, as shown in Figure 34-12. The two initial bytes that indicate the file is in Unicode format are visible, and thereafter you see that every character is represented by two bytes. This last fact is very obvious, because the high-order byte of every character in this particular file is zero, so every second byte in this file now displays x00.

image from book
Figure 34-12




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net