Chapter 17. Files and Streams

There's a good reason that this book covered ADO.NET before dealing with simpler data access techniques, such as writing and reading ordinary files. Traditional file access is generally much less useful in a web application than it is in a desktop program. Databases, on the other hand, are designed from the ground up to support a large number of simultaneous users with speed, safety, and efficiency. Most web applications will rely on a database for some features, but many won't have any reason to use direct file access.

Of course, enterprising ASP.NET developers can find a use for almost any technology. If this book didn't cover file access, no doubt many developers would be frustrated when designing web applications with legitimate (and innovative) uses for ordinary files. In fact, file access is so easy and straightforward in .NET that it may be perfect for simple, small-scale solutions that don't need a full-fledged database product like SQL Server.

This chapter explains how you can use the classes in .NET to read and change file system information and even build a simple file browser. You'll also learn how to create simple text and binary files of your own. Finally, you'll consider how you can allow users to upload their own files to your web server.

Files and Web Applications

Why is it that most web applications don't use files? There are several limitations to files:

File-naming limitations:

When you create a new file, it obviously can't have the same name as an existing file in the same directory. That means you'll probably need to fall back on some system for randomly generating files names. For example, you might create a file name based on a random number combined with the current date and time, or create a file name that incorporates a globally unique identifier (GUID). With both of these approaches, file names would be statistically unique, which means duplicates would be extremely unlikely. However, the file names wouldn't be very meaningful. In databases, this problem is solved more neatly with the auto-increment data type, which automatically fills a specific field with a unique number when you create a record.

Multiuser limitations:

Relational databases provide features such as locking and transactions to prevent inconsistencies and make sure multiple people can use the same data at the same time. Comparatively, the web server's file system is woefully backward. Although you can allow multiple users to read a file at once, it's almost impossible to let multiple users update the same file at the same time without catastrophe.

Scalability problems:

File operations suffer from some overhead. In a simple scenario, file access may be faster than connecting to a database and performing a query. But the cumulative effect in a large web application is very different. When multiple users are working with files at the same time, your web server may slow down dramatically.

Security risks:

If you allow the user to specify a file or path name, the user could devise a way to trick your application into accessing or overwriting a protected system file. Even without this ability, a malicious or careless user might use an ASP.NET page that creates or uploads files to fill up your web server hard drive and cause it to stop working. All of these problems are preventable, but they require a bit more work than a database-backed solution.

Of course, file access does have its uses. Maybe you need to access information that another application has stored in a file. Or maybe you need to store your information in a file so that other applications can access it. For example, you might be creating an intranet application that allows a small set of trusted employees to upload and manage documents. You could store their documents in a binary field in a database table, but that would make it more difficult to browse and open those files without using your web front end.

In these situations, you'll be happy to know that ASP.NET can use all the file access features of the .NET Framework. That means your web applications can freely explore the file system, manage files, and create new files with custom content.

File System Information

The simplest level of file access just involves retrieving information about existing files and directories and performing typical file system operations such as copying files and creating directories.

.NET provides five basic classes for retrieving this sort of information. They are all located in the System.IO namespace (and, incidentally, can be used in desktop applications in exactly the same way they are used in web applications). They include the following:

  • The Directory and File classes, which provide shared methods that allow you to retrieve information about any files and directories visible from your server

  • The DirectoryInfo and FileInfo classes, which use similar instance methods and properties to retrieve the same sort of information

  • The DriveInfo class, which provides shared methods that allow you to retrieve information about a drive and the amount of free space it provides

In Chapter 3, you saw how a class can provide two types of members. Shared members are always available—you just use the name of the class. But instance members are available only if you have a live object.

With the file access classes, shared methods are more convenient to use because they don't require you to create an instance of the class. That means you can use a quick one-line code statement to perform a simple task such as checking whether a file exists. On the other hand, if you need to retrieve several pieces of information from the same file or directory, it's easier to use the instance members. That way, you don't need to keep specifying the name of the directory or file each time you call a method. The instance approach is also a bit faster in this situation. That's because the FileInfo and DirectoryInfo classes perform their security checks once—when you create the object instance. The Directory and File classes perform a security check every time you invoke a method, which adds more overhead.

You'll learn about all of these classes in this chapter. But first, it's worth taking a detour to look at another class that can simplify code that deals with the file system: the Path class.

The Path Class

Along with the five classes outlined in the previous section, .NET also includes a helper class named Path in the same System.IO namespace. The Path class doesn't include any real file management functionality. It simply provides a few shared methods that are useful when manipulating strings that contain file and directory paths.

For example, the Path class includes a GetFileName() method that pulls the file name out of a full string. Here's an example:

Dim file As String = Path.GetFileName( _
  "c:DocumentsUploadUsersJamesX
esume.doc")
' file now contains "resume.doc"

The Path class also includes a Combine() method that can tack a relative path on the end of an absolute path. Here it is at work, fusing two strings together:

Dim absolutePath As String = "c:UsersMyDocuments"
Dim subPath As String = "Sarahworksheet.xls"
Dim combined As String = Path.Combine(absolutePath, subPath)
' combined now contains "c:UsersMyDocumentsSarahworksheet.xls"

You could perform all of these tasks on your own, but the Path class is a great way to avoid errors. Table 17-1 lists the methods of the Path class.

Table 17.1. Path Methods

Methods

Description

Combine()

Combines a path with a file name or a subdirectory.

ChangeExtension()

Returns a copy of the string with a modified extension. If you don't specify an extension, the current extension is removed.

GetDirectoryName()

Returns all the directory information, which is the text between the first and last directory separators ().

GetFileName()

Returns just the file name portion of a path, which is the portion after the last directory separator.

GetFileNameWithoutExtension()

Returns just the file name portion of a path, but omits the file extension at the end.

GetFullPath()

Changes a relative path into an absolute path using the current directory. For example, if c:Temp is the current directory, calling GetFullPath() on a file name such as test.txt returns c:Temp est.txt. This method has no effect on an absolute path.

GetPathRoot()

Retrieves a string with the root drive (for example, "c:"), provided that information is in the string. For a relative path, it returns a null reference.

HasExtension()

Returns True if the path ends with an extension.

IsPathRooted()

Returns True if the path is an absolute path and False if it's a relative path.

The Directory and File Classes

The Directory and File classes provide a number of useful shared methods. Table 17-2 and Table 17-3 show an overview of the most important methods. Most of these methods take the same parameter: a fully qualified path name identifying the directory or file you want the operation to act on. A few methods, such as Delete() and Move(), take additional parameters.

Table 17.2. Directory Class Members

Method

Description

CreateDirectory()

Creates a new directory. If you specify a directory inside another nonexistent directory, ASP.NET will thoughtfully create all the required directories.

Delete()

Deletes the corresponding empty directory. To delete a directory along with its contents (subdirectories and files), add the optional second parameter of True.

Exists()

Returns True or False to indicate whether the specified directory exists.

GetCreationTime(), GetLastAccessTime(), and GetLastWriteTime()

Returns a DateTime object that represents the time the directory was created, accessed, or written to. Each GetXxx() method has a corresponding SetXxx() method, which isn't shown in this table.

GetDirectories() and GetFiles()

Returns an array of strings, one for each subdirectory or file (depending on the method you're using) in the specified directory. These methods can accept a second parameter that specifies a search expression (such as ASP*.*).

GetLogicalDrives()

Returns an array of strings, one for each drive that's present on the current computer. Drive letters are in this format: "c:".

GetParent()

Parses the supplied directory string and tells you what the parent directory is. You could do this on your own by searching for the character (or, more generically, the Path.DirectorySeparatorChar), but this function makes life a little easier.

GetCurrentDirectory() and SetCurrentDirectory()

Allows you to set or retrieve the current directory, which is useful if you need to use relative paths instead of full paths. Generally, these functions aren't necessary.

Move()

Accepts two parameters: the source path and the destination path. The directory and all its contents can be moved to any path, as long as it's located on the same drive. (If you need to move files from one drive to another, you'll need to pair up a copy operation and a delete operation instead.)

Table 17.3. File Class Members

Method

Description

Copy()

Accepts two parameters: the fully qualified source file name and the fully qualified destination file name. To allow overwriting, use the version that takes a Boolean third parameter and set it to True.

Delete()

Deletes the specified file but doesn't throw an exception if the file can't be found.

Exists()

Indicates True or False in regard to whether a specified file exists.

GetAttributes() and SetAttributes()

Retrieves or sets an enumerated value that can include any combination of the values from the FileAttributes enumeration.

GetCreationTime(), GetLastAccessTime(), and GetLastWriteTime()

Returns a DateTime object that represents the time the file was created, accessed, or last written to. Each GetXxx() method has a corresponding SetXxx() method, which isn't shown in this table.

Move()

Accepts two parameters: the fully qualified source file name and the fully qualified destination file name. You can move a file across drives and even rename it while you move it (or rename it without moving it).

The File class also includes some methods that allow you to create and open files as streams. You'll explore these features in the "Reading and Writing with Streams" section of this chapter. The only feature the File class lacks (and the FileInfo class provides) is the ability to retrieve the size of a specified file.

The File and Directory methods are quite intuitive. For example, consider the code for a simple page that displays some information about the files in a specific directory. You might use this code to create a simple admin page that allows you to review the contents of an FTP directory (see Figure 17-1). Clients could use this page to review their documents and remove suspicious files.

An admin page with file information

Figure 17.1. An admin page with file information

You should begin by importing the namespace that has the IO classes:

Imports System.IO

The code for this page is as follows:

Public Partial Class ViewFiles
    Inherits System.Web.UI.Page

    Private ftpDirectory As String

    Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As EventArgs) Handles Me.Load
        ftpDirectory = Path.Combine(Request.PhysicalApplicationPath, "FTP")
        If Not Me.IsPostBack Then
            CreateFileList()
        End If
    End Sub

    Private Sub CreateFileList()
' Retrieve the list of files, and display it in the page.
        ' This code also disables the delete button, ensuring the
        ' user must view the file information before deleting it.
        Dim fileList() As String = Directory.GetFiles(ftpDirectory)
        lstFiles.DataSource = fileList
        lstFiles.DataBind()
        lblFileInfo.Text = ""
        cmdDelete.Enabled = False
    End Sub

    Protected Sub cmdRefresh_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdRefresh.Click
        CreateFileList()
    End Sub

    Protected Sub lstFiles_SelectedIndexChanged(ByVal sender As Object, _
      ByVal e As EventArgs) Handles lstFiles.SelectedIndexChanged

        ' Display the selected file information.
        ' Use the StringBuilder for the fastest way to build the string.
        Dim fileName As String = lstFiles.SelectedItem.Text
        Dim displayText As New System.Text.StringBuilder
        displayText.Append("<b>")
        displayText.Append(fileName)
        displayText.Append("</b><br /><br />")
        displayText.Append("Created: ")
        displayText.Append(File.GetCreationTime(fileName).ToString())
        displayText.Append("<br />Last Accessed: ")
        displayText.Append(File.GetLastAccessTime(fileName).ToString())
        displayText.Append("<br />")

        ' Show attribute information. GetAttributes can return a combination
        ' of enumerated values, so you need to evaluate it with the
        ' And keyword.
        Dim Attr As FileAttributes = File.GetAttributes(fileName)
        If (Attr And FileAttributes.Hidden) = FileAttributes.Hidden Then
            displayText.Append("This is a hidden file.<br />")
        End If
        If (Attr And FileAttributes.ReadOnly) = FileAttributes.ReadOnly Then
            displayText.Append("This is a read-only file.<br />")

            cmdDelete.Enabled = False
        Else
            cmdDelete.Enabled = True
        End If

        ' Display the information.
        lblFileInfo.Text = displayText.ToString()
    End Sub

    Protected Sub cmdDelete_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdDelete.Click
        File.Delete(lstFiles.SelectedItem.Text)
        CreateFileList()
End Sub

End Class

Dissecting the Code . . .

  • Every time the page loads, it sets the ftpDirectory string. The path is set to the FTP subfolder in the current web application directory (which is provided by the Request.PhysicalApplicationPath property). These two details (the current web application directory and the FTP subfolder) are fused together into one path string using the Combine() method of the Path class.

  • The CreateFileList() procedure is easy to code, because it uses the data binding feature of the ListBox. The array returned from the GetFiles() method can be placed in the list with just a couple of lines of code.

  • The AutoPostBack property of the ListBox is set to True. That way, when the user chooses an item in the list, the ListBox posts the page back immediately so the code can read the file information and refresh the file details on the page.

  • When evaluating the FileAttributes enumeration, you need to use the And operator to perform bitwise arithmetic. This is because the value returned from GetAttributes() can actually contain a combination of more than one attribute. Using bitwise arithmetic, you can pull out just the attribute that you're interested in, and then determine whether it's set.

  • The code that gets the file information builds a long string of text, which is then displayed in a label. For optimum performance, this code uses the System.Text.StringBuilder class. Without the StringBuilder, you'd need to use string concatenation to join the string together. This is much slower, because every time the code adds a piece of text to the string, .NET creates an entirely new string object behind the scenes.

  • The code that displays file information could benefit by switching to the FileInfo class (as shown in the next section). As it is, every method needs to specify the same file name. This is a bit tedious, and it's a bit slower because each method requires a separate security check.

One ingredient this code lacks is error handling. When using any external resource, including files, it's essential that you defend yourself with a Try/Catch block. This way you can deal with unpredictable occurrences that are beyond your control—for example, if the file isn't accessible because it's already open in another program, or the account running the code doesn't have the required permissions. The code in this example is easy to correct—simply wrap all the file operations into a Try/Catch block. (You'll need three—one for the code that reads the files in the current directory, one for the code that retrieves the information from the selected file, and one for the code that deletes the file.) To see the code with the added error handling logic, refer to the downloadable samples for this chapter.

The DirectoryInfo and FileInfo Classes

The DirectoryInfo and FileInfo classes mirror the functionality in the Directory and File classes. In addition, they make it easy to walk through directory and file relationships. For example, you can easily retrieve the FileInfo objects for the files in a directory represented by a DirectoryInfo object.

Note that while the Directory and File classes expose only methods, DirectoryInfo and FileInfo provide a combination of properties and methods. For example, while the File class had separate GetAttributes() and SetAttributes() methods, the FileInfo class includes an Attributes property.

Another nice thing about the DirectoryInfo and FileInfo classes is that they share a common set of properties and methods because they derive from the common FileSystemInfo base class. Table 17-4 describes the members they have in common.

Table 17.4. DirectoryInfo and FileInfo Members

Member

Description

Attributes

Allows you to retrieve or set attributes using a combination of values from the FileAttributes enumeration.

CreationTime, LastAccessTime, and LastWriteTime

Allows you to set or retrieve the creation time, last-access time, and last-write time using a DateTime object.

Exists

Returns True or False depending on whether the file or directory exists. In other words, you can create FileInfo and DirectoryInfo objects that don't actually correspond to current physical directories, although you obviously won't be able to use properties such as CreationTime and methods such as MoveTo().

FullName, Name, and Extension

Returns a string that represents the fully qualified name, the directory or file name (with extension), or the extension on its own, depending on which property you use.

Delete()

Removes the file or directory, if it exists. When deleting a directory, it must be empty, or you must specify an optional parameter set to True.

Refresh()

Updates the object so it's synchronized with any file system changes that have happened in the meantime (for example, if an attribute was changed manually using Windows Explorer).

Create()

Creates the specified directory or file.

MoveTo()

Copies the directory and its contents or the file. For a DirectoryInfo object, you need to specify the new path; for a FileInfo object, you specify a path and file name.

In addition, the FileInfo and DirectoryInfo classes have a few unique members, as indicated in Table 17-5 and Table 17-6.

Table 17.5. Unique DirectoryInfo Members

Member

Description

Parent and Root

Returns a DirectoryInfo object that represents the parent or root directory. For a directory like c: empmyfiles, the parent is c: emp, and the root is c:.

CreateSubdirectory()

Creates a directory with the specified name in the directory represented by the DirectoryInfo object. It also returns a new DirectoryInfo object that represents the subdirectory.

GetDirectories()

Returns an array of DirectoryInfo objects that represent all the subdirectories contained in this directory.

GetFiles()

Returns an array of FileInfo objects that represent all the files contained in this directory.

Table 17.6. Unique FileInfo Members

Member

Description

Directory

Returns a DirectoryInfo object that represents the parent directory.

DirectoryName

Returns a string that identifies the name of the parent directory.

Length

Returns a Long (64-bit integer) with the file size in bytes.

CopyTo()

Copies a file to the new path and file name specified as a parameter. It also returns a new FileInfo object that represents the new (copied) file. You can supply an optional additional parameter of True to allow overwriting.

When you create a DirectoryInfo or FileInfo object, you specify the full path in the constructor:

Dim myDirectory As New DirectoryInfo("c:Temp")
Dim myFile As New FileInfo("c:Temp
eadme.txt")

This path may or may not correspond to a real physical file or directory. If it doesn't, you can always use the Create() method to create the corresponding file or directory:

' Define the new directory and file.
Dim myDirectory As New DirectoryInfo("c:TempTest")
Dim myFile As New FileInfo("c:TempTest
eadme.txt")

' Now create them. Order here is important.
' You can't create a file in a directory that doesn't exist yet.
myDirectory.Create()
myFile.Create()

The DriveInfo Class

The DriveInfo class allows you to retrieve information about a drive on your computer. Just a few pieces of information will interest you. Typically, the DriveInfo class is merely used to retrieve the total amount of used and free space.

Table 17-7 shows the DriveInfo members. Unlike the FileInfo and DriveInfo classes, there's no Drive class with instance versions of these methods.

Table 17.7. DriveInfo Members

Member

Description

TotalSize

Gets the total size of the drive, in bytes. This includes allocated and free space.

TotalFreeSpace

Gets the total amount of free space, in bytes.

AvailableFreeSpace

Gets the total amount of available free space, in bytes. Available space may be less than the total free space if you've applied disk quotas limiting the space the ASP.NET process can use.

DriveFormat

Returns the name of the file system used on the drive (such as NTFS or FAT32) as a string.

DriveType

Returns a value from the DriveType enumeration, which indicates whether the drive is a Fixed, Network, CDRom, Ram, or Removable drive (or Unknown if the drive's type cannot be determined).

IsReady

Returns whether the drive is ready for reading or writing operations. Removable drives are considered "not ready" if they don't have any media. For example, if there's no CD in a CD drive, IsReady will return False. In this situation, it's not safe to query the other DriveInfo properties. Fixed drives are always readable.

Name

Returns the drive letter name of the drive (such as C: or E:).

VolumeLabel

Gets or sets the descriptive volume label for the drive. In an NTFS-formatted drive, the volume label can be up to 32 characters. If not set, this property returns a null reference (Nothing).

RootDirectory

Returns a DirectoryInfo object for the root directory in this drive.

GetDrives()

Retrieves an array of DriveInfo objects, representing all the logical drives on the current computer.

Tip

Attempting to read from a drive that's not ready (for example, a CD drive that doesn't have a CD in it) will throw an exception. To avoid this problem, check the DriveInfo.IsReady property, and attempt to read other properties only if it returns True.

A Sample File Browser

You can use methods such as DirectoryInfo.GetFiles() and DirectoryInfo.GetDirectories() to create a simple file browser. The following example shows you how. Be warned that, although this code is a good example of how to use the DirectoryInfo and FileInfo classes, it isn't a good example of security. Generally, you wouldn't want a user to be able to find out so much information about the files on your web server.

The sample file browser program allows the user to see information about any file in any directory in the current drive, as shown in Figure 17-2.

A web server file browser

Figure 17.2. A web server file browser

The code for the file browser page is as follows:

Public Partial Class FileBrowser
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As EventArgs) Handles Me.Load
        If Not Me.IsPostBack Then
            Dim startingDir As String = "c:"
            lblCurrentDir.Text = startingDir
            ShowFilesIn(startingDir)
            ShowDirectoriesIn(startingDir)
        End If
    End Sub

    Private Sub ShowFilesIn(ByVal dir As String)
        lblFileInfo.Text = ""
lstFiles.Items.Clear()

        Try
            Dim dirInfo As New DirectoryInfo(dir)
            For Each fileItem As FileInfo In dirInfo.GetFiles()
                lstFiles.Items.Add(fileItem.Name)
            Next
        Catch err As Exception
            ' Ignore the error and leave the list box empty.
        End Try
    End Sub

    Private Sub ShowDirectoriesIn(ByVal dir As String)
        lstDirs.Items.Clear()

        Try
            Dim dirInfo As New DirectoryInfo(dir)
            For Each dirItem As DirectoryInfo In dirInfo.GetDirectories()
                lstDirs.Items.Add(dirItem.Name)
            Next
        Catch err As Exception
            ' Ignore the error and leave the list box empty.
        End Try
    End Sub

    Protected Sub cmdBrowse_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdBrowse.Click
        ' Browse to the currently selected subdirectory.
        If lstDirs.SelectedIndex <> −1 Then
            Dim newDir As String = Path.Combine(lblCurrentDir.Text, _
              lstDirs.SelectedItem.Text)
            lblCurrentDir.Text = newDir
            ShowFilesIn(newDir)
            ShowDirectoriesIn(newDir)
        End If
    End Sub

    Protected Sub cmdParent_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdParent.Click
        ' Browse up to the current directory's parent.
        ' The Directory.GetParent method helps us out.
        Dim newDir As String
        If Directory.GetParent(lblCurrentDir.Text) Is Nothing Then
            ' This is the root directory; there are no more levels.
            Exit Sub
        Else
            newDir = Directory.GetParent(lblCurrentDir.Text).FullName
        End If

        lblCurrentDir.Text = newDir
        ShowFilesIn(newDir)
        ShowDirectoriesIn(newDir)
    End Sub
Protected Sub cmdShowInfo_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdShowInfo.Click
        ' Show information for the currently selected file.
        If lstFiles.SelectedIndex <> −1 Then
            Dim fileName As String = Path.Combine(lblCurrentDir.Text, _
              lstFiles.SelectedItem.Text)

            Dim displayText As New StringBuilder()
            Try
                Dim selectedFile As New FileInfo(fileName)
                displayText.Append("<b>")
                displayText.Append(selectedFile.Name)
                displayText.Append("</b><br />Size: ")
                displayText.Append(selectedFile.Length)
                displayText.Append("<br />")
                displayText.Append("Created: ")
                displayText.Append(selectedFile.CreationTime.ToString())
                displayText.Append("<br />Last Accessed: ")
                displayText.Append(selectedFile.LastAccessTime.ToString())
            Catch err As Exception
                displayText.Append(err.Message)
            End Try

            lblFileInfo.Text = displayText.ToString()
        End If
    End Sub

End Class

Dissecting the Code . . .

  • The list controls in this example don't post back immediately. Instead, the web page relies on the Browse to Selected, Up One Level, and Show Info buttons.

  • By default, directory names don't end with a trailing backslash () character (for example, c:Temp is used instead of c:Temp). However, when referring to the root drive, a slash is required. This is because of an interesting inconsistency that dates back to the days of DOS. When using directory names, c: refers to the root drive, but c: refers to the current directory, whatever it may be. This quirk can cause problems when you're manipulating strings that contain file names, because you don't want to add an extra trailing slash to a path (as in the invalid path c:\myfile.txt). To solve this problem, the page uses the Combine() method of the Path class. This method correctly joins any file and path name together, adding the when required.

  • The code includes all the necessary error handling code. If you attempt to read the information for a file that you aren't permitted to examine, the error message is displayed instead of the file details section. If an error occurs when calling DirectoryInfo.GetFiles() or DirectoryInfo.GetDirectories(), the error is simply ignored and the files or subdirectories aren't shown. This error occurs if the account that's running your code doesn't have permission to read the contents of the directory. For example, this occurs if you try to access the c:System Volume Information directory in Windows and you're not an administrator.

  • The ShowFilesIn() and ShowDirectoriesIn() methods loop through the file and directory collections to build the lists. Another approach is to use data binding instead, as shown in the following code sample:

    ' Another way to fill lstFiles.
    Dim dirInfo As New DirectoryInfo(dir)
    
    lstFiles.DataSource = dirInfo.GetFiles()
    lstFiles.DataMember = "Name"
    lstFiles.DataBind()

    Just remember that when you bind a collection of objects, you need to specify which property will be used for the list. In this case, it's the DirectoryInfo.Name or FileInfo.Name property.

Reading and Writing with Streams

The .NET Framework makes it easy to create simple "flat" files in text or binary format. Unlike a database, these files don't have any internal structure (that's why they're called flat). Instead, these files are really just a list of whatever information you want to store.

Text Files

You can write to a file and read from a file using a StreamWriter and a StreamReader—dedicated classes that abstract away the process of file interaction. There really isn't much to it. You can create the StreamWriter and StreamReader classes on your own, or you can use one of the helpful shared methods included in the File class, such as CreateText() or OpenText().

Here's an example that gets a StreamWriter for writing data to the file c:myfile.txt:

' Define a StreamWriter (which is designed for writing text files).
Dim w As StreamWriter

' Create the file, and get a StreamWriter for it.
w = File.CreateText("c:myfile.txt")

When you call the CreateText() method, you create the file and receive the StreamWriter object. At this point, the file is open and ready to receive your content. You need to write your data to the file and then close it as soon as possible.

Using the StreamWriter, you can call the WriteLine() method to add information to the file. The WriteLine() method is overloaded so it can write many simple data types, including strings, integers, and other numbers. These values are essentially all converted into strings when they're written to a file and must be converted back into the appropriate types manually when you read the file.

w.WriteLine("This file generated by ASP.NET")   ' Write a string.
w.WriteLine(42)                                 ' Write a number.

When you finish with the file, you must make sure to close it by calling the Close() or Dispose() method. Otherwise, the changes may not be properly written to disk, and the file could be locked open.

w.Close()

Finally, when you're debugging an application that writes to files it's always a good idea to look at what you wrote using a text editor like Notepad. Figure 17-3 shows the contents that are created in c:myfile.txt with the simple code you've considered.

A sample text file

Figure 17.3. A sample text file

To read the information, you use the corresponding StreamReader class. It provides a ReadLine() method that gets the next available value and returns it as a string. ReadLine() starts at the first line and advances the position to the end of the file, one line at a time.

Dim r As StreamReader = File.OpenText("c:myfile.txt")
Dim inputString As String
inputString = r.ReadLine()     ' = "This file generated by ASP.NET"
inputString = r.ReadLine()     ' = "42"

ReadLine() returns a null reference when there is no more data in the file. This means you can read all the data in a file using code like this:

' Read and display the lines from the file until the end
' of the file is reached.
Dim line As String
Do
    line = r.ReadLine()
    If line IsNot Nothing Then
        ' (Process the line here.)
    End If
Loop Until line Is Nothing

As when writing to a file, you must close the file once you're finished:

r.Close()

The code you've seen so far opens a file in single-user mode. If a second user tries to access the same file at the same time, an exception will occur. You can reduce this problem when opening files using the more generic four-parameter version of the File.Open() method instead of File.OpenText(). You must specify FileShare.Read for the final parameter. Unlike the OpenText() method, the Open() method returns a FileStream object, and you must manually create a StreamReader that wraps it.

Here's the code you need to create a multiuser-friendly StreamReader:

Dim fs As FileStream
fs = File.Open("c:myfile.txt", FileMode.Open, _
  FileAccess.Read, FileShare.Read)
Dim r As New StreamReader(fs)

Tip

In Chapter 8, you saw how you can create a cookie for the current user, which can be persisted to disk as a simple text file. This is a common technique for storing information in a web application, but it's quite a bit different from the file access code you've seen in this chapter. Cookies are created on the client side rather than on the server. This means your ASP.NET code may be able to use them on subsequent requests from the same user, but they aren't suitable when storing information you need to review later, information that's more permanent, or information that affects more than one user.

Binary Files

You can also read and write to binary files. Binary data uses space more efficiently but also creates files that aren't human-readable. If you open a file in Notepad, you'll see a lot of extended ASCII characters (politely known as gibberish).

To open a file for binary writing, you need to create a new BinaryWriter object. The constructor accepts a stream, which you can retrieve using the File.OpenWrite() method. Here's the code to open the file c:inaryfile.bin for binary writing:

Dim fs As FileStream = File.OpenWrite("c:inaryfile.bin")
Dim w As New BinaryWriter(fs)

.NET concentrates on stream objects, rather than the source or destination for the data. This means you can write binary data to any type of stream, whether it represents a file or some other type of storage location, using the same code. In addition, writing to a binary file is almost the same as writing to a text file.

Dim str As String = "ASP.NET Binary File Test"
Dim int As Integer = 42
w.Write(str)
w.Write(int)
w.Close()

Reading data from a binary file is easy, but not quite as easy as reading data from a text file. The problem is that you need to know the data type of the data you want to retrieve. To retrieve a string, you use the ReadString() method. To retrieve an integer, you must use ReadInt32(). That's why the preceding code example writes variables instead of literal values. If the value 42 were hard-coded as the parameter for the Write() method, it wouldn't be clear if the value would be written as a 16-bit integer, 32-bit integer, decimal, or something else. Unfortunately, you may need to micromanage binary files in this way to prevent errors.

Dim r As New BinaryReader(File.OpenRead("c:inaryfile.bin"))
Dim str As string
Dim int As Integer
str = r.ReadString()
int = r.ReadInt32()

r.Close()

Once again, if you want to use file sharing, you need to use File.Open() instead of File.OpenRead(). You can then create a BinaryReader by hand, as shown here:

Dim fs As FileStream
fs = File.Open("c:inaryfile.bin", FileMode.Open, _
  FileAccess.Read, FileShare.Read)
Dim r As New BinaryReader(fs)

Note

You have no easy way to jump to a location in a text or binary file without reading through all the information in order. Although you can use methods such as Seek() on the underlying stream, you need to specify an offset in bytes, which involves some fairly involved calculations based on data type sizes. If you need to store a large amount of information and move through it quickly, you need a dedicated database, not a binary file.

Shortcuts for Reading and Writing Files

.NET includes functionality for turbo-charging your file writing and reading. This functionality comes from several shared methods in the File class that let you read or write an entire file in a single line of code.

For example, here's a quick code snippet that writes a three-line file and then retrieves it into a single string:

Dim lines() As String = {"This is the first line of the file.", _
  "This is the second line of the file.", _
  "This is the third line of the file."}

' Write the file in one shot.
File.WriteAllLines("c:	estfile.txt", lines)

' Read the file in one shot (into a variable named content).
Dim content As String = File.ReadAllText("c:	estfile.txt")

Table 17-8 describes the full set of quick file access methods. All of these are shared methods.

Table 17.8. File Methods for Quick Input/Output

Method

Description

ReadAllText()

Reads the entire contents of a file and returns it as a single string.

ReadAllLines()

Reads the entire contents of a file and returns it as an array of strings, one for each line.

ReadAllBytes()

Reads the entire file and returns its contents as an array of bytes.

WriteAllText()

Creates a file, writes a supplied string to the file, and closes it. If the file already exists, it is overwritten.

WriteAllLines()

Creates a file, writes a supplied array of strings to the file (separating each line with a hard return), and closes the file. If the file already exists, it is overwritten.

WriteAllBytes()

Creates a file, writes a supplied byte array to the file, and closes it. If the file already exists, it is overwritten.

The quick file access methods are certainly convenient for creating small files. They also ensure a file is kept only for as short a time as possible, which is always the best approach to minimize concurrency problems. But are they really practical? It all depends on the size of the file. If you have a large file (say, one that's several megabytes), reading the entire content into memory at once is a terrible idea. It's much better to read one piece of data at a time and process the information bit by bit. Even if you're dealing with medium-sized files (say, several hundreds of kilobytes), you might want to steer clear of the quick file access methods. That's because in a popular website you might have multiple requests dealing with files at the same time, and the combined overhead of keeping every user's file data in memory might reduce the performance of your application.

A Simple Guest Book

The next example demonstrates the file access techniques described in the previous sections to create a simple guest book. The page actually has two parts. If there are no current guest entries, the client will see only the controls for adding a new entry, as shown in Figure 17-4.

The initial guest book page

Figure 17.4. The initial guest book page

When the user clicks Submit, a file will be created for the new guest book entry. As long as at least one guest book entry exists, a GridView control will appear at the top of the page, as shown in Figure 17-5.

The full guest book page

Figure 17.5. The full guest book page

The GridView that represents the guest book is constructed using data binding, which you explored in Chapters 15 and 16. Technically speaking, the GridView is bound to a collection that contains instances of the BookEntry class. The BookEntry class definition is included in the code-behind file for the web page and looks like this:

Public Class BookEntry
    Private _author As String
    Private _submitted As Date
    Private _message As String

    Public Property Author() As String
        Get
            Return _author
        End Get
        Set(ByVal Value As String)
            _author = Value
        End Set
End Property

    Public Property Submitted() As Date
        Get
            Return _submitted
        End Get
        Set(ByVal Value As Date)
            _submitted = Value
        End Set
    End Property

    Public Property Message() As String
        Get
            Return _message
        End Get
        Set(ByVal Value As String)
            _message = Value
        End Set
    End Property
End Class

The GridView uses a single template column, which fishes out the values it needs to display. Here's what it looks like (without the style details):

<asp:GridView ID="GuestBookList" runat="server" AutoGenerateColumns="False">
  <Columns>
      <asp:TemplateField HeaderText="Guest Book Comments">
        <ItemTemplate>
          Left By:
          <%# Eval("Author") %>
          <br />
          <b><%# Eval("Message") %></b>
          <br />
          Left On:
          <%# Eval("Submitted") %>
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
</asp:GridView>

It also adds some style information that isn't included here (because it isn't necessary to understand the logic of the program). In fact, these styles were applied in Visual Studio using the GridView's Auto Format feature.

As for the entries, the guest book page uses the subfolder (App_DataGuestBook) to store a collection of files. Each file represents a separate entry in the guest book. By putting the GuestBook folder in the App_Data folder, the web application ensures that the user can't access any of the guest book files directly, because the web server won't allow it. However, an even better approach would usually be to create a GuestBook table in a database and make each entry a separate record.

The code for the web page is as follows:

Public Partial Class GuestBook
    Inherits System.Web.UI.Page
Private guestBookName As String

    Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As EventArgs) Handles MyBase.Load
        guestBookName = Server.MapPath("~/App_Data/GuestBook")

        If Not Me.IsPostBack Then
            GuestBookList.DataSource = GetAllEntries()
            GuestBookList.DataBind()
        End If
    End Sub

    Protected Sub cmdSubmit_Click(ByVal sender As Object, _
      ByVal e As EventArgs) Handles cmdSubmit.Click
        ' Create a new BookEntry object.
        Dim newEntry As New BookEntry()
        newEntry.Author = txtName.Text
        newEntry.Submitted = DateTime.Now
        newEntry.Message = txtMessage.Text

        ' Let the SaveEntry procedure create the corresponding file.
        Try
            SaveEntry(newEntry)
        Catch err As Exception
            ' An error occurred. Notify the user and don't clear the
            ' display.
            lblError.Text = err.Message & " File not saved."
            Exit Sub
        End Try

        ' Refresh the display.
        GuestBookList.DataSource = GetAllEntries()
        GuestBookList.DataBind()

        txtName.Text = ""
        txtMessage.Text = ""
    End Sub

    Private Function GetAllEntries() As List(Of BookEntry)
        ' Return a collection that contains BookEntry objects
        ' for each file in the GuestBook directory.
        ' This method relies on the GetEntryFromFile method.
        Dim entries As New List(Of BookEntry)()

        Try
            Dim guestBookDir As New DirectoryInfo(guestBookName)

            For Each fileItem As FileInfo In guestBookDir.GetFiles()
                Try
                    entries.Add(GetEntryFromFile(fileItem))
                Catch
                    ' An error occurred when calling GetEntryFromFile().
                    ' Ignore this file because it can't be read.
End Try
Next
        Catch err As Exception
            ' An error occurred when calling GetFiles().
            ' Ignore this error and leave the entries collection empty.
        End Try

        Return entries
    End Function

    Private Function GetEntryFromFile(ByVal entryFile As FileInfo) _
      As BookEntry
        ' Turn the file information into a BookEntry object.
        Dim newEntry As New BookEntry()
        Dim r As StreamReader = entryFile.OpenText()
        newEntry.Author = r.ReadLine()
        newEntry.Submitted = DateTime.Parse(r.ReadLine())
        newEntry.Message = r.ReadLine()
        r.Close()

        return newEntry
    End Function

    Private Sub SaveEntry(ByVal entry As BookEntry)
        ' Create a new file for this entry, with a file name that should
        ' be statistically unique.
        Dim random As New Random()
        Dim fileName As String = guestBookName & ""
        fileName &= DateTime.Now.Ticks.ToString() & random.Next(100).ToString()
        Dim newFile As New FileInfo(fileName)
        Dim w As StreamWriter = newFile.CreateText()

        ' Write the information to the file.
        w.WriteLine(entry.Author)
        w.WriteLine(entry.Submitted.ToString())
        w.WriteLine(entry.Message)
        w.Close()
    End Sub

End Class

Dissecting the Code . . .

  • The code uses text files so you can easily review the information on your own with Notepad. You could use binary files just as easily, which would save a small amount of space.

  • The file name for each entry is generated using a combination of the current date and time (in ticks) and a random number. Practically speaking, this makes it impossible for a file to be generated with a duplicate file name.

  • This program uses error handling to defend against possible problems. However, errors are handled in a different way depending on when they occur. If an error occurs when saving a new entry in the cmdSubmit_Click() method, the user is alerted to the problem, but the display is not updated. Instead, the user-supplied information is left in the controls so the save operation can be reattempted. When reading the existing files in the cmdGetAllEntries_Click() method, two problems can occur, and they're dealt with using separate exception blocks. A problem can happen when the code calls GetFiles() to retrieve the file list. In this situation, the problem is ignored but no files are found, and so no guest book entries are shown. If this step succeeds, a problem can still occur when reading each file in the GetEntryFromFile() method. In this situation, the file that caused the problem is ignored, but the code continues and attempts to read the remaining files.

    Note

    The error handling code in this example does a good job of recovering from the brink of disaster and allowing the user to keep working, when it's possible. However, the error handling code might not do enough to alert you that there's a problem. If the problem is a freak occurrence, this behavior is fine. But if the problem is a symptom of a deeper issue in your web application, you should know about it.

    To make sure that problems aren't overlooked, you might choose to show an error message on the page when an exception occurs. Even better, your code could quietly create an entry in the event log that records the problem (as explained in Chapter 7). That way, you can find out about the problems that have occurred and correct them later.

  • Careful design makes sure this program isolates file writing and reading code in separate functions, such as SaveEntry(), GetAllEntries(), and GetEntryFromFile(). For even better organization, you could move these routines in a separate class or even a separate component. This would allow you to use the ObjectDataSource to reduce your data binding code. For more information, read Chapter 22.

Allowing File Uploads

Although you've seen detailed examples of how to work with files and directories on the web server, you haven't yet considered the question of how to allow file uploads. The problem with file uploading is that you need some way to retrieve information from the client—and as you already know, all ASP.NET code executes on the server.

The FileUpload Control

Fortunately, ASP.NET includes a control that allows website users to upload files to the web server. Once the web server receives the posted file data, it's up to your application to examine it, ignore it, or save it to a back-end database or a file on the web server. The FileUpload control does this work, and it represents the <input type="file"> HTML tag.

Declaring the FileUpload control is easy. It doesn't expose any new properties or events you can use through the control tag:

<asp:FileUpload ID="Uploader" runat="server" />

The <input type="file"> tag doesn't give you much choice as far as user interface is concerned (it's limited to a text box that contains a file name and a Browse button). When the user clicks Browse, the browser presents an Open dialog box and allows the user to choose a file. This part is hardwired into the browser, and you can't change this behavior. Once the user selects a file, the file name is filled into the corresponding text box. However, the file isn't uploaded yet—that happens later, when the page is posted back. At this point, all the data from all input controls (including the file data) is sent to the server. For that reason, it's common to add a button to post back the page.

To get information about the posted file content, you can access the FileUpload.PostedFile object. You can save the content by calling the PostedFile.SaveAs() method:

Uploader.PostedFile.SaveAs("c:Uploads
ewfile")

Figure 17-6 shows a complete web page that demonstrates how to upload a user-specified file. This example introduces a twist—it allows the upload of only those files with the extensions .bmp, .gif, and .jpg.

A simple file uploader

Figure 17.6. A simple file uploader

Here's the code for the upload page:

Public Partial Class UploadFile
    Inherits System.Web.UI.Page

    Private uploadDirectory As String

    Protected Sub Page_Load(ByVal sender As Object, _
      ByVal e As EventArgs)  Handles Me.Load

        ' Place files in a website subfolder named Uploads.
        uploadDirectory = Path.Combine( _
          Request.PhysicalApplicationPath, "Uploads")
    End Sub
Protected Sub cmdUpload_Click(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles cmdUpload.Click

        ' Check that a file is actually being submitted.
        If Uploader.PostedFile.FileName = "" Then
            lblInfo.Text = "No file specified."
        Else
            ' Check the extension.
            Dim extension As String = _
              Path.GetExtension(Uploader.PostedFile.FileName)

            Select Case extension.ToLower()
                Case ".bmp", ".gif", ".jpg"
                    ' This is an allowed file type.
                Case Else
                    lblInfo.Text = "This file type is not allowed."
                    Return
            End Select

            ' Using this code, the saved file will retain its original
            ' file name when it's placed on the server.
            Dim serverFileName As String = _
              Path.GetFileName(Uploader.PostedFile.FileName)
            Dim fullUploadPath As String = _
              Path.Combine(uploadDirectory, serverFileName)

            Try
                Uploader.PostedFile.SaveAs(fullUploadPath)

                lblInfo.Text = "File " & serverFileName
                lblInfo.Text &= " uploaded successfully to "
                lblInfo.Text &= fullUploadPath
            Catch Err As Exception
                lblInfo.Text = err.Message
            End Try
        End If

    End Sub

End Class

Dissecting the Code . . .

  • The saved file keeps its original (client-side) name. The code uses the Path.GetFileName() shared method to transform the fully qualified name provided by FileUpload.PostedFile.FileName and retrieve just the file, without the path.

  • The FileUpload.PostedFile object contains only a few properties. One interesting property is ContentLength, which returns the size of the file in bytes. You could examine this setting and use it to prevent a user from uploading excessively large files.

The Last Word

Although databases and websites make a perfect fit, nothing is preventing you from using the classes in the .NET Framework to access other types of data, including files. In fact, the code you use to interact with the file system is the same as what you would use in a desktop application or any .NET program. Thanks to the .NET Framework, you can finally solve common programming problems in the same way, regardless of the type of application you're creating.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.254.44