Working with Directories and Files


The .NET Framework class library makes working with directories and files an easy and painless experience. It provides an easy-to-understand set of classes located in the System.IO namespace. These classes can be used to:

  • Retrieve and change information about directories and files.

  • Manipulate paths, including combining them and extracting individual elements.

  • Read and write bytes of data from generic streams such as files and memory buffers.

It's important to understand early on that the classes in System.IO are not designed for working just with the file system. They work with any number of backing stores that are accessed using stream objects. A backing store is the .NET Framework term used to define a source which data can be read from or written to using a stream object. Each backing store provides a Stream object that is used to communicate with it. For example, the FileStream class (the Stream object) can be used to read and write data to the file system (the backing store), and the MemoryStream class can be used to read and write data to memory.

All stream classes derive from a common Stream base class, and (just like the collection interfaces described in the previous chapter) once you know what the common System.IO classes are and how they're organized, you'll find working with new data sources a breeze .

Class Overview

The following classes are commonly used when working with directories, files, and streams:

Class

Description

Directory

Provides static (shared) methods for enumerating directories and logical drives

DirectoryInfo

Used to work with a specific directory and its subdirectories

File

Provides static methods for working with files

FileInfo

Used to work with a specific file

Stream

Base class used to read from and write to a backing store, such as the file system or network

StreamReader

Used in conjunction with a stream to read characters from backing store

StreamWriter

Used in conjunction with a stream to write characters to a backing store

TextReader

Abstract class used to define methods for reading characters from any source (backing store, string, and so on)

TextWriter

Abstract class used to define methods to write characters to any source

BinaryReader

Used to read primitive types such as strings, integers, and Booleans from a stream

BinaryWriter

Used to write primitive types such as strings, integers, and Booleans to a stream

FileStream

Used to read and write data in the file system

MemoryStream

Used to read and write data in a memory buffer

DirectoryInfo and Directory

The base class library provides two classes for working with directories: Directory and DirectoryInfo . The Directory class contains a number of static methods (in VB.NET. these are known as shared methods) that can be used to manipulate and query information about any directory. The DirectoryInfo class contains a series of instance methods (also known as non-static or non-shared methods) and properties that can be used to manipulate and work with a single named directory.

For the most part, these classes have equivalent functionality and can be used to:

  • Create and delete directories.

  • Determine if a directory exists.

  • Get a list of subdirectories and files for a given directory.

  • Get information about directories, such as creation times and attributes, and modify it.

  • Get and set the current working directory ( Directory class only).

  • Get a list of available drives ( Directory class only).

  • Move directories.

Having two classes, although confusing at first, actually simplifies and increases the performance of your applications. For example, to determine whether a given directory existed, you could use the static Exists method of the Directory class as follows (written here in VB.NET):

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   If Directory.Exists("C:\Wrox") Then   Response.Write("C:\Wrox directory exists")   Else   Response.Write("C:\Wrox directory does not exist")   End If   %>  

The Exists method is static, so declaring a variable and instantiating an instance of the Directory class is not necessary. This makes the code more readable, and also saves a few CPU cycles.

Note

The constructor of the Directory class is declared as private, so it is not possible to instantiate an instance of the class.

To check if a directory exists using the DirectoryInfo class, you have to instantiate an instance of the DirectoryInfo class, passing the directory name you want into the constructor. Then call the Exists property. Using VB.NET, you would write:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim dir As DirectoryInfo   dir = New DirectoryInfo("C:\Wrox")   If dir.Exists = True Then   Response.Write("C:\Wrox directory exists")   Else   Response.Write("C:\Wrox directory does not exist")   End If   %>  

As the DirectoryInfo class has instance members (that is, they are not static) you have to use an object reference to access them. If all you want to do is check for the existence of a directory, using DirectoryInfo is overkill “ you'd be better off using the Directory class. However, if you want to perform several operations against a single directory, then using the DirectoryInfo class is the correct approach. Use of this class means that the readability and general style of the code is improved, as demonstrated by this additional line of code that displays the creation time of a directory (if it exists):

 <%@ Page Language="VB" %> <%@ Import Namespace="System.IO" %> <% Dim dir As DirectoryInfo dir = New DirectoryInfo("C:\Wrox") If dir.Exists = True Then    Response.Write("C:\Wrox directory exists")  Response.Write("<br />Created: " & dir.CreationTime)  Else    Response.Write("<br />C:\Wrox directory does not exist") End If %> 

Instantiating an object in this way and then using its members, or passing it as a parameter to method calls is a fundamental concept in object-oriented programming. This is something familiar from classic ASP, where objects like ADO Connection or Recordset were used. To write a method to display the contents of a directory in ASP.NET, I'd probably design the method to accept a DirectoryInfo object rather than a string that represented the directory name. It looks neater, feels right, and can have performance benefits if the method was going to use the DirectoryInfo class to do a lot of work. Also, why create a new instance of the DirectoryInfo class when the caller might already have one?

Another subtler benefit of using the DirectoryInfo class is that it will typically execute multiple operations against a single directory in an efficient manner. Once instantiated , it can maintain state such as the creation time and last modification date of a directory. Then, when members such as the CreationTime property are used, this state can be used to provide the results. The Directory class cannot do this. It must go out and retrieve information about a directory each time a method is called.

Although traditionally this wasn't a terribly expensive operation, with the advent of the CLR this type of operation requires code access permissions to be granted by the runtime, which means that the runtime has to ensure that the code calling the method is allowed to know about the directory. These checks can be relatively expensive to perform and their use should be minimized. Accordingly, using the DirectoryInfo class wherever possible makes good coding sense. The DirectoryInfo class performs different code access permission checks depending on the methods called. While some methods will not cause permission checks, others, such as Delete , always will.

File and FileInfo

You can use the File and FileInfo classes to discover information about files, as well as to get access to a stream object that allows you to read from and write to the contents of a file.

The File and FileInfo classes provide equivalent functionality and can be used to:

  • Create, delete, open , copy, and move files (these classes are not used to read, write, append to, or close files).

  • Retrieve information “ such as creation times and attributes “ about files, and modify it.

Like the Directory class, the File class has a series of static methods to manipulate or query information about a file. The FileInfo class has a series of instance methods and properties that can be used to manipulate and work with a single named file.

Here is a simple (VB.NET) code example that shows how to use the File class to determine if a file exists:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   If File.Exists("C:\Wrox\Hello.txt") = True Then   Response.Write("C:\Wrox\Hello.Txt file exists")   Else   Response.Write("C:\Wrox\Hello.Txt file does not exist")   End If   %>  

The Exists method returns true if the file exists, and false if it does not. Here is the equivalent VB.NET code using FileInfo , although this time the file's creation time is also shown (as in the earlier DirectoryInfo sample):

 <%@ Page Language="VB" %> <%@ Import Namespace="System.IO" %> <%  Dim myfile As FileInfo   myfile = New FileInfo("C:\Wrox\Hello.Txt")   If myfile.Exists = True Then  Response.Write("C:\Wrox\Hello.Txt file exists")  Response.Write("<br />Created: " & myfile.CreationTime)  Else    Response.Write("<br />C:\Wrox\Hello.Txt file does not exist") End If %> 

As with the DirectoryInfo class, FileInfo is the preferred class to use when you need to perform multiple operations as it results in greater readability, style, and performance.

Common Directory and File Tasks

Having introduced the various directory and file classes, let's look at some examples of how they can be used to perform common tasks, as well as some of the common exceptions that can be thrown.

Setting and Getting the Current Directory

When an ASP.NET page is executed, the thread used to execute the code that generates the page will, by default, have a current working directory of %windir%\system32 . If you pass a relative filename into any class in the System.IO namespace, the file is assumed to be located within the current working directory.

Retrieving and changing the current working directory is a function of the Directory class. The following example shows how the working directory can be changed using SetCurrentDirectory and retrieved again using GetCurrentDirectory :

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Directory.SetCurrentDirectory("C:\Wrox")   Response.Write("The current directory is " & _   Directory.GetCurrentDirectory())   %>  

When writing an ASP.NET page, make no assumptions about the current working directory. Typically, you should never need to change it, since you should not use relative filenames within ASP.NET pages. Rather, you should use the Server.MapPath method to create a fully qualified filename from a relative filename.

Common Exceptions

In most of the code samples for this chapter, exception handling is not included. This is done to keep the examination of the methods as clear as possible. However, like most other classes in .NET, the System.IO classes throw exceptions when an error condition occurs. The most common exceptions include:

  • IOException : Indicates that a general problem has occurred during the method.

  • ArgumentException : Indicates that one or more of the method input parameters are invalid.

  • UnauthorizedAccessException : Indicates that a specified directory, file, or other resource is read-only and cannot be accessed or modified.

  • SecurityException : Indicates that the code calling the method doesn't have enough runtime privileges to perform the operation.

When writing production code, always use exception handling, as discussed in Chapter 22.

Listing Available Logical Drives

The GetLogicalDrives method of the Directory class returns a string array that contains a list of the available drives. Using VB.NET, you could write:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim Drives() As string   Dim Drive As string   Drives = Directory.GetLogicalDrives()   For Each Drive in Drives   Response.Write(drive)   Response.Write("<br />")   Next   %>  

This code displays the server-side logical drives returned by the method call, as seen in Figure 16-1 (your system will probably display drives different from these):

click to expand
Figure 16-1:

Creating a Directory

The following VB.NET code shows how to create a hierarchy of directories in a single method call by using Directory.CreateDirectory :

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Directory.CreateDirectory("C:\Create\Several\Directories")   %>  

When the CreateDirectory method is called, it first checks if the C:\Create directory exists; it will be created if it doesn't exist. Next, the method will check if the Several directory exists within the Create directory. Again, it will be created if it doesn't exist. Finally, the method will check if the Directories directory exists within the Several directory, again creating it if it doesn't exist. The DirectoryInfo class also has a Create method that provides the same functionality.

If you try to create a directory that already exists, an exception will not be thrown. An ArgumentException will be thrown only if part of the directory path is invalid. Use the Directory.Exists method to determine if a directory exists.

Listing the Contents of a Directory

The Directory class has the following methods that can be used to retrieve a list of a directory's contents:

Method Name

Parameters

Description

GetDirectories

Pathname

Returns a string array filled with the fully qualified names of each contained directory

GetDirectories

Pathname, Search path

Returns a string array filled with the fully qualified names of each contained directory that matches the search pattern

GetFiles

Pathname

Returns a string array filled with the fully qualified names of each contained file

GetFiles

Pathname, Search path

Returns a string array filled with the fully qualified names of each contained file that matches the search pattern

GetFile SystemEntries

Pathname

Returns a string array filled with fully qualified names of each contained directory and file

GetFile SystemEntries

Pathname, Search path

Returns a string array filled with the fully qualified names of each contained directory and file that matches the search pattern

The following VB.NET code demonstrates how to use the GetDirectories method:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim dir As string   Dim subdirs() As string     ' Get all child directories of C:\ and enumerate each one   subdirs = Directory.GetDirectories("c:\")   For Each dir In subdirs   Response.Write(dir & "<br />")   Next     ' Get all child directories that start with a 't' and enumerate each one   subdirs = Directory.GetDirectories("c:\","t*")   For Each dir In subdirs   Response.Write(dir & "<br />")   Next   %>  

The following code demonstrates how to use the GetFiles method:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim f As string   Dim files() As string   files = Directory.GetFiles("C:\Wrox\")   For Each f In files   Response.Write(f & "<br />")   Next   files = Directory.GetFiles("C:\Wrox\","h*")   For Each f in files   Response.Write(f & "<br />")   Next   %>  

The following code demonstrates how to use the GetFileSystemEntries method:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim item As string   Dim items() As string   ' Get all files & directories in C:\Wrox and enumerate them   items = Directory.GetFileSystemEntries("C:\Wrox\")   For Each item In items   Response.Write(item & "<br />")   Next   ' Get all files & directories in C:\Wrox starting with 'h' and enum them   items = Directory.GetFileSystemEntries("C:\Wrox\","h*")   For Each item in items   Response.Write(item & "<br />")   Next   %>  

The DirectoryInfo class also has GetDirectories , GetFiles , and GetFileSystemEntries methods. These provide equivalent functionality, but with two important differences:

  • No pathname is passed as an argument to these methods, as the class already knows the path (it was passed in as a parameter to the constructor).

  • These methods do not return string arrays. The GetDirectories method returns an array of DirectoryInfo . The GetFiles method returns an array of FileInfo . The GetFileSystemEntries method returns an array of FileSystemInfo (which will be discussed shortly).

Deleting a Directory

A directory can be deleted using the Directory.Delete or DirectoryInfo.Delete methods. For example, you could write the following VB.NET code:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Directory.Delete("C:\Create")   Dim dir As DirectoryInfo   dir = New DirectoryInfo("C:\Create")   dir.Delete()   %>  

If you attempt to delete a non-existent directory, a DirectoryNotFound exception will be thrown. If you attempt to delete a directory that contains other files or directories, an IOException will be thrown, unless you use an overloaded version of the Delete method that allows you to specify whether any contained files or directories should also be deleted. For example:

 <%@ Page Language="VB" %> <%@ Import Namespace="System.IO" %> <%    Directory.Delete("C:\Create",True)    Dim dir As DirectoryInfo    dir = New DirectoryInfo("C:\Create")  dir.Delete(True)  %> 

Deleting a File

You can delete a file using the File.Delete or FileInfo.Delete methods. For example:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   File.Delete("C:\myfile.txt")   Dim f As FileInfo   f = New FileInfo("myfile.txt")   f.Delete()   %>  

If you attempt to delete a file that does not exist, no exceptions are thrown unless part of the path does not exist (in which case, a DirectoryNotFoundException is thrown).

Properties and Attributes of Files and Directories

Directories and files share common operations (such as deleting them) that can be performed on them. They also share common properties, such as their creation time, fully qualified name, and attributes.

The FileSystemInfo class defines members that are common to both files and directories. Both the DirectoryInfo and FileInfo classes are derived from this class. The FileSystemInfo class has the following properties:

Name

Type

Read/ Write

Description

Attributes

FileAttributes

Read/ Write

The attributes such as hidden , archive , and read- only that are set for this file.

CreationTime

System.DateTime

Read/ Write

The time that the file or directory was created.

LastAccessTime

System.DateTime

Read/ Write

The time that the file or directory was last accessed.

LastWriteTime

System.DateTime

Read/ Write

The time that the file or directory was last updated.

Exists

Boolean

Read

Indicates if the file or directory exists.

Extension

String

Read

Returns the file or directory extension, including the period. For a directory, the extension is the text located after last period in the name.

Name

String

Read

Returns the name of the file/directory relative to its containing directory. This includes the extension.

FullName

String

Read

Returns the fully qualified name for the file or directory.

The FileSystemInfo class has the following methods:

Name

Description

Delete

Deletes the file or directory

Refresh

Updates any cached state such as creation time and attributes with those present on disk

Exists

Determines whether the file or directory exists

To know how to use attributes, and some interesting methods and properties of the DirectoryInfo and FileInfo classes, let's take a look at the code required to write a simple Web-based file browser. Here the file browser is being used to display information about the C:\program files\internet explorer directory, as shown in Figure 16-2:

click to expand
Figure 16-2:

This application takes a path and then lists any directories and files it contains. It also displays the last time that each directory and file was modified, and their various attributes (such as whether they have been archived). The application uses an HTML form to capture the path to be examined. This has an input control ( marked as a server control using the runat ="server" attribute) with an id of DirName :

  <form runat="server">   Directory Name: <input type="text" id="DirName" size="60"   value="c:\program files\internet explorer"   runat="server">   <input type="submit" value="List">   </form>  

When the page is rendered, it uses the DirName.Value server control property to initialize an instance of the DirectoryInfo class:

  Dim dir As DirectoryInfo   Dim anchor As String   dir = New DirectoryInfo(DirName.Value)  

The DirectoryInfo class is used rather than the Directory class, since you want to display details about the contained directories and files, such as their last modification date. The GetDirectories method of Directory does not give this information “it only provides the name.

The first block of rendering logic for the application outputs a table that lists the name of the directory being listed and its subdirectories. The subdirectories are retrieved using the GetDirectories method of the dir object:

  Response.Write("<h3>Sub Directories in " & DirName.Value & "</h3>")   Response.Write("<table>")   Response.Write("<tr bgcolor=cornflowerblue>")   Response.Write("<td>Name</td>")   Response.Write("<td>Last Modified</td>")   Response.Write("</tr>")   Dim SubDir as DirectoryInfo   For Each SubDir In dir.GetDirectories()   anchor = "<a href='" & "default.aspx?dir=" & _   SubDir.FullName & "'>" & SubDir.Name & "</a>"   Response.Write("<tr>")   Response.Write("<td>" & anchor & "</td>")   Response.Write("<td>" & SubDir.LastWriteTime & "</td>")   Response.Write("</tr>")   Next   Response.Write("</table>")  

As you list each contained directory, output an anchor tag that points back to your page with a URL containing a dir parameter that holds the fully qualified name of the subdirectory. This fully qualified name is returned by the FullName property. The actual text of the anchor is just the directory's relative name within its parent, which is accessed using the Name property:

  For Each SubDir In dir.GetDirectories()   anchor = "<a href='" & "default.aspx?dir=" & _   SubDir.FullName & "'>" + SubDir.Name & "</a>"   Next  

If the dir parameter is present in the query string when a postback occurs, the Page_Load event handler sets the value of the DirName.Text property to the value of dir . This allows the application to navigate down to subdirectories and to list their contents:

  <script runat="server">   Sub Page_Load(sender As Object, e As EventArgs)   If Not Request.Form("dir") Is Nothing Then   DirName.Value = Request("dir")   End If   End Sub   </script>  

The next section of the page has an anchor tag that displays the parent directory of that being listed. This is determined using the Parent property. This value will be null if there isn't a parent directory, so the following code checks for this:

  If (Not dir.Parent Is Nothing) Then   anchor = "<a href='" & "default.aspx?dir=" & dir.Parent.FullName & _   "'>" & dir.Parent.FullName & "</a>"   Response.Write("<p>Parent directory is " + anchor)   End If  

The parent directory is displayed using an anchor tag, which also uses the dir parameter, this time to allow the user to navigate up from the current directory to the parent directory.

The final section of the page uses the GetFiles method (see the sourcecode for details) to list the files within the directory. Apart from displaying the name and last modified date of the file, this code also shows what attributes are set on the file (such as if it's a system file or is hidden). These attributes are available from the Attributes property of the FileInfo class (which returns a FileAttributes enumeration). The code uses the bit-wise and operator to determine if these attributes are set for a given file. If they are, some simple custom formatting is done to shows its presence:

  Dim f as FileInfo   For Each f in dir.GetFiles()   Response.Write("<tr>")   Response.Write("<td>" & f.Name)     If ((f.Attributes And FileAttributes.ReadOnly) <> 0) Then   Response.Write(" (read only)")   End If   If ((f.Attributes And FileAttributes.Hidden) <> 0) Then   Response.Write(" (hidden)")   End If   If ((f.Attributes And FileAttributes.System) <> 0) Then   Response.Write(" (system)")   End If   If ((f.Attributes And FileAttributes.Archive) <> 0) Then   Response.Write(" (archive)")   End If   Response.Write("<td>" & f.LastWriteTime & "</td>")   Response.Write("<td>" & f.Length.ToString() & "</td>")   Response.Write("</tr>")   Next  

All enumeration types support the ability to convert a numeric enumeration value into a text value. This is a very useful technique for use in debugging. If you don't want any custom formatting in your application, replace your explicit checks for given attributes with a call to the ToString method. Then the enumeration type will do the conversion for you. For example, this would list out each of the attributes specified separated by a comma:

  Response.Write(f.Attributes.ToString())  

Working with Paths

When working with files and directories, you often need to manipulate paths. The Path class allows you to:

  • Extract the elements of a path, such as the root path, directory, filename, and extension

  • Change the extension of a file or directory

  • Combine paths

  • Determine special characters, such as the path and volume separator characters

  • Determine if a path is rooted or has an extension

The Path class has the following methods:

Method

Parameters

Description

ChangeExtension

Path , Extension

Takes a path (with or without an extension) and a new extension (with or without the period) as input and returns a new path with the new extension.

Combine

Path1 , Path2

Concatenates two paths. The second path should not be rooted. For example, Path.Combine("c:\rich", " anderson ") returns c:\rich\anderson.

GetDirectoryName

Path

Returns the directory or directories within the path.

GetExtension

Path

Returns the extension of the path (if present).

GetFileName

Path

Returns the filename if present.

GetFileName WithoutExtension

Path

Returns the filename without its extension.

GetFullPath

Path

Given a non-rooted path, returns a rooted path name based on the current working directory. For example, if the path was test and the working directory was c:\wrox , the return path would be c:\wrox\test .

GetPathRoot

Path

Returns the root path (excludes any filename).

GetTempFileName

None

Returns a temporary filename, located in the temporary directory returned by GetTempPath .

GetTempPath

None

Returns the temporary directory name.

HasExtension

Path

Returns a Boolean value that indicates whether a path has an extension or not.

IsPathRooted

Path

Returns a Boolean value that indicates if a path is rooted or not.

The Path class uses a number of static constants to define the special characters that are used with paths (the values shown in the table are for the Windows platform):

Constant

Type

Description

DirectorySeparatorChar

Char

The default character used to separate directories within a path. Returns the backslash character \ .

AltDirectorySeparatorChar

Char

Alternative character that can be used to separate directories within a path. Returns forward slash character / .

PathSeparator

Char

The character used when a string contains multiple paths. This returns the semicolon character ; .

VolumeSeparatorChar

Char

The character used to separate the volume name from the directory and/or filename. This returns the colon character : .

InvalidPathChars

Char array

Returns all of the characters that cannot be used in a path because they have special significance.

The application shown in Figure 16-3 accepts a path, and displays the component parts of that path:

click to expand
Figure 16-3:

Note the entire path including the root path (logical drive), the directory, filename, and extension.

The code for this page shows how to use the various methods and constant properties of the Path class:

  If (Page.IsPostBack) Then   Response.Write("<br />Root Path = ")   Response.Write(Path.GetPathRoot(PathName.Text))   Response.Write("<br />Directory = ")   Response.Write(Path.GetDirectoryName(PathName.Text))   Response.Write("<br />Filename = ")   Response.Write(Path.GetFileName(PathName.Text))   Response.Write("<br />Filename (without extension) = ")   Response.Write(Path.GetFileNameWithoutExtension(PathName.Text))   If (Path.HasExtension(PathName.Text)) Then   Response.Write("<br />Extension = ")   Response.Write(Path.GetExtension(PathName.Text))   End If     Response.Write("<br />Temporary Directory = ")   Response.Write(Path.GetTempPath())   Response.Write("<br />Directory Separator Character = ")   Response.Write(Path.DirectorySeparatorChar)   Response.Write("<br />Alt Directory Separator Character = ")   Response.Write(Path.AltDirectorySeparatorChar)   Response.Write("<br />Volume Separator Character = ")   Response.Write(Path.VolumeSeparatorChar)   Response.Write("<br />Path Separator Character = ")   Response.Write(Path.PathSeparator)   Response.Write("<br />Invalid Path Characters = ")   Response.Write(HttpUtility.HtmlEncode(new String(Path.InvalidPathChars)))   End If  

Here the HttpUtility.HtmlEncode method is used to encode the Path.InvalidPathChars character array so that the characters it contains are suitable for display within HTML. This is done because the characters returned would otherwise be interpreted as HTML elements (the returned character array contains the greater than > and less than < characters).

Reading and Writing Files

The File and FileInfo classes provide a number of helper methods that can open and create files. These methods don't actually perform the reading and writing of files, rather they instantiate and return other classes such as:

  • FileStream :For reading and writing bytes of data to and from a file

  • StreamReader :For reading characters from a stream

  • StreamWriter :For writing characters to a stream

The following code example shows how to open a text file using the static OpenText method of the File class and then read several lines of text from it:

  <%@ Import Namespace="System.IO" %>   <html>   <body>   <%   Dim myfile As StreamReader   Dim name As String   myfile = File.OpenText(Server.MapPath("names.txt"))   name = myfile.ReadLine()   Do While Not name Is Nothing   Response.Write(name & "<br />")   name = myfile.ReadLine()   Loop     myfile.Close()   %>   </body>   </html>  

Here the File.OpenText method is used to open the names.txt file. If successful, this method returns a StreamReader object that can be used to read characters (not bytes) from the file. The code uses the ReadLine method, which reads all characters up to the next carriage return line feed. Although this method reads the carriage return line feeds from the stream, they are not returned as part of the return string. When the end of the file is reached, a null string ( Nothing in VB.NET) is returned. This is checked and used to terminate the While loop. Calling the Close method closes the file.

Note

To ensure that the code remains scalable, you should always close files as soon as possible.

The following code shows how to create a new text file and write a few lines to it:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim books As StreamWriter   books = File.CreateText(Server.MapPath("books.txt"))   books.WriteLine("Professional ASP.NET")   books.WriteLine("Professional C#")   books.Close()   %>  

Here, the File.CreateText method is used to create a new file. This method returns a StreamWriter object that you can use to write data to the file. You then call the WriteLine method of the object (which is inherited from the base class, TextWriter ) and output the names of the two books. Finally, the Close method is called to close the connection to the file.

Once you've written code to read or write data from a backing store (such as the file system) using the StreamReader or StreamWriter classes, you can easily read and write character data from other backing stores (such as memory buffers or network connections) using the same classes. This consistency makes working with streams of data easy.

The main role of the StreamReader and StreamWriter classes is essentially to convert bytes of data into characters. Different character encoding types, such as Unicode, ASCII, or UTF-8, use different byte sequences to represent their characters, but no matter where bytes are read from, or written to, the same translations are performed, so it makes sense to always use the same classes for this purpose. To support this, the classes read and write bytes of data using a Stream class, as shown in the Figure 16-4:

click to expand
Figure 16-4:

This generic model is very powerful. To support reading character data from different backing stores, all you require is a stream object for each backing store. Each of these stream objects inherits from the Stream class and overrides several abstract methods that can be used to read and write bytes of data, provide the current position in the stream as well as change it, determine the length of the stream, and expose the capabilities of the backing store (for example, whether it is read-only, or write-only). Figure 16-5 shows how reading and writing from the file system, network sockets, and memory buffers is supported by this model:

click to expand
Figure 16-5:

The FileStream , NetworkStream , and MemoryStream classes all derive from the Stream class.The StreamReader and StreamWriter classes contain a reference to the stream object they use to access the associated backing store. This reference is held in the BaseStream property (defined as type Stream ). If you had a reference to a StreamReader and knew the backing store was actually a FileStream , you could use this property to get a reference to the original FileStream object:

  Dim myfile As StreamReader   Dim backingStore As FileStream   ' assuming backingStore and myfile are already initialized...   backingStore = CType(myfile.BaseStream,FileStream)   backingStore.Seek(0,SeekOrigin.Begin)  

The capabilities of a stream object will depend on the backing data store. For example, if you're using a StreamReader to read data from a socket (for example, a web page over HTTP), you cannot change the position of the stream since you cannot push data back into a socket once it has been read. To determine the capability of a backing store, the Stream class has a number of read-only properties:

  • CanRead : Determines if data can be read from a stream. If this property returns true , the Read method can be used to read a specified number of bytes from the Stream into a byte array at a given offset, or the ReadByte method can be used to read a single byte.

  • CanWrite : Determines if data can be written to a stream. If this property returns true , the Write method can be used to write a specified number of bytes from a byte array to the Stream , or the WriteByte method can be used to write a single byte.

  • CanSeek : Indicates if a stream supports random access. If it does, the Position property of the stream class can be used to set the stream position. Alternatively, the Seek method can be used to set a relative position from the start of the stream, the end of the stream, or the current position of the stream. The SetLength method can also be called to change the size of the underlying backing data store object.

    Note

    Consider Stream in .NET to be the replacement of the IStream interface in COM. In future versions of .NET, the Stream object will automatically expose the IStream interface through COM interop.

FileStream

The FileStream class provides all of the functionality needed for reading and writing data to files. It derives from the Stream class, so it inherits all of the properties and methods just discussed. The FileStream class has the following constructors that can be used to open and create files in various modes:

Parameters

Description

path as string , mode as FileMode

Specifies a path/file and how you want to work with it. FileMode is an enumeration that defines how you want to work with a file, and what actions you want to take if it already exists. The values of FileMode will be covered after this table, when we look at an example of creating a FileStream .

path as string , mode as FileMode , access as FileAccess

As for the previous constructor, but also allows you to specify permissions to read, write, or read and write from the stream. Values for FileAccess are Read , ReadWrite , and Write . The default is ReadWrite .

path as string , mode as FileMode , access as FileAccess , share as FileShare

As with the previous constructor, but also allows you to specify what access other people will have to the file while you're working with it. Values for FileShare are None , Read , ReadWrite , Write , and Inheritable . The default is None (nobody else can access the file).

path as string, mode as FileMode, access as FileAccess, share as FileShare, bufferSize as Integer

As with the previous constructor, but also allows you to specify the size of the internal buffer used to reduce the number of calls to the underlying operation system. The default value is 4KB. You should not change the size of this buffer unless you have good reasons to do so.

path as string, mode as FileMode, access as FileAccess, share as FileShare, bufferSize as Integer, useAsync as Boolean

As with the previous constructor, but also tells the class the application calling it is using asynchronous IO. This can result in better performance for large reads and writes . The default value of this parameter is False.

The following code shows how to create a new text file using the FileStream class:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim fs As FileStream   fs = New FileStream("MyFile.Txt", FileMode.Create)   fs.Close()   %>  

Since the FileMode.Create parameter is specified, any existing file called MyFile.Txt will be truncated (that is, all existing content will be overwritten) when the file is opened. The values of FileMode include:

  • Append : Opens the specified file and seeks to the end of the stream. If a file does not exist, it is created.

  • CreateNew : Creates the specified file. If the file already exists, an IOException is thrown.

  • Create : Creates the specified file, truncating the file content if it already exists.

  • Open : Opens the specified file. If the file doesn't exist, a FileNotFound exception is thrown.

  • OpenToCreate : Opens the specified file, and creates it if it doesn't already exist.

  • Truncate : Opens the specified file and clears the existing contents. If the file doesn't exist, a FileNotFound exception is thrown.

Once a file is opened and you have a FileStream object, you can create a reader or writer object to work with the file's contents. The following code shows how to write a few lines of text to a file using the StreamWriter class:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim fs As FileStream   Dim sw As StreamWriter   fs = New FileStream("MyFile.Txt", FileMode.Create)   sw = New StreamWriter(fs)   sw.WriteLine("Professional ASP.NET")   sw.WriteLine("Professional C#")   sw.Close()   %>  

To use a writer object to write data to a stream, use only one writer. You should never have multiple writers per stream. Writer objects buffer data in an internal cache to reduce the number of calls to the underlying backing store, and having multiple writers active on one stream will result in unpredictable results.

The lifetime of the writer is tied to that of the stream. When the writer is closed, the stream is also closed by the writer, which is why you call sw.Close rather than fs.Close in this code.

When a stream is closed (assuming the writer didn't close it), the writer can no longer write to the stream. The same is true for reader objects. Any attempt to perform an operation on a closed stream will result in an exception.

The following code shows how to open an existing file using the FileStream class and read lines of text from it using the StreamReader class:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim fs As FileStream   Dim sr As StreamReader   Dim line As String   fs = New FileStream("MyFile.Txt", FileMode.Open)   sr = New StreamReader(fs)   line = sr.ReadLine()   Response.Write(line & "<br />")   line = sr.ReadLine()   Response.Write(line & "<br />")   sr.Close()   %>  

In this code the FileMode.Open parameter is being used to tell the FileStream that you're opening an existing file. Use the ReadLine method to read two lines from the file and write them to your ASP.NET page using Response.Write .

MemoryStream

A memory stream allows you to read and write bytes of data from memory. It has several constructors that allow you to initialize the buffer to a given size (default is 256 bytes) that indicate whether the buffer is read-only (and can therefore not be written to), and copy specified data from an existing array.

The following code demonstrates how you can use the MemoryStream class to create a byte array containing the text "Professional ASP.NET" . Although something of an esoteric example, it demonstrates how to use a stream writer to fill the memory stream with some text, and then create a byte array containing that text:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim memstream As MemoryStream   Dim writer As StreamWriter   Dim array() As Byte   memstream = New MemoryStream()   writer = New StreamWriter(memstream)   writer.Write("Professional ASP.NET")   writer.Flush()   array = memstream.ToArray()   writer.Close()   %>  

The StreamWriter class uses an internal 1KB buffer to write blocks of data to the underlying stream (and its associated backing store) more efficiently . Calling its Flush method causes any buffered data to be written to the underlying stream (before the 1KB limit is reached), and resets the buffer to an empty state. The Flush method is automatically called by the Close method of the StreamWriter .

In your code use the ToArray method of MemoryStream to convert the memory buffer into a byte array. You have to explicitly call the Flush method before calling this method, since the amount of data written to the stream using the Write method is less than 1KB. If you didn't call Flush first, you'd simply end up with an empty array, as no data would have actually been written to the memory stream. In this case, you have to call Flush , since calling the ToArray method after the Close method would also result in an empty array, as the memory stream releases its resources (memory) when the Close method is called.

The Capacity property can be used to determine the amount of data a memory stream can hold before it will need to reallocate its buffer. This property can be set to increase or shrink the size of the memory buffer. However, you cannot set the capacity of the memory stream to be less than the current length of the memory stream, as the length reflects how much data has already been written to the memory stream. To determine how much data is currently in a memory stream, use the read-only Length property.

The MemoryStream class automatically manages its own capacity expansion. A memory stream is full it doubles in size, allocating a new buffer and copying the old data across.

Note

When using classes such as StreamWriter to populate a memory stream, the memory stream's Length property will not be accurate until the stream writer's Flush method is called (because of the buffering it performs).

TextReader and TextWriter

The StreamReader class derives from the abstract TextReader class. This class defines the base methods that are useful to applications that need to read character data. It does not define any methods for opening or connecting to an underlying data source (backing store), those are provided by derived classes such as StringReader and StreamReader .

The TextReader class has the methods shown in the following table:

Method Name

Parameters

Description

Close

None

Closes the underlying backing store connection and dispose of any held resources.

Read

None

Reads the next character from the input stream.

Read

Char array , index , count

Reads a specified number of characters from the input stream into an array at the specified offset. The number of characters read is returned.

ReadBlock

Char array , index , count

Reads a specified number of characters from the input stream into an array at the specified offset. The number of characters read is returned. This method will block (that is, the method will not return) until data is available.

ReadLine

None

Returns a string containing the next line of characters.

ReadToEnd

None

Reads all of the remaining content from the input stream into a string. You should not use this method for large streams, as it can consume a lot of memory.

Synchronized

TextReader

Accepts a TextReader object as input and returns a thread-safe wrapper. This is a static method.

One of the reasons the TextReader class exists is so that non-stream-oriented backing stores such as a string can have an interface consistent with streams. It provides a mechanism by which classes can expose or consume a text stream without having to be aware of where the underlying data stream is. The following C# code shows how a function can output the data read from a text-oriented input stream using an ASP.NET page :

  <script runat="server">   protected void WriteContentsToResponse(TextReader r)   {   string line;   line = r.ReadLine();   while (line != null)   {   Response.Write(line);   Response.Write("<br />");   line = r.ReadLine();   }   }   </script>  

This function is passed a TextReader and reads lines of text using the ReadLine method. It then writes that back to the client browser using the Response.Write method. As the HTML standard defines line breaks using the <br /> element, it is written after each line.

The StringReader class derives from TextReader in order to provide a way of accessing the contents of a string in a text-stream-oriented way. The StreamReader class extends TextReader to provide an implementation that makes it easy to read text data from a file. You can derive your own classes from TextReader to provide an implementation that makes it easy to read from your internal data source. This same model is used for the TextWriter class. The StreamWriter class derives from the abstract TextWriter class. StreamWriter defines methods for writing character data. It also provides many overloaded methods for converting primitive types like bool and integer into character data:

Method Name

Parameters

Description

Close

None

Closes the underlying backing store connection and disposes of any resources that are held.

Flush

None

Flushes any buffered data to the underlying backing store.

Synchronized

TextWriter

Accepts a TextWriter object as input and returns a thread safe wrapper. This is a static method.

Write

Numerous overloads

Writes the passed parameter to the underlying data stream. The primitive types string, char, char array, bool, integer, unsigned integer, long, unsigned long, float, and decimal are valid parameter types. If a string and an object parameter are passed, the string is assumed to contain formatting specifications, so the String.Format method is called. There are method overloads for formatting that take either between one and three object parameters, or an array of objects as input.

WriteLine

Numerous overloads

Implemented as per the Write method, but also outputs the carriage return line feed characters.

Following VB.NET code shows how to use the Write method to write formatted strings using the various available overloads:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim myfile As TextWriter   myfile = File.CreateText("c:\authors.txt")   myfile.WriteLine("My name is {0}", "Richard")   myfile.WriteLine("My name is {0} {1}", "Richard", "James")   myfile.WriteLine("My name is {0} {1} {2}", "Richard", "James", "Anderson")   Dim authors(5) as Object   authors(0) = "Alex"   authors(1) = "Dave"   authors(2) = "Rich"   authors(3) = "Brian"   authors(4) = "Karli"   authors(5) = "Rob"   myfile.WriteLine("Authors:{0},{1},{2},{3},{4},{5}", authors)   myfile.Close()   %>  

The contents of the authors.txt file created by this code are:

 My name is Richard My name is Richard James My name is Richard James Anderson Authors:Alex,Dave,Rich,Brian,Karli,Rob 
StringReader and StringWriter

The StringReader derives from the TextReader class and uses a string as the underlying input stream. The string to read from is passed in as a parameter to the constructor.

The StringWriter class derives from the TextWriter class and uses a string as the underlying output stream. For reasons of efficiency, this underlying string is actually built using a string builder. Optionally, you can pass in your StringBuilder object as a constructor parameter if you want to add data to existing strings.

The following code shows how to build a multi-line string using the StringWriter class:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <html>   <body>   <%   Dim sw As StringWriter = New StringWriter()   sw.WriteLine("The Cow")   sw.WriteLine("Jumped Over")   sw.WriteLine("The Moon")   Response.Write("<pre>")   Response.Write(sw.ToString())   Response.Write("</pre>")     sw.Close()   %>   </body>   </html>  

Here, you allocate a StringWriter and use the WriteLine method to build up the contents of the string. Retrieve the string using the ToString method, and render it within an HTML <pre> element to ensure that the carriage return line feeds within the string are not ignored by the browser, as shown in Figure 16-6:

click to expand
Figure 16-6:
Reading and Writing Binary Data

When working with streams of binary data, you often need to read and write primitive types. For this, you can use the BinaryReader and BinaryWriter classes respectively. The following C# code demonstrates how to use a BinaryWriter with a FileStream to write a few primitive types to a file:

  <%@ Page Language="C#" %>   <%@ Import Namespace="System.IO" %>   <%   BinaryWriter bw;   FileStream fs;   string filename;     filename = Server.MapPath("myfile.bin");   fs = new FileStream(filename, FileMode.Create);   bw = new BinaryWriter(fs);     string s = "a string";   long l = 0x123456789abcdef;   int i = 0x12345678;   char c = 'c';   float f = 1.5f;   Decimal d = 100.2m;     bw.Write(s);   bw.Write(l);   bw.Write(i);   bw.Write(c);   bw.Write(f);   bw.Write(d);     fs.Close();   %>  

The following C# code shows how to re-read the created binary file using the BinaryReader class:

  <%@ Page Language="C#" %>   <%@ Import Namespace="System.IO" %>   <%   BinaryReader br;   FileStream fs;   string filename;   filename = Server.MapPath("myfile.bin");   fs = new FileStream(filename, FileMode.Open);   br = new BinaryReader(fs);     string s = br.ReadString();   long l = br.ReadInt64();   int i = br.ReadInt32();   char c = br.ReadChar();   float f = br.ReadSingle();   Decimal d = br.ReadDecimal();   fs.Close();   %>  
Methods of Encoding

The StreamReader class will, by default, attempt to determine the encoding format of a file. If one of the supported methods of encoding (such as UTF-8 or Unicode) is detected , it will be used. If the encoding is not recognized, the default encoding of UTF-8 will be used. Depending on the constructor you call, you can change the default encoding used, and even turn off encoding detection.

The following VB.NET code shows how you can specify a default encoding of Unicode to use to read from a file:

  Dim Reader As StreamReader   Reader = new StreamReader("somefile.txt", System.Encoding.Text.Unicode);  

The default encoding for StreamWriter is also UTF-8, and you can override it in the same manner as the StreamReader class. For example, the following C# code creates a file using each supported encoding:

  <%@Page Language="C#"%>   <%@Import Namespace="System.IO" %>   <%@Import Namespace="System.Text" %>   <%   StreamWriter stream;   char HiChar;   HiChar = (char) 0xaaaa;   stream = new StreamWriter(Server.MapPath("myfile.utf8"), false,   System.Text.Encoding.UTF8);   stream.Write("Hello World");   stream.Write(HiChar);   stream.Close();     stream = new StreamWriter(Server.MapPath("myfile.utf7"), false,   System.Text.Encoding.UTF7);   stream.Write("Hello World");   stream.Write(HiChar);   stream.Close();     stream = new StreamWriter(Server.MapPath("myfile.ascii"), false,   System.Text.Encoding.ASCII);   stream.Write("Hello World");   stream.Write(HiChar);   stream.Close();   stream = new StreamWriter(Server.MapPath("myfile.unicode"), false,   System.Text.Encoding.Unicode);   stream.Write("Hello World");   stream.Write(HiChar);   stream.Close();   %>  

The size of each created file varies due to the way the different methods of encoding work. The largest is the Unicode-encoded file at 26 bytes. The smallest file is the ASCII file at 12 bytes. However, since ASCII encoding can only encode 8-bit characters, and you've got a 16-bit character ( 0xaaaa ) you're actually losing data. Avoid ASCII encoding whenever possible and stick with the default UTF-8 encoding, or use Unicode. UTF-8 encoding is preferred since it typically requires less space than Unicode (17 bytes compared to 26 bytes in this example) and is the standard encoding for Web technologies such as XML and HTML.

BufferedStream

The BufferedStream class reads and writes data to another stream through an internal buffer, the size of which can be specified in the constructor. This class is designed to be composed with other stream classes that do not have internal buffers, enabling you to reduce potentially expensive calls by reading data in large chunks and buffering it. The BufferedStream class should not be used with the FileStream or MemoryStream classes because they already buffer their own data.

Copying between Streams

One of the functions of the stream object not included in version 1.0 of .NET is the ability to write the content of one stream into another. Here is some C# code that shows how it can be implemented:

  public static long Pump(Stream input, Stream output)   {   if (input == null)   {   throw new ArgumentNullException("input");   }   if (output == null)   {   throw new ArgumentNullException("output");   }     const int count = 4096;   byte[] bytes = new byte[count];   int numBytes;   long totalBytes = 0;     while((numBytes = input.Read(bytes, 0, count)) > 0)   {   output.Write(bytes, 0, numBytes);   totalBytes += numBytes;   }   return totalBytes;   }  

This code uses a 4KB buffer to read data from the input stream and write it to the output stream. If the copy is successful, the total number of bytes copied is returned. The method throws an ArgumentNullException if the input parameters are invalid.

Always Call Close, and Watch for Exceptions

In the non-deterministic world of .NET, always make sure that you call the Close method on your streams. If you don't call Close , the time at which the buffered contents of a stream will be written to the underlying backing store is not predictable (due to the way the CLR garbage collector works). Furthermore, since garbage collection does not guarantee the order in which objects are finalized, you may also find that your data is not written correctly and is corrupted. For example, it is possible for a stream to be closed before a writer object has flushed its data.

Because of this non-deterministic behavior, always add exception handling to your code when using streams. There is no performance overhead at runtime for doing this in cases when exceptions are not thrown, and by putting your stream cleanup code in the finally section of the exception handler, you can ensure resources aren't held for an unpredictable amount of time (in the unlikely case that error conditions do arise).

For C# code, it's worth considering the using statement, which can be used to automatically close a stream when it goes out of scope, even if an exception is thrown. The following code shows the using statement in action:

  <%@ Page Language="C#" %>   <%@ Import Namespace="System.IO" %>   <%   FileStream fs = new FileStream("MyFile.Txt", FileMode.Create);   using(fs)   {   //...   }   %>  

In this code you create a file stream, and then begin a new scope by using the using statement. When this using statement is exited (either normally or if an exception occurs), the resources held by the stream are released. Under the hood, the using statement causes code to be generated that calls the IDiposable.Dispose method implemented by the FileStream .

ASP.NET and Streams

The ASP.NET page framework allows you to read and write content to a page using a stream:

  • Page.Response.Output property: Returns a TextWriter that can be used to write text content into the output stream of a page

  • Page.Response.OutputStream property: Returns a Stream object that can be used to write bytes to the output stream of a page

  • The Page.Request.InputStream property: Returns a Stream object that can be used to read bytes of data from a posted request

Suppose content, such as an XML file, was posted to an ASP.NET page. The following VB.NET shows how you could read and display the data using the Page.Request.InputStream property:

  <%@ Page Language="VB" %>   <%@ Import Namespace="System.IO" %>   <%   Dim reader As StreamReader   Dim line As String   reader = New StreamReader(Page.Request.InputStream)   line = reader.ReadLine()   Do While Not line Is Nothing   Response.Write(line & "<br />")   line = reader.ReadLine()   Loop   %>  

Writing Custom Streams

Depending on the type of applications or components that you write, you may want to create your own stream class. Custom streams are fairly easy to write, and can be used just like the other stream classes (such as FileStream) as well as in conjunction with classes like StreamReader and StreamWriter .

There are essentially two types of streams you are likely to write:

  • Streams that provide access to a custom backing store

  • Streams that are composed of other streams in order to provide services such as filtering, compression, or encryption

To implement either of these, you need to create a new class that derives from the Stream class and overrides the following properties:

Name

Get/Set

Type

CanRead

Get

Bool

CanWrite

Get

Bool

CanSeek

Get

Bool

Length

Get

Long

Position

Get/Set

Long

It also needs to override the Close , Flush , Seek , SetLength , Read , and Write methods. The other methods of the Stream object such as ReadByte and WriteByte use these overridden members. You can override these methods to provide custom implementation (which could have performance benefits).

Here is a simple custom stream implementation (written in C#) that you can compose from other stream objects. It accepts a Stream object as a constructor parameter, and implements all of the stream members (except the Read and Write methods) by directly delegating to that object:

  using System;   using System.IO;   namespace CustomStreams   {   public class UpperCaseStream : Stream   {   Stream _stream;   public UpperCaseStream(Stream stream)   {   _stream = stream;   }     public override bool CanRead   {   get { return _stream.CanRead; }   }     public override bool CanSeek   {   get { return _stream.CanSeek; }   }     public override bool CanWrite   {   get { return _stream.CanWrite; }   }     public override long Length   {   get { return _stream.Length; }   }     public override long Position   {   get { return _stream.Position; }   set { _stream.Position = value; }   }     public override void Close()   {   _stream.Close();   }     public override void Flush()   {   _stream.Flush();   }     public override long Seek(long offset, System.IO.SeekOrigin origin)   {   return _stream.Seek(offset, origin);   }     public override void SetLength(long length)   {   _stream.SetLength(length);   }  

The Read and Write methods scan the data passed in to them and convert any lowercase characters to uppercase. In the case of the Read method, this is done after the Read method of the contained stream class is called. For the Write method, it is done before the Write method of the contained stream is called:

  public override int Read(byte[] buffer, int offset, int count)   {   int bytesRead;   int index;   // let base class do the read   bytesRead = _stream.Read(buffer, offset, count);   // if something was read   if (bytesRead > 0)   {   for(index = offset; index < (offset+bytesRead); index++)   {   if (buffer[index] >= 'a' && buffer[index] <= 'z')   {   buffer[index] = (byte) (buffer[index]  32);   }   }   }   return bytesRead;   }     public override void Write(byte[] buffer, int offset, int count)   {   int index;   // if something was to be written   if (count > 0)   {   for(index = offset; index < (offset+count); index++)   {   if (buffer[index] >= 'a' && buffer[index] <= 'z')   {   buffer[index] = (byte) (buffer[index]  32);   }   }   }   // write the content   _stream.Write(buffer, offset, count);   }   }   }  

The following code shows how you could create this custom stream and then use it to interact with a FileStream in order to automatically read and convert the characters contained within a file to uppercase:

  public static void Main()   {   UpperCaseStream customStream;     // Create our custom stream, passing it a file stream   customStream = new UpperCaseStream(new FileStream("file.txt",   FileMode.Open));   StreamReader sr = new StreamReader(customStream);   Console.WriteLine("{0}",sr.ReadToEnd());   customStream.Close();   }  

The following code shows how to use this custom stream, in conjunction with a FileStream , to automatically convert written data to uppercase:

  public static void Main()   {   UpperCaseStream customStream;   customStream = new UpperCaseStream(new FileStream("fileout.txt",   FileMode.Create));   StreamWriter sw = new StreamWriter(customStream,   System.Text.Encoding.ASCII);   sw.WriteLine("Hello World!");   sw.Close();   }  

The fileout.txt file will now contain the text HELLO WORLD!

This is a fairly simple custom stream implementation, but you could use the same technique to write a more sophisticated class, perhaps to dynamically compress, or secure data. Although not covered in this book, the System.Security namespace contains a CryptoStream class to encrypt data, and third- party vendors are already working on compression streams.

Web Request Classes and Streams

Once it's understood that streams provide a generic mechanism by which data can be read and written to a backing store, and that reader and writer objects provide higher-level functions to a stream such as the ability to read and write text, it's easy to work with the numerous backing stores in .NET.

To demonstrate how classes in other namespaces in the .NET Framework build upon this stream paradigm, let's take a brief look at the HttpWebRequest and HttpWebResponse classes in the System.Net namespace. These classes make it easy to download a file over HTTP. However, we're not going to examine the System.Net namespace in depth, since it's outside the scope of this book.

To make an HTTP request, you need to create an instance of the HttpWebRequest class using the static WebRequest.Create method. This is a factory method that accepts the URI of an Internet resource and then, based upon that protocol, creates an instance of a protocol-specific request object. All protocol- specific request objects derive from the abstract WebRequest class.

If a URI uses HTTP, the actual concrete class created by the WebRequest.CreateRequest method will be of type HttpWebRequest . So, when you write code to create a URI starting with http:// , you can safely cast the object returned from WebRequest.CreateRequest back to HttpWebRequest .

Once you have a request object, use the GetResponse method to retrieve the resource. As with request objects, each protocol has its own response object that derives from a common abstract class, WebResponse . This is the type returned by the GetResponse method of the request object. For the HTTP protocol, the response object can be cast back to the concrete HttpWebResponse class.

The HttpWebResponse class has a GetResponseStream method, which returns a Stream object. This Stream object can be used to read the response data in exactly the same way that you would read data from a file, or any other stream. The following VB.NET code shows how to download the Amazon.com home page using the System.Net classes:

  <%@ Import Namespace="System.IO" %>   <%@ Import Namespace="System.Net" %>   <h3>HTML for http://www.amazon.com</h3>   <%   Dim myRequest As HttpWebRequest   Dim myResponse As HttpWebResponse   Dim sr As StreamReader   Dim line As String   myRequest = CType(WebRequest.Create("http://www.amazon.com"), _   HttpWebRequest)     myResponse = CType(myRequest.GetResponse(), HttpWebResponse)     sr = New StreamReader(myResponse.GetResponseStream())   line = sr.ReadLine()   Do While Not line Is Nothing   line = HttpUtility.HtmlEncode(line)   If line.Length <> 0 Then   Response.Write(line & "<br />")   End If   line = sr.ReadLine()   Loop   sr.Close   %>  

Figure 16-7 shows the output generated by this page:

click to expand
Figure 16-7:

Let's take a look a closer look at what this code does. It initially constructs a web request using WebRequest.Create , and casts the returned object back to an HttpWebRequest :

 myRequest = CType(WebRequest.Create("http://www.amazon.com"),HttpWebRequest) 

Next, the request is executed and the response object is retrieved. Once again, the Response object is safely cast back to HttpWebResponse since you know the protocol being used:

 myResponse = CType(myRequest.GetResponse(), HttpWebResponse) 

Once you have the web response object, the GetResponseStream method is called to get a Stream object that can be used to read the contents of the web page:

 sr = new StreamReader(myResponse.GetResponseStream()) 

To output the underlying HTML in a useful form, create a StreamReader object that can be used to read the web page line-by-line . HttpUtility.HtmlEncode method is used for escaping characters that would otherwise be interpreted by the browser (if you want the user to see the underlying HTML):

 line = sr.ReadLine() Do While Not line Is Nothing    line = HttpUtility.HtmlEncode(line)    If line.Length <> 0 Then       Response.Write(line & "<br />")    End If    line = sr.ReadLine() Loop 

Finally, the stream is closed using the Close method of the StreamReader .

The HttpWebRequest and HttpWebResponse classes make it really simple to work with resources located anywhere over the Internet or a local network. They don't use the WinInet APIs under the hood, so they can be safely used in ASP.NET without any worries about affecting the scalability of applications.

To round off our coverage of the HttpWebRequest and HttpWebResponse classes, and to introduce our next topic, regular expressions , let's create a simple application that can determine the ranking of a book on Amazon.com. The technique shown is often called screen scraping and should give an idea of how you can apply these classes in real-world applications.

Our application accepts the URL of a book on Amazon.com and displays the book rank along with details about the response such as the content length, encoding, and HTTP response code (see Figure 16-8):

click to expand
Figure 16-8:

The application works by downloading the specified page and placing the retrieved HTML into a string. To get the page content into a string we create a StreamReader object and call its ReadToEnd method, which returns a string that contains the complete content of the stream:

  HttpWebRequest myRequest;   HttpWebResponse myResponse;   Stream s;   myRequest = (HttpWebRequest) WebRequest.Create(URLToRead.Value);   myResponse = (HttpWebResponse) myRequest.GetResponse();   s = myResponse.GetResponseStream();   _HtmlContent = new StreamReader(s).ReadToEnd();   s.Close();  

Once the page content is retrieved, the string that contains the HTML is processed and regular expressions are used to extract the ranking:

  void RenderStreamIntoPage()   {   Regex re;   Match m;     re =   new Regex("(?<x>Amazon.com Sales Rank: </b>)(?<rank>.*)</font><br>");   m = re.Match(_HtmlContent);   // Check for multiple matches   while(m.Success == true)   {   foreach(Capture c in m.Captures)   {   Response.Write("<br />Ranking : " + m.Result("${rank}"));   }   m = m.NextMatch();   }   }  



Professional ASP. NET 1.1
Professional ASP.NET MVC 1.0 (Wrox Programmer to Programmer)
ISBN: 0470384611
EAN: 2147483647
Year: 2006
Pages: 243

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