3. 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:\testfile.txt", lines)
' Read the file in one shot (into a variable named content).
Dim content As String = File.ReadAllText("c:\testfile.txt")
Table 1 describes the full set of quick file access methods. All of these are shared methods.
Table 1. 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.
4. 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 2.
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 3.
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_Data\GuestBook) 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
4.1. 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 . 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.