Reading and Writing to Files

 
Chapter 12 - File and Registry Operations
bySimon Robinsonet al.
Wrox Press 2002
  

Reading and writing to files is in principle very simple; however, it is not done through the DirectoryInfo or FileInfo objects that we've just been examining. Instead, it is done through a number of classes that represent a generic concept called a stream , which we will examine next .

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 then we talk about reading from the stream

  • If the data is being transferred from your program to some outside source then we talk about 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 , while 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 wished 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, we make it easier for this code to be reused (through inheritance) in different circumstances. As an example of this, the StringReader and StringWriter classes mentioned above are part of the same inheritance tree as two classes that we 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.

The actual hierarchy of stream- related classes in the System.IO namespace looks like this:

click to expand

As far as reading and writing files is concerned, the classes that we are going to be most concerned with are:

  • FileStream . This class is intended for reading and writing binary data in a binary file though if you wish you can 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.

Although we won't be using them in our examples, we'll also mention a couple of other classes that you may find useful, BinaryReader and BinaryWriter . If you wish to use these classes, you should refer to the MSDN documentation for details of their operation.

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. The easiest way to think about it is that the BinaryReader and BinaryWriter sit between the stream and your code, providing extra formatting:

click to expand

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 a plain 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 explicitly 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, in the file). A corresponding BinaryReader.Read() method will extract 8 bytes from the stream and recover the value of the long .

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, Windows will not go to 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 will grab 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 it may be the case that you need 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. ( BufferedStream is not, however, designed for the situation in which an application frequently alternates between reading and writing data.)

Reading and Writing to Binary Files

Reading and writing to binary files is usually done using the FileStream class.

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 opening an existing file should any write operations be interpreted as overwriting the contents of the file or appending to the file?

  • The access , indicating how you want access to file are you intending to read or write to the file or do both?

  • The share access in other words do 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 in this chapter we will only consider 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 respectively called FileMode , FileAccess , and FileShare . The values of these enumerations should be self-explanatory, and they are:

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 any access to the file while     // fs3 is open     FileStream fs3 = new FileStream(@"C:\C# Projects\Project3.doc",     FileMode.Create, FileAccess.Write, FileShare.None);   

From this code we can see that these overloads of the 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, while FileInfo.OpenWrite() gives you read-write access. FileInfo.Open() allows you to specify the mode, access, and file share parameters explicitly.

You won't be surprised to learn that once 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 we 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 then you know you're at the end of the stream. Here's an example where we read into a Byte array 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 wish to write data to a file, then there are two parallel methods 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 we initialized the ByteArray we mentioned before with some values, we 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 .

Besides these methods, FileStream implements various other methods and properties to do with bookkeeping tasks like 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 MSDN documentation.

Example: BinaryFileReader

We'll illustrate the use of the FileStream class by writing an example, BinaryFileReader , which reads in and displays any file. The example is as usual created in Visual Studio .NET as a Windows application. We've added one menu item, which brings up a standard OpenFileDialog asking what file to read in, then displays the file. As we are reading in binary files, we need to be able to display non-printable characters . The way we will do this is by displaying each byte of the file individually, showing 16 bytes on each line of a multiline textbox. If the byte represents a printable ASCII character we'll display that character, otherwise we'll display the value of the byte in hexadecimal format. In either case, we pad out the displayed text with spaces so that each 'byte' displayed occupies four columns so the bytes line up nicely under each other.

This is what the BinaryFileReader looks like when viewing a text file, (since the BinaryFileReader can view any file, it's quite possible to use it on text files as well as binary ones). In this case, the example has read in a file that contains some negotiations that went on in a recent game of Civilization (Sid Meier's well-known computer game):

click to expand

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

For this example, we won't demonstrate writing to files. That's because we don't want to get bogged down in the complexities of trying to translate the contents of a textbox like the one above into a binary stream! We will demonstrate writing to files later on when we develop an example that can read or write, but only to text files.

Let's look at the code used to get these results. First, we need an extra using statement, since besides System.IO , this example is going to use the StringBuilder class from the System.Text namespace to construct the strings in the textbox:

 using System.IO;   using System.Text;   

Next, we 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 :

 public class Form1 : System.Windows.Forms.Form    {   private OpenFileDialog chooseOpenFileDialog = new OpenFileDialog();     private string chosenFile;   

We 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();     }   

From this we see that once the user clicks OK to select a file in the file dialog, we call a method, DisplayFile() , which actually does the work of reading in the 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 we'll break it down. We instantiate a FileStream object for the selected file specifying that we wish to open an existing file for reading. We 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. Textboxes can only display a maximum of 65,536 characters, however, and with our chosen format, we are displaying 4 characters for every byte in the file, so we will need to cap the number of bytes shown if the file is longer than 65,536/4 = 16,384.

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 textbox, but has many more advanced formatting facilities and does not have a limit on how much text it can display. We are using TextBox 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. We use a StringBuilder class to construct each line for performance reasons: We will be appending suitable text for each byte to the string that represents each line 16 times. If on each occasion we allocate a new string and take a copy of the half- constructed line, we 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 our definition of "printable" characters is anything that is a letter, digit, or punctuation, as indicated by the relevant static System.Char methods. We've excluded any character with a value less than 16 from the printable list, however, which means we'll trap the carriage return (13) and line feed (10) as binary characters (a multiline textbox isn't able to display these characters properly if they occur individually within a line).

A couple of other points; Using the Properties Window, we changed the Font for the text box to a fixed width font we chose Courier New 9pt regular, and also set the textbox to have vertical and horizontal scroll bars.

Finally, we close the stream and set the contents of the textbox to the array of strings that we'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. We have, after all, just demonstrated doing that. The format in which we displayed the CivNegotiations.txt file above wasn't particularly user-friendly, but that wasn't due to any intrinsic problem with the FileStream class that was just because of the way we had chosen to display the results in the textbox.

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. 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 upon 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 is 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, and UTF8. Text files on Windows 9x systems are always in ASCII, because Windows 9x doesn't support Unicode, but Windows NT, 2000, and XP all do support Unicode, and so text files might theoretically contain Unicode, UTF7, or UTF8 data instead of ASCII data. The convention is that if the file is in ASCII format, it will simply contain the text. If 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.

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 simpler than constructing a FileStream instance, because some of the FileStream options are not required. In particular, the mode and access types are not relevant, because the only thing you can do with a StreamReader is read! As well as this, there is no direct option to specify the sharing permissions. However, there are a couple of new options:

  • We need to specify what to do about the different encoding methods. We can instruct the StreamReader to examine the byte code markers in the file to determine the encoding method, or we 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, we can supply a reference to another stream.

This last option deserves a bit more discussion, because it illustrates another advantage of basing our model for reading and writing data around 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 we discussed earlier with regard to the BinaryReader class. However, in this book we 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() . Here we will just illustrate 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);   

We 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 defined, which implement methods that actually perform the text encoding. Each property returns an instance of the appropriate class, and the possible properties we can use here are:

  • ASCII

  • Unicode

  • UTF7

  • UTF8

  • BigEndianUnicode

The following example demonstrates hooking a StreamReader up to a FileStream . The advantage of this is that we can explicitly specify whether to create the file and the share permissions, which we cannot do if we 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, we 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 we've gone to the trouble of instantiating a StreamReader , we can do something with it. As with the FileStream , we'll simply point out the various ways there are to read data, and leave the other, less commonly used, StreamReader methods to the MSDN 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 we 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");   

The above code will use UTF8 Encoding, which is regarded by .NET as the default encoding method. If you want to 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 a StreamWriter up 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);   

FileInfo does not implement any methods that return a StreamWriter .

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 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 textbox). It will save any file in Unicode format.

The screenshot shows ReadWriteText being used to display the same CivNegotiations file that we saw earlier. This time, however, we are able to read the contents a bit more easily!

click to expand

We won't go over the details of adding the event handlers for the open file dialog, because they are basically the same as with the BinaryFileReader example. As with that example, opening a new file causes the DisplayFile() method to be called. The only real differences between this example and the previous one are the implementation of DisplayFile , and also that we now have the option to save a file. This is represented by another menu option, Save . The handler for this option calls another method we'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.)

We'll look at SaveFile() first, since that is the simplest function. We simply write each line of the textbox, 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 we have read in (just as for the previous example). Notice that we specify Unicode encoding when we open the stream. If we'd wanted to write files in some other format then we'd simply need to change the value of this parameter. The second parameter to this constructor would be set to true if we wanted to append to a file, but we 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 we'll examine how files are read in. The process of reading in is complicated by the fact that we don't know until we'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 since char(13)char(10) is the carriage return-line feed combination that occurs at the end of a line). We 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 we 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, we now know how many lines there are, so we are in a position to copy the StringCollection into a normal, fixed size array and feed this array into the textbox. Since when we make the copy it is only the references to the strings that get copied , 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 we want the collection to start.

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

   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 the example.

If we run ReadWriteText , read in the CivNegotiations file, and then save it, the file will be in Unicode format. We wouldn't be able to tell this from any of the usual Windows applications: Notepad, WordPad, and even our own ReadWriteText example, will still read the file in and display it correctly under Windows NT/2000/XP, 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, you can try this!) However, if we try to display the file again using our earlier BinaryFileReader example, we can see the difference immediately, as shown in the following screenshot. The two initial bytes that indicate the file is in Unicode format are visible, and thereafter we 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 :

click to expand
  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

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