Professional ASP.NET 1.1 [Electronic resources]

Alex Homeret

نسخه متنی -صفحه : 244/ 126
نمايش فراداده

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):

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:

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:

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" %>

<l>

<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>

<l>

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:

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:

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" %>

<l>

<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>

<l>

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:

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:

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):

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

}

}