The Classes for Input and Output


The System.IO namespace contains almost all of the classes that you will be covering in this chapter. System.IO contains the classes for reading and writing data to and from files, and you must reference this namespace in your C# application to gain access to these classes. There are quite a few classes contained in System.IO, as you can see in Figure 22-1, but you will only be covering the primary classes needed for file input and output.

image from book
Figure 22-1

The classes you look at in this chapter are:

  • File: A static utility class that exposes many static methods for moving, copying, and deleting files.

  • Directory: A static utility class that exposes many static methods for moving, copying, and deleting directories.

  • Path: A utility class used to manipulate path names.

  • FileInfo: Represents a physical file on disk, and has methods to manipulate this file. For any reading and writing to the file, a Stream object must be created.

  • DirectoryInfo: Represents a physical directory on disk and has methods to manipulate this directory.

  • FileSystemInfo: Serves as the base class for both FileInfo and Directory info, making it possible to deal with files and directories at the same time using polymorphism.

  • FileStream: Represents a file that can be written to or read from, or both. This file can be written to and read from asynchronously or synchronously.

  • StreamReader: Reads character data from a stream and can be created by using a FileStream as a base.

  • StreamWriter: Writes character data to a stream and can be created by using a FileStream as a base.

  • FileSystemWatcher: The FileSystemWatcher is the most advanced class you will be examining in this chapter. It is used to monitor files and directories, and exposes events that your application can catch when changes occur in these locations. This functionality has always been missing from Windows programming, but now the .NET Framework makes it much easier to respond to file system events.

You'll also look at the System.IO.Compression namespace in this chapter, which allows you to read and write compressed files, using either GZIP compression or the Deflate compression scheme:

  • DeflateStream: Represents a stream where data is compressed automatically when writing, or uncompressed automatically when reading. Compression is achieved using the Deflate algorithm.

  • GZipStream: Represents a stream where data is compressed automatically when writing or uncompressed automatically when reading. Compression is achieved using the GZIP algorithm.

Finally, you'll look at object serialization using the System.Runtime.Serialization namespace and its child namespaces. You'll primarily be looking at the BinaryFormatter class in the System. Runtime.Serialization.Formatters.Binary namespace, which enables you to serialize objects to a stream as binary data, and deserialize them again.

The File and Directory Classes

The File and Directory utility classes expose many static methods for manipulating, surprisingly enough, files and directories. These methods make it possible to move files, query, and update attributes, and create FileStream objects. As you learned in Chapter 8, static methods can be called on classes without having to create instances of them.

Some of the most useful static methods of the File class are shown in the following table.

Method

Description

Copy()

Copies a file from a source location to a target location.

Create()

Creates a file in the specified path.

Delete()

Deletes a file.

Open()

Returns a FileStream object at the specified path.

Move()

Moves a specified file to a new location. You can specify a different name for the file in the new location.

Some useful static methods of the Directory class are shown in the next table.

Method

Description

CreateDirectory()

Creates a directory with the specified path.

Delete()

Deletes the specified directory and all the files within it.

GetDirectories()

Returns an array of string objects that represent the names of the directories below the specified directory.

GetFiles()

Returns an array of string objects that represent the names of the files in the specified directory.

GetFileSystemEntries()

Returns an array of string objects that represent the names of the files and directories in the specified directory.

Move()

Moves the specified directory to a new location. You can specify a new name for the folder in the new location.

The FileInfo Class

Unlike the File class, the FileInfo class is not static and does not have static methods. This class is only useful when instantiated. A FileInfo object represents a file on a disk or network location, and you can create one by supplying a path to a file, for example:

 FileInfo aFile = new FileInfo(@"C:\Log.txt"); 
Note

Since you will be working with strings representing the path of a file throughout this chapter, which will mean a lot of \ characters in your strings, it's worth reminding yourself that the @ that prefixes the string above means that this string will be interpreted literally. Thus \ will be interpreted as \, and not as an escape character. Without the @ prefix, you would need to use \\ instead of \ to avoid having this character be interpreted as an escape character. In this chapter you'll stick to the @ prefix for your strings.

You can also pass the name of a directory to the FileInfo constructor, although in practical terms this isn't particularly useful. Doing this causes the base class of FileInfo, which is FileSystemInfo, to be initialized with all the directory information, but none of the FileInfo methods or properties relating specifically to files will work.

Many of the methods exposed by the FileInfo class are similar to those of the File class, but because File is a static class, it requires a string parameter specifying the file location for every method call. Therefore, the following calls do the same thing:

 FileInfo aFile = new FileInfo("Data.txt"); if (aFile.Exists) Console.WriteLine("File Exists"); if (File.Exists("Data.txt")) Console.WriteLine("File Exists"); 

In this code a check is made to see if the file Data.txt exists. Note that no directory information is specified here, meaning that the current working directory is the only location examined. This directory is the one containing the application that calls this code. You'll look at this in more detail a little later, in the section "Pathnames and Relative Paths."

Most of the FileInfo methods mirror the File methods in this manner. In most cases it doesn't matter which technique you use, although the following criteria may help you to decide which is more appropriate:

  • It makes sense to use methods on the static File class if you are only making a single method call — the single call will be faster because the .NET Framework will not have to go through the process of instantiating a new object and then calling the method.

  • If your application is performing several operations on a file, it makes more sense to instantiate a FileInfo object and use its methods — this will save time because the object will already be referencing the correct file on the file system, whereas the static class will have to find it every time.

The FileInfo class also exposes properties relating to the underlying file, some of which can be manipulated to update the file. Many of these properties are inherited from FileSystemInfo, and thus apply to both the File and Directory classes. The properties of FileSystemInfo are shown in the following table.

Property

Description

Attributes

Gets or sets the attributes of the current file or directory, using the FileAttributes enumeration.

CreationTime

Gets or sets the creation date and time of the current file.

Extension

Retrieves the extension of the file. This property is read-only.

Exists

Determines whether a file exists. This is a read-only abstract property, and is overridden in FileInfo and DirectoryInfo.

FullName

Retrieves the full path of the file. This property is read-only.

LastAccessTime

Gets or sets the date and time that the current file was last accessed.

LastWriteTime

Gets or sets the date and time that the current file was last written to.

Name

Retrieves the full path of the file. This is a read-only abstract property, and is overridden in FileInfo and DirectoryInfo.

The properties specific to FileInfo are shown in the next table.

Property

Description

Directory

Retrieves a DirectoryInfo object representing the directory containing the current file. This property is read-only.

DirectoryName

Returns the path to the file's directory. This property is read-only.

IsReadOnly

Shortcut to the read-only attribute of the file. This property is also accessible via Attributes.

Length

Gets the size of the file in bytes, returned as a long value. This property is read-only.

Note that a FileInfo object doesn't in itself represent a stream. To read or write to a file, a Stream object has to be created. the FileInfo object aids you in doing this by exposing several methods that return instantiated Stream objects.

The DirectoryInfo Class

The DirectoryInfo class works exactly like the FileInfo class. It is an instantiated object that represents a single directory on a machine. Like the FileInfo class, many of the method calls are duplicated across Directory and DirectoryInfo. The guidelines for choosing whether to use the methods of File or FileInfo also apply to DirectoryInfo methods:

  • If you are making a single call, use the static Directory class.

  • If you are making a series of calls, use an instantiated DirectoryInfo object.

The DirectoryInfo class inherits most of its properties from FileSystemInfo, as does FileInfo, although these properties operate on directories instead of files. There are also two DirectoryInfo-specific properties, shown in the following table.

Property

Description

Parent

Retrieves a DirectoryInfo object representing the directory containing the current directory. This property is read-only.

Root

Retrieves a DirectoryInfo object representing the root directory of the current volume, for example the C:\ directory. This property is read-only.

Path Names and Relative Paths

When specifying a path name in .NET code, you can use either absolute or relative path names. An absolute path name explicitly specifies where a file or directory is from a known location — like the C: drive. An example of this would be C:\Work\LogFile.txt. Note that this path defines exactly where the file is, with no ambiguity.

Relative path names are relative to a starting location. By using relative path names, no drive or known location needs to be specified. You saw this earlier, where the current working directory was the starting point — which is the default behavior for relative path names. For example, if your application is running in the C:\Development\FileDemo directory and uses the relative path LogFile.txt, the file references would be C:\Development\FileDemo\LogFile.txt. To move "up" a directory the .. string is used. Thus, in the same application, the path ..\Log.txt points to the file C:\Development\Log.txt.

As you saw earlier, the working directory is initially set to the directory in which your application is running. When you are developing with Visual Studio 2005, this means the application is several directories beneath the project folder you created. It is usually located in ProjectName\bin\Debug. This means that to access a file in the root folder of the project, you will have to move up two directories with ..\..\ — you will see this happen often in the chapter.

Should you need to, you can find out what the working directory is currently set to using Directory. GetCurrentDirectory(), or you can set it to a new path using Directory.SetCurrentDirectory().

The FileStream Object

The FileStream object represents a stream pointing to a file on a disk or a network path. While the class does expose methods for reading and writing bytes from and to the files, most often you will use a StreamReader or StreamWriter to perform these functions. This is because the FileStream class operates on bytes and byte arrays, while the Stream classes operate on character data. Character data is easier to work with, but you will see that there are certain operations, such as random file access (access to data at some point in the middle of a file), that can only be performed by a FileStream object. You'll examine this subject later in the chapter.

There are several ways to create a FileStream object. The constructor has many different overloads but the simplest takes just two arguments: the filename and a FileMode enumeration value.

 FileStream aFile = new FileStream(filename, FileMode.Member); 

The FileMode enumeration has several members that specify how the file is opened or created. You'll see the possibilities shortly. Another commonly used constructer is as follows:

 FileStream aFile = new FileStream(filename, FileMode.Member, FileAccess.Member); 

The third parameter is a member of the FileAccess enumeration and is a way of specifying the purpose of the stream. The members of the FileAccess enumeration are shown in the following table.

Member

Description

Read

Opens the file for reading only

Write

Opens the file for writing only

ReadWrite

Opens the file for reading or writing only

Attempting to perform an action other than that specified by the FileAccess enumeration member will result in an exception being thrown. This property is often used as a way of varying user access to the file based on the authorization level of the user.

In the version of the FileStream constructor that doesn't use a FileAccess enumeration parameter, the default value is used, which is FileAccess.ReadWrite.

The FileMode enumeration members are shown in the next table. What actually happens when each of these values is used depends on whether the filename specified refers to an existing file. Note that the entries in this table refer to the position in the file that the stream points to when it is created, a subject you'll examine in more detail in the next section. Unless otherwise stated, the stream will point to the beginning of a file.

Member

File Exists Behavior

No File Exists Behavior

Append

The file is opened, with the stream positioned at the end of the file. Can only be used in conjunction with FileAccess.Write.

A new file is created. Can only be used in conjunction with FileAccess.Write.

Create

The file is destroyed, then a new file is created in its place.

A new file is created.

CreateNew

An exception is thrown.

A new file is created.

Open

The file is opened, with the stream positioned at the beginning of the file.

An exception is thrown.

OpenOrCreate

The file is opened, with the stream positioned at the beginning of the file.

A new file is created.

Truncate

The file is opened and erased. The stream is positioned at the beginning of the file. The original file creation date is retained.

An exception is thrown.

Both the File and FileInfo classes expose OpenRead() and OpenWrite() methods that make it easier to create FileStream objects. The first opens the file for read-only access, and the second allows you write-only access. These methods provide shortcuts, so you do not have to provide all the information required in the form of parameters to the FileStream constructor. For example, the following line of code opens the Data.txt file for read-only access:

 FileStream aFile = File.OpenRead("Data.txt"); 

Note that the following code performs the same function:

 FileInfo aFileInfo = new FileInfo("Data.txt"); FileStream aFile = aFileInfo.OpenRead(); 

File Position

The FileStream class maintains an internal file pointer. This points to the location within the file where the next read or write operation will occur. In most cases, when a file is opened it points to the beginning of the file, but this pointer can be modified. This allows an application to read or write anywhere within the file. This allows for random access to a file and the ability to jump directly to a specific location in the file. This can be very time saving when dealing with very large files, because you can instantly move to the location you want.

The method that implements this functionality is the Seek() method, which takes two parameters. The first parameter specifies how far to move the file pointer, in bytes. The second parameter specifies where to start counting from, in the form of a value from the SeekOrigin enumeration. the SeekOrigin enumeration contains three values: Begin, Current, and End.

For example, the following line would move the file pointer to the eighth byte in the file, starting from the very first byte in the file:

 aFile.Seek(8, SeekOrigin.Begin); 

The following line would move the file pointer 2 bytes forward, starting from the current position. If this were executed directly after the previous line, the file pointer would now point to the tenth byte in the file:

 aFile.Seek(2, SeekOrigin.Current); 

Note that when you read from or write to a file the file pointer changes as well. After you have read 10 bytes, the file pointer will point to the byte after the tenth byte read.

You can specify negative seek positions as well, which could be combined with the SeekOrigin.End enumeration value to seek near the end of the file. The following line will seek to the fifth byte from the end of the file:

 aFile.Seek(-5, SeekOrigin.End); 

Files accessed in this manner are sometimes referred to as random access files, because an application can access any position within the file. the Stream classes you will look at later access files sequentially, and they do not allow you to manipulate the file pointer in this way.

Reading Data

Reading data using the FileStream class is not as easy as using the StreamReader class, which you will look at later in this chapter. This is because the FileStream class deals exclusively with raw bytes. Working in raw bytes makes the FileStream class useful for any kind of data file, not just text files. By reading byte data, the FileStream object can be used to read files such as images or sound files. The cost of this flexibility is that you cannot use a FileStream to read data directly into a string as you can with the StreamReader class. However, there are several conversion classes that make it fairly easy to convert byte arrays into character arrays and vice versa.

The FileStream.Read() method is the primary means to access data from a file that a FileStream object points to. This method reads the data from a file and then writes this data into a byte array. There are three parameters, the first parameter being a byte array passed in to accept data from the FileStream object. The second parameter is the position in the byte array to begin writing data to — this will normally be zero to begin writing data from the file at the beginning of the array. The last parameter specifies how many bytes to read from the file.

The following Try It Out demonstrates reading data from a random access file. The file you will read from will actually be the class file you create for the example.

Try It Out – Reading Data from Random Access Files

image from book
  1. Create a new console application called ReadFile in the directory C:\BegVCSharp\Chapter22.

  2. Add the following using directive to the top of the Program.cs file.

    using System; using System.Collections.Generic; using System.Text; using System.IO; 
  3. Add the following code to the Main() method:

    static void Main(string[] args) { byte[] byData = new byte[200]; char[] charData = new Char[200]; try { FileStream aFile = new FileStream("../../Program.cs", FileMode.Open); aFile.Seek(135, SeekOrigin.Begin); aFile.Read(byData, 0, 200); } catch(IOException e) { Console.WriteLine("An IO exception has been thrown!"); Console.WriteLine(e.ToString()); Console.ReadKey(); return; }

  4. Run the application. The result is shown in Figure 22-2.

    image from book
    Figure 22-2

How It Works

This application opens its own .cs file to read from. It does this by navigating two directories up the file structure with the .. string in the following line:

FileStream aFile = new FileStream("../../Program.cs", FileMode.Open);

The two lines that implement the actual seeking and reading from a specific point in the file are:

aFile.Seek(135, SeekOrigin.Begin); aFile.Read(byData, 0, 200);

The first line moves the file pointer to byte number 135 in the file. This is the "n" of namespace in the Program.cs file; the 135 characters preceding it are the using directives and associated #region. The second line reads the next 200 bytes into the byte array byData.

Note that these two lines were enclosed in try...catch blocks to handle any exceptions that may be thrown:

try {    aFile.Seek(135, SeekOrigin.Begin);    aFile.Read(byData,0,100); } catch(IOException e) {    Console.WriteLine("An IO exception has been thrown!");    Console.WriteLine(e.ToString());    Console.ReadKey();    return; } 

Almost all operations involving file IO can throw an exception of type IOException. All production code must contain error handling, especially when dealing with the file system. The examples in this chapter will all have a basic form of error handling.

Once you have the byte array from the file, you then need to convert it into a character array so that you can display it to the console. To do this you use the Decoder class from the System.Text namespace. This class is designed to convert raw bytes into more useful items, such as characters:

Decoder d = Encoding.UTF8.GetDecoder(); d.GetChars(byData, 0, byData.Length, charData, 0);

These lines create a Decoder object based on the UTF8 encoding schema, which is the Unicode encoding schema. Then the GetChars() method is called, which takes an array of bytes and converts it to an array of characters. Once this has been done, the character array can be written to the console.

image from book

Writing Data

The process for writing data to a random access file is very similar. A byte array must be created; the easiest way to do this is to first build the character array you wish to write to the file. Next, use the Encoder object to convert it to a byte array, very much like you used the Decoder object. Last, call the Write() method to send the array to the file.

Here's a simple example to demonstrate how this is done.

Try It Out – Writing Data to Random Access Files

image from book
  1. Create a new console application called WriteFile in the directory C:\BegVCSharp\Chapter22.

  2. Just like before, add the following using directive to the top of the Program.cs file:

    using System; using System.Collections.Generic; using System.Text; using System.IO; 
  3. Add the following code to the Main() method:

    static void Main(string[] args) { byte[] byData; char[] charData; try { FileStream aFile = new FileStream("Temp.txt", FileMode.Create); charData = "My pink half of the drainpipe.".ToCharArray(); byData = new byte[charData.Length]; Encoder e = Encoding.UTF8.GetEncoder(); e.GetBytes(charData, 0, charData.Length, byData, 0, true); // Move file pointer to beginning of file. aFile.Seek(0, SeekOrigin.Begin); aFile.Write(byData, 0, byData.Length); } catch (IOException ex) { Console.WriteLine("An IO exception has been thrown!"); Console.WriteLine(ex.ToString()); Console.ReadKey(); return; } }

  4. Run the application. It should run briefly, then close.

  5. Navigate to the application directory — the file will have been saved there because you used a relative path. This is located in the WriteFile\bin\Debug folder. Open the Temp.txt file. You should see text in the file as shown in Figure 22-3.

    image from book
    Figure 22-3

How It Works

This application opens up a file in its own directory and writes a simple string to it. In structure, this example is very similar to the previous example, except you use Write() instead of Read(), and Encoder instead of Decoder.

The following line creates a character array by using the ToCharArray() static method of the String class. Because everything in C# is an object, the text "My pink half of the drainpipe." is actually a string object (albeit a slightly odd one), so these static methods can be called even on a string of characters.

CharData = "My pink half of the drainpipe.".ToCharArray();

The following lines show how to convert the character array to the correct byte array needed by the FileStream object.

Encoder e = Endoding.UTF8.GetEncoder(); e.GetBytes(charData, 0, charData.Length, byData, 0, true);

This time, an Encoder object is created based on the UTF8 encoding. You used Unicode for the decoding as well, and this time you need to encode the character data into the correct byte format before you can write to the stream. the GetBytes() method is where the magic happens. This converts the character array to the byte array. It accepts a character array as the first parameter (charData in your example), and the index to start in that array as the second parameter (0 for the start of the array). The third parameter is the number of characters to convert (charData.Length — the number of elements in the charData array). The fourth parameter is the byte array to place the data into (byData), and the fifth parameter is the index to start writing in the byte array (0 for the start of the byData array).

The sixth and final parameter determines if the Encoder object should flush its state after completion. This refers to the fact that the Encoder object retains an in-memory record of where it was in the byte array. This aids in subsequent calls to the Encoder object, but is meaningless when only a single call is made. The final call to the Encoder must set this parameter to true to clear its memory and free the object for garbage collection.

After this it is a simple matter of writing the byte array to the FileStream using the Write() method:

aFile.Seek(0, SeekOrigin.Begin); aFile.Write(byData, 0, byData.Length);

Like the Read() method, the Write() method has three parameters: the array to write from, the index in the array to start writing from, and the number of bytes to write.

image from book

The StreamWriter Object

Working with arrays of bytes is not most people's idea of fun — having worked with the FileStream object, you may be wondering if there is an easier way. Fear not, for once you have a FileStream object you will usually wrap it in a StreamWriter or StreamReader and use their methods to manipulate the file. If you do not need the ability to change the file pointer to any arbitrary position, then these classes make working with files much easier.

The StreamWriter class allows you to write characters and strings to a file, with the class handling the underlying conversions and writing to the FileStream object for you.

There are many ways to create a StreamWriter object. If you already have a FileStream object, then you can use this to create a StreamWriter:

 FileStream aFile = new FileStream("Log.txt", FileMode.CreateNew); StreamWriter sw = new StreamWriter(aFile); 

A StreamWriter object can also be created directly from a file:

 StreamWriter sw = new StreamWriter("Log.txt", true); 

This constructor takes the file name, and a Boolean value that specifies whether to append to the file or create a new one:

  • If this is set to false, then a new file is created or the existing file is truncated and then opened.

  • If it is set to true, then the file is opened, and the data is retained. If there is no file, a new one is created.

Unlike when creating a FileStream object, creating a StreamWriter does not provide you with a similar range of options — other than the Boolean value to append or create a new file, you have no option for specifying the FileMode property as you did with the FileStream class. Also, you do not have an option of setting the FileAccess property, so you will always have read/write privileges to the file. To use any of the advanced parameters, you must first specify them in the FileStream constructor and then create a StreamWriter from the FileStream object, as you do in the following Try It Out.

Try It Out – Output Stream

image from book
  1. Create a new console application called StreamWrite in the directory C:\BegVCSharp\ Chapter22.

  2. You will be using the System.IO namespace again, so add the following using directive near the top of the Program.cs file:

    using System; using System.Collections.Generic; using System.Text; using System.IO; 
  3. Add the following code to the Main() method:

    static void Main(string[] args) { try { FileStream aFile = new FileStream("Log.txt", FileMode.OpenOrCreate); StreamWriter sw = new StreamWriter(aFile); bool truth = true; // Write data to file. sw.WriteLine("Hello to you."); sw.WriteLine("It is now {0} and things are looking good.", DateTime.Now.ToLongDateString()); sw.Write("More than that,"); sw.Write(" it's {0} that C# sw.Close(); } catch(IOException e) { Console.WriteLine("An IO exception has been thrown!"); Console.WriteLine(e.ToString()); Console.ReadLine(); return; } }

  4. Build and run the project. If no errors are found, it should quickly run and close. Since you are not displaying anything on the console, it is not a very exciting program to watch.

  5. Go to the application directory and find the Log.txt file. This is located in the StreamWrite\ bin\Debug folder because you used a relative path.

  6. Open up the file, and you should see the text shown in Figure 22-4.

    image from book
    Figure 22-4

How It Works

This simple application demonstrates the two most important methods of the StreamWriter class, Write() and WriteLine(). Both of them have many overloaded versions for performing more advanced file output, but you used basic string output in this example.

The WriteLine() method will write the string passed to it, followed immediately by a newline character. You can see in the example that this causes the next write operation to begin on a new line.

Just as you can write formatted data to the console, so you can also do this to files. For example, you can write out the value of variables to the file using standard format parameters:

sw.WriteLine("It is now {0} and things are looking good.",              DateTime.Now.ToLongDateString());

DateTime.Now holds the current date, the ToLongDateString() method is used to convert this date into an easy-to-read form.

The Write() method simply writes the string passed to it to the file, without a newline character appended, allowing you to write a complete sentence or paragraph using more than one Write() statement.

sw.Write("More than that,"); sw.Write(" it's {0} that C# is fun.", truth);

Here again, you use format parameters, this time with Write() to display the Boolean value truth — you set this variable to true earlier, and its value is automatically converted into the string "True" for the formatting.

You can use Write() and format parameters to write comma-separated files:

 [StreamWriter object].Write("{0},{1},{2}", 100, "A nice product", 10.50); 

In a more sophisticated example, this data could come from a database or other data source.

image from book

The StreamReader Object

Input streams are used to read data from an external source. Many times this will be a file on a disk or network location. But remember that this source could be almost anything that can send data, such as a network application, Web service, or even the console.

The StreamReader class is the one that you will be using to read data from files. Like the StreamWriter class, this is a generic class that can be used with any stream. In the next Try It Out, you will again be constructing it around a FileStream object so that it points to the correct file.

StreamReader objects are created in much the same way as StreamWriter objects. The most common way to create one is to use a previously created FileStream object:

FileStream aFile = new FileStream("Log.txt", FileMode.Open); StreamReader sr = new StreamReader(aFile); 

Like the StreamWriter, the StreamReader class can be created directly from a string containing the path to a particular file:

StreamReader sr = new StreamReader("Log.txt");

Try It Out – Stream Input

image from book
  1. Create a new console application called StreamRead in the directory C:\BegVCSharp\ Chapter22.

  2. Again you must import the System.IO namespace, so place the following line of code near the top of Program.cs:

    using System; using System.Collections.Generic; using System.Text; using System.IO; 
  3. Add the following code to the Main() method:

    static void Main(string[] args) { string strLine; try { FileStream aFile = new FileStream("Log.txt", FileMode.Open); StreamReader sr = new StreamReader(aFile); strLine = sr.ReadLine(); // Read data in line by line. while(strLine != null) { Console.WriteLine(strLine); strLine = sr.ReadLine(); } sr.Close(); } catch(IOException e) { Console.WriteLine("An IO exception has been thrown!"); Console.WriteLine(e.ToString()); return; } Console.ReadKey(); }

  4. Copy the Log.txt file, created in the previous example, into the StreamRead\bin\Debug directory. If you don't have a file named Log.txt, the FileStream constructor will throw an exception when it doesn't find the file.

  5. Run the application — you should see the text of the file written to the console. The result is shown in Figure 22-5.

    image from book
    Figure 22-5

How It Works

This application is very similar to the previous one, with the obvious difference being that it is reading a file rather than writing one. As before you must import the System.IO namespace to be able to access the necessary classes.

You use the ReadLine() method to read text from the file. This method reads text until a carriage return is found, and returns the resulting text as a string. The method returns a null when the end of the file has been reached, which you use to test for the end of the file. Note that you use a while loop, which checks to be sure that the line read isn't null before any code in the body of the loop is executed — this way only the genuine contents of the file are displayed:

strLine = sr.ReadLine(); while(strLine != null) {    Console.WriteLine(strLine);    strLine = sr.ReadLine(); }
image from book

Reading Data

The ReadLine() method is not the only way you have of accessing data in a file. the StreamReader class has many methods for reading data.

The simplest of the reading methods is Read(). This method returns the next character from the stream as a positive integer value or a -1 if it has reached the end. This value can be converted into a character by using the Convert utility class. In the example above the main parts of the program could be rewritten as follows:

StreamReader sr = new StreamReader(aFile); int nChar; nChar = sr.Read(); while(nChar != -1) { Console.Write(Convert.ToChar(nChar)); nChar = sr.Read(); } sr.Close();

A very convenient method to use with smaller files is the ReadToEnd() method. This method reads the entire file and returns it as a string. In this case, the earlier application could be simplified to this:

StreamReader sr = new StreamReader(aFile); strLine = sr.ReadToEnd(); Console.WriteLine(strLine); sr.Close();

While this may seem very easy and convenient, care must be taken. By reading all the data into a string object, you are forcing the data in the file to exist in memory. Depending on the size of the data file, this can be prohibitive. If the data file is extremely large, it is better to leave the data in the file and access it with the methods of the StreamReader.

Delimited Files

Delimited files are a common form of data storage and are used by many legacy systems — if your application must interoperate with such a system, then you will encounter the delimited data format quite often. A particularly common form of delimiter is a comma — for example, the data in an Excel spreadsheet, an Access database, or a SQL Server database can be exported as a comma-separated value (CSV) file.

You've seen how to use the StreamWriter class to write such files using this approach — it is also easy to read comma-separated files. If you cast your mind back to Chapter 5, you may remember that you saw the Split() method of the String class, that is used to convert a string into an array based on a supplied separator character. If you specify a comma as the separator, it will create a correctly dimensioned string array containing all of the data in the original comma-separated string.

In the next Try It Out, you see how useful this can be. The example deals with comma-separated values, loading them into a List<Dictionary<string, string>> object. This useful example is quite generic, and you may find yourself using this technique in your own applications if you need to work with comma-separated values.

Try It Out – Comma-Separated Values

image from book
  1. Create a new console application called CommaValues in the directory C:\BegVCSharp\ Chapter22.

  2. Place the following line of code near the top of Program.cs. You need to import the System.IO namespace for your file handling:

    using System; using System.Collections.Generic; using System.Text; using System.IO; 
  3. Add the following GetData() method into the body of Program.cs, before the Main() method:

     private static List<Dictionary<string, string>> GetData(out List<string> columns) { string strLine; string[] strArray; char[] charArray = new char[] {','}; List<Dictionary<string, string>> data = new List<Dictionary<string, string>>(); columns = new List<string>(); try { FileStream aFile = new FileStream("../../../SomeData.txt", FileMode.Open); StreamReader sr = new StreamReader(aFile); // Obtain the columns from the first line. // Split row of data into string array strLine = sr.ReadLine(); strArray = strLine.Split(charArray); for (int x = 0; x <= strArray.GetUpperBound(0); x++) { columns.Add(strArray[x]); } strLine = sr.ReadLine(); while (strLine != null) { // Split row of data into string array strArray = strLine.Split(charArray); Dictionary<string, string> dataRow = new Dictionary<string, string>(); for (int x = 0; x <= strArray.GetUpperBound(0); x++) { dataRow.Add(columns[x], strArray[x]); } data.Add(dataRow); strLine = sr.ReadLine(); } sr.Close(); return data; } catch (IOException ex) { Console.WriteLine("An IO exception has been thrown!"); Console.WriteLine(ex.ToString()); Console.ReadLine(); return data; } } 

  4. Now add the following code to the Main() method:

    static void Main(string[] args) { List<string> columns; List<Dictionary<string, string>> myData = GetData(out columns); foreach (string column in columns) { Console.Write("{0,-20}", column); } Console.WriteLine(); foreach (Dictionary<string, string> row in myData) { foreach (string column in columns) { Console.Write("{0,-20}", row[column]); } Console.WriteLine(); } Console.ReadKey(); }

  5. In VS, create a new text file by choosing Text File from the File New File dialog.

  6. Enter the following text into this new text file:

    ProductID,Name,Price 1,Spiky Pung,1000 2,Gloop Galloop Soup,25 4,Hat Sauce,12
  7. Save the file as SomeData.txt in the CommaValues project directory.

  8. Run the application — you should see the text of the file written to the console, as shown in Figure 22-6.

    image from book
    Figure 22-6

How It Works

Like the previous example, this application reads the file line by line into a string. However, since you know this is a file containing comma-separated text values, you are going to handle it differently. Not only that, but you will actually store the values you read in a data structure.

First, you need to look at some of the comma-separated data itself:

ProductID,Name,Price 1,Spiky Pung,1000

Note that the first line holds the names of the columns of data and subsequent lines hold the data. Thus, your procedure will be to obtain the column names from the first line of the file and then proceed to retrieve the data in the remaining lines.

Now look at the GetData() method — this method is declared as static, so you can call this method without creating an instance of your class. This method returns a List<Dictionary<string, string>> object that you will create and then populate with data from the comma-separated text file. It also returns a List<string> object containing the header names. The following lines initialize these objects:

List<Dictionary<string, string>> data = new List<Dictionary<string, string>>(); columns = new List<string>();

columns will contain the column names from the first row of the comma-separated text file, and data will hold the values on subsequent rows.

You start by creating a FileStream object and then construct a StreamReader around that as you did in earlier examples. Now you can read the first line of the file and create an array of strings from that one string:

strLine = sr.ReadLine(); strArray = strLine.Split(charArray);

You saw the Split() method in Chapter 5 — it accepts a character array, in this case consisting of just "," so that strArray will hold the array of strings formed from splitting strLine at each instance of ",". Since you are currently reading from the first line of the file, and this line holds the names of the columns of data, you need to loop through each string in strArray and add it to columns:

for (int x = 0; x <= strArray.GetUpperBound(0); x++) {    columns.Add(strArray[x]); }

Now that you have the names of the columns for your data, you can read in the data. The code for this is essentially the same as that for the earlier StreamRead example, except for the presence of the code required to add Dictionary<string, string> objects to data:

strLine = sr.ReadLine(); while (strLine != null) {    // Split row of data into string array.    strArray = strLine.Split(charArray);    Dictionary<string, string> dataRow = new Dictionary<string, string>();        for (int x = 0; x <= strArray.GetUpperBound(0); x++)    {       dataRow.Add(columns[x], strArray[x]);    }        data.Add(dataRow);        strLine = sr.ReadLine(); }

For each line in the file, you create a new Dictionary<string, string> object and fill it with a row of data. Each entry in this collection has a key corresponding to a column name and a value that is the value of the column for that row. The keys are extracted from the columns object you created earlier, and the values come from the string array obtained using Split() for the line of text extracted from the data file.

Once you've read all the data in from the file, you close the StreamReader and return your data.

The code in the Main() method obtains the data from the GetData() method in variables called myData and columns, and displays this information to the console. First, the name of each column is displayed:

foreach (string column in columns) {    Console.Write("{0,-20}", column); } Console.WriteLine();

The -20 part of the formatting string {0,-20} ensures that the name you display is left-aligned in a column of 20 characters — this will help to format the display.

Finally, you loop through each Dictionary<string, string> object in the myData collection and display the values in that row, once again using the formatting string to format your output.

foreach (Dictionary<string, string> row in myData) {    foreach (string column in columns)    {       Console.Write("{0,-20}", row[column]);    }    Console.WriteLine(); }

As you can see, it is very simple to extract meaningful data from comma-separated value (CSV) files using the .NET Framework. This technique is also easy to combine with the data access techniques you will see in Chapter 24, meaning that data from a CSV file can be manipulated just like any other data source (such as a database). However, there is no information about the data types of the data extracted from the CSV file. Currently you've just been treating all data as strings, but for an enterprise-level business application, you will need to go the extra step of adding type information to the data you extract. This could come from additional information stored in the CSV file, it could be configured manually, or it could be inferred from the strings in the file, all depending on the specific application

Even though XML, which you'll be looking at in the next chapter, is a superior method of storing and transporting data, you will find that CSV files are still very common and will be for quite some time. Delimited files such as comma-separated files also have the advantage of being very terse and, therefore, smaller than their XML counterparts.

image from book

Reading and Writing Compressed Files

Often when dealing with files you will find that quite a lot of space is used up on your hard disk. This is particularly true for graphics and sound files. You've probably come across utilities that enable you to compress and decompress files, which comes in handy when you want to move them around or e-mail them to your friends. the System.IO.Compression namespace contains classes that enable you to compress files from your code, either using the GZIP or Deflate algorithm — both of which are publicly available and free for anyone to use.

There is a little bit more to compressing files than just compressing them though. Commercial applications will allow multiple files to be placed in a single compressed file and so on. What you'll be looking at in this section is much simpler — you'll just be saving text data to a compressed file. You are unlikely to be able to access this file in an external utility. However, the file will be much smaller than its uncom- pressed equivalent!

The two compression stream classes in the System.IO.Compression namespace that you'll look at here, DeflateStream and GZipStream, work in very similar ways. In both cases, you initialize them with an existing stream, which in the case of files will be a FileStream object. After this you can use them with StreamReader and StreamWriter just like any other stream. All you need to specify on top of that is whether the stream will be used for compression (saving files) or decompression (loading files) so that the class knows what to do with the data that passes through it.

This is best illustrated with the following example.

Try It Out – Compressed Data

image from book
  1. Create a new console application called Compressor in the directory C:\BegVCSharp\ Chapter22.

  2. Place the following lines of code near the top of Program.cs. You need to import the System.IO namespace for your file handling and System.IO.Compression to use the compression classes:

    using System; using System.Collections.Generic; using System.Text; using System.IO; using System.IO.Compression; 
  3. Now add the following methods into the body of Program.cs, before the Main() method:

     static void SaveCompressedFile(string filename, string data) { FileStream fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write); GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Compress); StreamWriter writer = new StreamWriter(compressionStream); writer.Write(data); writer.Close(); } static string LoadCompressedFile(string filename) { FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read); GZipStream compressionStream = new GZipStream(fileStream, CompressionMode.Decompress); StreamReader reader = new StreamReader(compressionStream); string data = reader.ReadToEnd(); reader.Close(); return data; } 

  4. Now add the following code to the Main() method:

    static void Main(string[] args) { try { string filename = "compressedFile.txt"; Console.WriteLine( "Enter a string to compress (will be repeated 1000 times):"); string sourceString = Console.ReadLine(); StringBuilder sourceStringMultiplier =  new StringBuilder(sourceString.Length * 100); for (int i = 0; i < 100; i++) { sourceStringMultiplier.Append(sourceString); } sourceString = sourceStringMultiplier.ToString(); Console.WriteLine("Source data is {0} bytes long.", sourceString.Length); SaveCompressedFile(filename, sourceString); Console.WriteLine("\nData saved to {0}.", filename); FileInfo compressedFileData = new FileInfo(filename); Console.WriteLine("Compressed file is {0} bytes long.", compressedFileData.Length); string recoveredString = LoadCompressedFile(filename); recoveredString = recoveredString.Substring(0, recoveredString.Length / 100); Console.WriteLine("\nRecovered data: {0}", recoveredString); Console.ReadKey(); } catch (IOException ex) { Console.WriteLine("An IO exception has been thrown!"); Console.WriteLine(ex.ToString()); Console.ReadKey(); } }

  5. Run the application and enter a suitably long string. The result is shown in Figure 22-7.

    image from book
    Figure 22-7

  6. Open compressedFile.txt in Notepad. The text is shown in Figure 22-8.

    image from book
    Figure 22-8

How It Works

In this example, you define two methods for saving and loading a compressed text file. The first of these, SaveCompressedFile(), is as follows:

static void SaveCompressedFile(string filename, string data) {    FileStream fileStream =       new FileStream(filename, FileMode.Create, FileAccess.Write);    GZipStream compressionStream =       new GZipStream(fileStream, CompressionMode.Compress);    StreamWriter writer = new StreamWriter(compressionStream);    writer.Write(data);    writer.Close(); }

The code starts by creating a FileStream object, then uses this to create a GZipStream object. Note that you could replace all occurrences of GZipStream in this code with DeflateStream — the classes work in the same way. You use the CompressionMode.Compress enumeration value to specify that data is to be compressed, and then use a StreamWriter to write data to the file.

LoadCompressedFile() mirrors SaveCompressedFile() method. Instead of saving to a filename, this method loads a compressed file into a string:

static string LoadCompressedFile(string filename) {    FileStream fileStream =       new FileStream(filename, FileMode.Open, FileAccess.Read);    GZipStream compressionStream =       new GZipStream(fileStream, CompressionMode.Decompress);    StreamReader reader = new StreamReader(compressionStream);    string data = reader.ReadToEnd();    reader.Close();    return data; }

The differences are as you would expect — different FileMode, FileAccess, and CompressionMode enumeration values to load and uncompress data, and the use of a StreamReader to get the uncom- pressed text out of the file.

The code in Main() is a simple test of these methods. It simply asks for a string, duplicates the string 100 times to make things interesting, compresses it to a file, then retrieves it. In the example the opening stanza of Sir Gawain and the Green Knight repeated 100 times is 17,800 characters long, but when compressed only takes up 441 bytes; a compression ration of around 40:1. Admittedly, this is a bit of a cheat — the GZIP algorithm works particularly well with repetitive data, but this does illustrate compression in action.

You also looked at the text stored in the compressed file. Obviously, it isn't easily readable. This has implications, should you wish to share data between applications for example. However, since the file has been compressed with a known algorithm at least you know that it is possible for applications to uncompress it.

image from book




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

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