Chapter 17. Files and Streams

 

I can only assume that a “Do Not File” document is filed in a “Do Not File” file.

 
 --Senator Frank Church Senate Intelligence Subcommittee Hearing, 1975
 

Consciousness ... does not appear to itself chopped up in bits. ... A “river” or a “stream” are the metaphors by which it is most naturally described.

 
 --William James
 

I read part of it all the way through.

 
 --Samuel Goldwyn
<feature> <supertitle>Objectives</supertitle>

In this chapter you’ll learn:

<objective>

To create, read, write and update files.

</objective>
<objective>

To use classes File and Directory to obtain information about files and directories on your computer.

</objective>
<objective>

To use LINQ to search through directories.

</objective>
<objective>

To become familiar with sequential-access file processing.

</objective>
<objective>

To use classes FileStream, StreamReader and StreamWriter to read text from and write text to files.

</objective>
<objective>

To use classes FileStream and BinaryFormatter to read objects from and write objects to files.

</objective>
</feature>
<feature> <supertitle>Outline</supertitle> </feature>

Introduction

Variables and arrays offer only temporary storage of data—the data is lost when a local variable “goes out of scope” or when the program terminates. By contrast, files (and databases, which we cover in Chapter 18) are used for long-term retention of large amounts of data, even after the program that created the data terminates. Data maintained in files often is called persistent data. Computers store files on secondary storage devices, such as magnetic disks, optical disks, flash memory and magnetic tapes. In this chapter, we explain how to create, update and process data files in C# programs.

We begin with an overview of the data hierarchy from bits to files. Next, we overview some of the Framework Class Library’s file-processing classes. We then present two examples that show how you can determine information about the files and directories on your computer. The remainder of the chapter shows how to write to and read from text files that are human readable and binary files that store entire objects in binary format.

Data Hierarchy

Ultimately, all data items that computers process are reduced to combinations of 0s and 1s. This occurs because it’s simple and economical to build electronic devices that can assume two stable states—one state represents 0 and the other represents 1. It’s remarkable that the impressive functions performed by computers involve only the most fundamental manipulations of 0s and 1s.

Bits

The smallest data item that computers support is called a bit (short for “binary digit”—a digit that can assume one of two values). Each bit can assume either the value 0 or the value 1. Computer circuitry performs various simple bit manipulations, such as examining the value of a bit, setting the value of a bit and reversing a bit (from 1 to 0 or from 0 to 1).

Characters

Programming with data in the low-level form of bits is cumbersome. It’s preferable to program with data in forms such as decimal digits (i.e., 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9), letters (i.e., AZ and az) and special symbols (i.e., $, @, %, &, *, (, ), -, +, ", :, ?, / and many others). Digits, letters and special symbols are referred to as characters. The set of all characters used to write programs and represent data items on a particular computer is called that computer’s character set. Because computers can process only 0s and 1s, every character in a computer’s character set is represented as a pattern of 0s and 1s. Bytes are composed of eight bits. C# uses the Unicode® character set (www.unicode.org) in which characters are composed of 2 bytes. Programmers create programs and data items with characters; computers manipulate and process these characters as patterns of bits.

Fields

Just as characters are composed of bits, fields are composed of characters. A field is a group of characters that conveys meaning. For example, a field consisting of uppercase and lowercase letters can represent a person’s name.

Data items processed by computers form a data hierarchy (Fig. 17.1), in which data items become larger and more complex in structure as we progress from bits to characters to fields to larger data aggregates.

Data hierarchy.

Figure 17.1. Data hierarchy.

Records and Files

Typically, a record (which can be represented as a class) is composed of several related fields. In a payroll system, for example, a record for a particular employee might include the following fields:

  1. Employee identification number

  2. Name

  3. Address

  4. Hourly pay rate

  5. Number of exemptions claimed

  6. Year-to-date earnings

  7. Amount of taxes withheld

In the preceding example, each field is associated with the same employee. A file is a group of related records.[1] A company’s payroll file normally contains one record for each employee. A payroll file for a small company might contain only 22 records, whereas one for a large company might contain 100,000. It’s not unusual for a company to have many files, some containing millions, billions or even trillions of characters of information.

Record Key

To facilitate the retrieval of specific records from a file, at least one field in each record is chosen as a record key, which identifies a record as belonging to a particular person or entity and distinguishes that record from all others. For example, in a payroll record, the employee identification number normally would be the record key.

Sequential Files

There are many ways to organize records in a file. A common organization is called a sequential file, in which records typically are stored in order by a record-key field. In a payroll file, records usually are placed in order by employee identification number. The first employee record in the file contains the lowest employee identification number, and subsequent records contain increasingly higher ones.

Databases

Most businesses use many different files to store data. For example, a company might have payroll files, accounts-receivable files (listing money due from clients), accounts-payable files (listing money due to suppliers), inventory files (listing facts about all the items handled by the business) and many other files. A group of related files often are stored in a database. A collection of programs designed to create and manage databases is called a database management system (DBMS). We discuss databases in Chapter 18.

Files and Streams

C# views each file as a sequential stream of bytes (Fig. 17.2). Each file ends either with an end-of-file marker or at a specific byte number that’s recorded in a system-maintained administrative data structure. When a file is opened, an object is created and a stream is associated with the object. When a console application executes, the runtime environment creates three stream objects that are accessible via properties Console.Out, Console.In and Console.Error, respectively. These objects facilitate communication between a program and a particular file or device. Console.In refers to the standard input stream object, which enables a program to input data from the keyboard. Console.Out refers to the standard output stream object, which enables a program to output data to the screen. Console.Error refers to the standard error stream object, which enables a program to output error messages to the screen. We have been using Console.Out and Console.In in our console applications, Console methods Write and WriteLine use Console.Out to perform output, and Console methods Read and ReadLine use Console.In to perform input.

C#’s view of an n-byte file.

Figure 17.2. C#’s view of an n-byte file.

There are many file-processing classes in the Framework Class Library. The System.IO namespace includes stream classes such as StreamReader (for text input from a file), StreamWriter (for text output to a file) and FileStream (for both input from and output to a file). These stream classes inherit from abstract classes TextReader, TextWriter and Stream, respectively. Actually, properties Console.In and Console.Out are of type TextReader and TextWriter, respectively. The system creates objects of TextReader and TextWriter derived classes to initialize Console properties Console.In and Console.Out.

Abstract class Stream provides functionality for representing streams as bytes. Classes FileStream, MemoryStream and BufferedStream (all from namespace System.IO) inherit from class Stream. Class FileStream can be used to write data to and read data from files. Class MemoryStream enables the transfer of data directly to and from memory—this is much faster than reading from and writing to external devices. Class BufferedStream uses buffering to transfer data to or from a stream. Buffering is an I/O performance-enhancement technique, in which each output operation is directed to a region in memory, called a buffer, that’s large enough to hold the data from many output operations. Then actual transfer to the output device is performed in one large physical output operation each time the buffer fills. The output operations directed to the output buffer in memory often are called logical output operations. Buffering can also be used to speed input operations by initially reading more data than is required into a buffer, so subsequent reads get data from memory rather than an external device.

In this chapter, we use key stream classes to implement file-processing programs that create and manipulate sequential-access files.

Classes File and Directory

Information is stored in files, which are organized in directories (also called folders). Classes File and Directory enable programs to manipulate files and directories on disk. Class File can determine information about files and can be used to open files for reading or writing. We discuss techniques for writing to and reading from files in subsequent sections.

Figure 17.3 lists several of class File’s static methods for manipulating and determining information about files. We demonstrate several of these methods in Fig. 17.5.

Table 17.3. File class static methods (partial list).

static Method

Description

AppendText

Returns a StreamWriter that appends text to an existing file or creates a file if one does not exist.

Copy

Copies a file to a new file.

Create

Creates a file and returns its associated FileStream.

CreateText

Creates a text file and returns its associated StreamWriter.

Delete

Deletes the specified file.

Exists

Returns true if the specified file exists and false otherwise.

GetCreationTime

Returns a DateTime object representing when the file was created.

GetLastAccessTime

Returns a DateTime object representing when the file was last accessed.

GetLastWriteTime

Returns a DateTime object representing when the file was last modified.

Move

Moves the specified file to a specified location.

Open

Returns a FileStream associated with the specified file and equipped with the specified read/write permissions.

OpenRead

Returns a read-only FileStream associated with the specified file.

OpenText

Returns a StreamReader associated with the specified file.

OpenWrite

Returns a write FileStream associated with the specified file.

Class Directory provides capabilities for manipulating directories. Figure 17.4 lists some of class Directory’s static methods for directory manipulation. Figure 17.5 demonstrates several of these methods, as well. The DirectoryInfo object returned by method CreateDirectory contains information about a directory. Much of the information contained in class DirectoryInfo also can be accessed via the methods of class Directory.

Table 17.4. Directory class static methods.

static Method

Description

CreateDirectory

Creates a directory and returns its associated DirectoryInfo object.

Delete

Deletes the specified directory.

Exists

Returns true if the specified directory exists and false otherwise.

GetDirectories

Returns a string array containing the names of the subdirectories in the specified directory.

GetFiles

Returns a string array containing the names of the files in the specified directory.

GetCreationTime

Returns a DateTime object representing when the directory was created.

GetLastAccessTime

Returns a DateTime object representing when the directory was last accessed.

GetLastWriteTime

Returns a DateTime object representing when items were last written to the directory.

Move

Moves the specified directory to a specified location.

Example 17.5. Using classes File and Directory.

 1   // Fig. 17.5: FileTestForm.cs
 2   // Using classes File and Directory.
 3   using System;
 4   using System.Windows.Forms;
 5   using System.IO;
 6
 7   namespace FileTest
 8   {
 9      // displays contents of files and directories
10      public partial class FileTestForm : Form
11      {
12         // parameterless constructor
13         public FileTestForm()
14         {
15            InitializeComponent();
16         } // end constructor
17
18         // invoked when user presses key
19         private void inputTextBox_KeyDown( object sender, KeyEventArgs e )
20         {
21            // determine whether user pressed Enter key
22            if ( e.KeyCode == Keys.Enter )
23            {
24               // get user-specified file or directory
25                string fileName = inputTextBox.Text;
26
27               // determine whether fileName is a file
28               if ( File.Exists( fileName ) )
29               {
30                  // get file's creation date, modification date, etc.
31                  GetInformation( fileName );
32                  StreamReader stream = null; // declare StreamReader
33
34                  // display file contents through StreamReader
35                  try
36                  {
37                     // obtain reader and file contents
38                     using ( stream = new StreamReader( fileName ) )
39                     {
40                        outputTextBox.AppendText( stream.ReadToEnd() );
41                     } // end using
42                  } // end try
43                  catch  ( IOException )
44                  {
45                     MessageBox.Show( "Error reading from file",
46                        "File Error", MessageBoxButtons.OK,
47                         MessageBoxIcon.Error );
48                  } // end catch
49               } // end if
50               // determine whether fileName is a directory
51               else if ( Directory.Exists( fileName ) )
52               {
53                  // get directory's creation date,
54                  // modification date, etc.
55                  GetInformation( fileName );
56
57                  // obtain file/directory list of specified directory
58                  string[] directoryList =                
59                     Directory.GetDirectories( fileName );
60
61                  outputTextBox.AppendText( "Directory contents:
" );
62
63                  // output directoryList contents
64                  foreach ( var directory in directoryList )
65                     outputTextBox.AppendText( directory + "
" );
66               } // end else if
67               else
68               {
69                  // notify user that neither file nor directory exists
70                  MessageBox.Show( inputTextBox.Text +
71                     " does not exist", "File Error",
72                     MessageBoxButtons.OK, MessageBoxIcon.Error );
73               } // end else
74            } // end if
75         } // end method inputTextBox_KeyDown
76
77         // get information on file or directory,
78         // and output it to outputTextBox
79         private void GetInformation( string fileName )
80         {
81            outputTextBox.Clear();
82
83            // output that file or directory exists
84            outputTextBox.AppendText( fileName + " exists
" );
85
86            // output when file or directory was created
87            outputTextBox.AppendText( "Created: " +
88               File.GetCreationTime( fileName ) + "
" );
89
90            // output when file or directory was last modified
91            outputTextBox.AppendText( "Last modified: " +
92               File.GetLastWriteTime( fileName ) + "
" );
93
94            // output when file or directory was last accessed
95            outputTextBox.AppendText( "Last accessed: " +
96               File.GetLastAccessTime( fileName ) + "
" );
97         } // end method GetInformation
98      } // end class FileTestForm
99   } // end namespace FileTest
Using classes File and Directory.

a) Viewing the contents of file "quotes.txt"

Using classes File and Directory.

b) Viewing all files in directory C:Program Files

Using classes File and Directory.

c) User gives invalid input

Using classes File and Directory.

d) Error message is displayed

Demonstrating Classes File and Directory

Class FileTestForm (Fig. 17.5) uses File and Directory methods to access file and directory information. This Form contains the control inputTextBox, in which the user enters a file or directory name. For each key that the user presses while typing in the TextBox, the program calls event handler inputTextBox_KeyDown (lines 19–75). If the user presses the Enter key (line 22), this method displays either the file’s or directory’s contents, depending on the text the user input. (If the user does not press the Enter key, this method returns without displaying any content.) Line 28 uses File method Exists to determine whether the user-specified text is the name of an existing file. If so, line 31 invokes private method GetInformation (lines 79–97), which calls File methods GetCreationTime (line 88), GetLastWriteTime (line 92) and GetLastAccessTime (line 96) to access file information. When method GetInformation returns, line 38 instantiates a StreamReader for reading text from the file. The StreamReader constructor takes as an argument a string containing the name of the file to open. Line 40 calls StreamReader method ReadToEnd to read the entire contents of the file as a string, then appends the string to outputTextBox. Once the file has been read, the using block terminates, closes the file and disposes of the corresponding object.

If line 28 determines that the user-specified text is not a file, line 51 determines whether it’s a directory using Directory method Exists. If the user specified an existing directory, line 55 invokes method GetInformation to access the directory information. Line 59 calls Directory method GetDirectories to obtain a string array containing the names of subdirectories in the specified directory. Lines 64–65 display each element in the string array. Note that, if line 51 determines that the user-specified text is not a directory name, lines 70–72 notify the user (via a MessageBox) that the name the user entered does not exist as a file or directory.

Searching Directories with LINQ

We now consider another example that uses file- and directory-manipulation capabilities. Class LINQToFileDirectoryForm (Fig. 17.6) uses LINQ with classes File, Path and Directory to report the number of files of each file type that exist in the specified directory path. The program also serves as a “clean-up” utility—when it finds a file that has the .bak file-name extension (i.e., a backup file), the program displays a MessageBox asking the user whether that file should be removed, then responds appropriately to the user’s input. This example also uses LINQ to Objects to help delete the backup files.

Example 17.6. Using LINQ to search directories and determine file types.

 1   // Fig. 17.6: LINQToFileDirectoryForm.cs
 2   // Using LINQ to search directories and determine file types.
 3   using System;
 4   using System.Collections.Generic;
 5   using System.Linq;
 6   using System.Windows.Forms;
 7   using System.IO;
 8
 9   namespace LINQToFileDirectory
10   {
11      public partial class LINQToFileDirectoryForm : Form
12      {
13         string currentDirectory; // directory to search
14
15         // store extensions found, and number of each extension found
16         Dictionary<string, int> found = new Dictionary<string, int>();
17
18         // parameterless constructor
19         public LINQToFileDirectoryForm()
20         {
21            InitializeComponent();
22         } // end constructor
23
24         // handles the Search Directory Button's Click event
25         private void searchButton_Click( object sender, EventArgs e )
26         {
27            // check whether user specified path exists
28            if ( pathTextBox.Text != string.Empty &&
29               !Directory.Exists( pathTextBox.Text ) )
30            {
31               // show error if user does not specify valid directory
32               MessageBox.Show( "Invalid Directory", "Error",
33                  MessageBoxButtons.OK, MessageBoxIcon.Error );
34            } // end if
35            else
36            {
37               // use current directory if no directory is specified
38               if ( pathTextBox.Text == string.Empty )
39                  currentDirectory = Directory.GetCurrentDirectory();
40               else
41                  currentDirectory = pathTextBox.Text;
42
43               directoryTextBox.Text = currentDirectory; // show directory
44
45               // clear TextBoxes
46               pathTextBox.Clear();
47               resultsTextBox.Clear();
48
49               SearchDirectory( currentDirectory ); // search the directory
50
51               // allow user to delete .bak files
52               CleanDirectory( currentDirectory );
53
54               // summarize and display the results
55               foreach ( var current in found.Keys )
56               {
57                  // display the number of files with current extension
58                  resultsTextBox.AppendText( string.Format(
59                     "* Found {0} {1} files.
",
60                     found[ current ], current ) );
61               } // end foreach
62
63               found.Clear(); // clear results for new search
64            } // end else
65         } // end method searchButton_Click
66
67         // search directory using LINQ
68         private void SearchDirectory( string folder )
69         {
70            // files contained in the directory
71            string[] files = Directory.GetFiles( folder );
72
73            // subdirectories in the directory
74            string[] directories = Directory.GetDirectories( folder );
75
76            // find all file extensions in this directory
77            var extensions =
78               ( from file in files                            
79                 select Path.GetExtension( file ) ).Distinct();
80
81            // count the number of files using each extension
82            foreach ( var extension in extensions )
83            {
84               var temp = extension;
85
86               // count the number of files with the extension
87               var extensionCount =
88                  ( from file in files                     
89                    where Path.GetExtension( file ) == temp
90                    select file ).Count();                 
91
92               // if the Dictionary already contains a key for the extension
93               if ( found.ContainsKey( extension ) )
94                  found[ extension ] += extensionCount; // update the count
95               else
96                  found.Add( extension, extensionCount ); // add new count
97            } // end foreach
98
99            // recursive call to search subdirectories
100           foreach ( var subdirectory in directories )
101              SearchDirectory( subdirectory );
102        } // end method SearchDirectory
103
104        // allow user to delete backup files (.bak)
105        private void CleanDirectory( string folder )
106        {
107           // files contained in the directory
108           string[] files = Directory.GetFiles( folder );
109
110           // subdirectories in the directory
111           string[] directories = Directory.GetDirectories( folder );
112
113           // select all the backup files in this directory
114           var backupFiles =
115              from file in files                       
116              where Path.GetExtension( file ) == ".bak"
117              select file;                             
118
119           // iterate over all backup files (.bak)
120           foreach ( var backup in backupFiles )
121           {
122              DialogResult result = MessageBox.Show( "Found backup file " +
123                 Path.GetFileName( backup ) + ". Delete?", "Delete Backup",
124                 MessageBoxButtons.YesNo, MessageBoxIcon.Question );
125
126              // delete file if user clicked 'yes'
127              if ( result == DialogResult.Yes )
128              {
129                 File.Delete( backup ); // delete backup file       
130                 --found[ ".bak" ]; // decrement count in Dictionary
131
132                 // if there are no .bak files, delete key from Dictionary
133                 if ( found[ ".bak" ] == 0 )
134                    found.Remove( ".bak" );
135              } // end if
136           } // end foreach
137
138           // recursive call to clean subdirectories
139           foreach ( var subdirectory in directories )
140              CleanDirectory( subdirectory );
141        } // end method CleanDirectory
142     } // end class LINQToFileDirectoryForm
143  } // end namespace LINQToFileDirectory
Using LINQ to search directories and determine file types.

When the user clicks Search Directory, the program invokes searchButton_Click (lines 25–65), which searches recursively through the directory path specified by the user. If the user inputs text in the TextBox, line 29 calls Directory method Exists to determine whether that text is a valid directory. If it’s not, lines 32–33 notify the user of the error.

Method SearchDirectory

Lines 38–41 get the current directory (if the user did not specify a path) or the specified directory. Line 49 passes the directory name to recursive method SearchDirectory (lines 68–102). Line 71 calls Directory method GetFiles to get a string array containing file names in the specified directory. Line 74 calls Directory method GetDirectories to get a string array containing the subdirectory names in the specified directory.

Lines 78–79 use LINQ to get the Distinct file-name extensions in the files array. Path method GetExtension obtains the extension for the specified file name. For each file-name extension returned by the LINQ query, lines 82–97 determine the number of occurrences of that extension in the files array. The LINQ query at lines 88–90 compares each file-name extension in the files array with the current extension being processed (line 89). All matches are included in the result. We then use LINQ method Count to determine the total number of files that matched the current extension.

Class LINQToFileDirectoryForm uses a Dictionary (declared in line 16) to store each file-name extension and the corresponding number of file names with that extension. A Dictionary (namespace System.Collections.Generic) is a collection of key/value pairs, in which each key has a corresponding value. Class Dictionary is a generic class like class List (presented in Section 9.4). Line 16 indicates that the Dictionary found contains pairs of strings and ints, which represent the file-name extensions and the number of files with those extensions, respectively. Line 93 uses Dictionary method ContainsKey to determine whether the specified file-name extension has been placed in the Dictionary previously. If this method returns true, line 94 adds the extensionCount determined in lines 88–90 to the current total for that extension that’s stored in the Dictionary. Otherwise, line 96 uses Dictionary method Add to insert a new key/value pair into the Dictionary for the new file-name extension and its extensionCount. Lines 100–101 recursively call SearchDirectory for each subdirectory in the current directory.

Method CleanDirectory

When method SearchDirectory returns, line 52 calls CleanDirectory (defined at lines 105–141) to search for all files with extension .bak. Lines 108 and 111 obtain the list of file names and list of directory names in the current directory, respectively. The LINQ query in lines 115–117 locates all file names in the current directory that have the .bak extension. Lines 120–136 iterate through the query’s results and prompt the user to determine whether each file should be deleted. If the user clicks Yes in the dialog, line 129 uses File method Delete to remove the file from disk, and line 130 subtracts 1 from the total number of .bak files. If the number of .bak files remaining is 0, line 134 uses Dictionary method Remove to delete the key/value pair for .bak files from the Dictionary. Lines 139–140 recursively call CleanDirectory for each subdirectory in the current directory. After each subdirectory has been checked for .bak files, method CleanDirectory returns, and lines 55–61 display the summary of file-name extensions and the number of files with each extension. Line 55 uses Dictionary property Keys to get all the keys in the Dictionary. Line 60 uses the Dictionary’s indexer to get the value for the current key. Finally, line 63 uses Dictionary method Clear to delete the contents of the Dictionary.

Creating a Sequential-Access Text File

C# imposes no structure on files. Thus, the concept of a “record” does not exist in C# files. This means that you must structure files to meet the requirements of your applications. The next few examples use text and special characters to organize our own concept of a “record.”

Class BankUIForm

The following examples demonstrate file processing in a bank-account maintenance application. These programs have similar user interfaces, so we created reusable class BankUIForm (Fig. 17.7) to encapsulate a base-class GUI (see the screen capture in Fig. 17.7). Class BankUIForm contains four Labels and four TextBoxes. Methods ClearTextBoxes (lines 28–40), SetTextBoxValues (lines 43–64) and GetTextBoxValues (lines 67–78) clear, set the values of and get the values of the text in the TextBoxes, respectively.

Example 17.7. Base class for GUIs in our file-processing applications.

 1   // Fig. 17.7: BankUIForm.cs
 2   // A reusable Windows Form for the examples in this chapter.
 3   using System;
 4   using System.Windows.Forms;
 5
 6   namespace BankLibrary
 7   {
 8      public partial class BankUIForm : Form
 9      {
10         protected int TextBoxCount = 4; // number of TextBoxes on Form
11
12         // enumeration constants specify TextBox indices
13         public enum TextBoxIndices
14         {
15            ACCOUNT,
16            FIRST,
17            LAST,
18            BALANCE
19         } // end enum
20
21         // parameterless constructor
22         public BankUIForm()
23         {
24            InitializeComponent();
25         } // end constructor
26
27         // clear all TextBoxes
28         public void ClearTextBoxes()
29         {
30            // iterate through every Control on form
31            foreach ( Control guiControl in Controls )
32            {
33               // determine whether Control is TextBox
34               if ( guiControl is TextBox )
35               {
36                  // clear TextBox
37                  ( ( TextBox ) guiControl ).Clear();
38               } // end if
39            } // end for
40         } // end method ClearTextBoxes
41
42         // set text box values to string-array values
43         public void SetTextBoxValues( string[] values )
44         {
45            // determine whether string array has correct length
46            if ( values.Length != TextBoxCount )
47            {
48               // throw exception if not correct length
49               throw ( new ArgumentException( "There must be " +
50                  ( TextBoxCount + 1 ) + " strings in the array" ) );
51            } // end if
52            // set array values if array has correct length
53            else
54            {
55               // set array values to TextBox values
56               accountTextBox.Text =
57                  values[ ( int ) TextBoxIndices.ACCOUNT ];
58               firstNameTextBox.Text =
59                  values[ ( int ) TextBoxIndices.FIRST ];
60               lastNameTextBox.Text = values[ ( int ) TextBoxIndices.LAST ];
61               balanceTextBox.Text =
62                  values[ ( int ) TextBoxIndices.BALANCE ];
63            } // end else
64         } // end method SetTextBoxValues
65
66         // return TextBox values as string array
67         public string[] GetTextBoxValues()
68         {
69            string[] values = new string[ TextBoxCount ];
70
71            // copy TextBox fields to string array
72            values[ ( int ) TextBoxIndices.ACCOUNT ] = accountTextBox.Text;
73            values[ ( int ) TextBoxIndices.FIRST ] = firstNameTextBox.Text;
74            values[ ( int ) TextBoxIndices.LAST ] = lastNameTextBox.Text;
75            values[ ( int ) TextBoxIndices.BALANCE ] = balanceTextBox.Text;
76
77            return values;
78         } // end method GetTextBoxValues
79      } // end class BankUIForm
80   } // end namespace BankLibrary
Base class for GUIs in our file-processing applications.

Using visual inheritance (Section 15.13), you can extend this class to create the GUIs for several examples in this chapter. Recall that to reuse class BankUIForm, you must compile the GUI into a class library, then add a reference to the new class library in each project that will reuse it. This library (BankLibrary) is provided with the code for this chapter. You might need to re-add the references to this library in our examples when you copy them to your system, since the library most likely will reside in a different location on your system.

Class Record

Figure 17.8 contains class Record that Figs. 17.9, 17.11 and 17.12 use for maintaining the information in each record that’s written to or read from a file. This class also belongs to the BankLibrary DLL, so it’s located in the same project as class BankUIForm.

Example 17.8. Record for sequential-access file-processing applications.

 1   // Fig. 17.8: Record.cs
 2   // Class that represents a data record.
 3
 4   namespace BankLibrary
 5   {
 6      public class Record
 7      {
 8         // auto-implemented Account property  
 9         public int Account { get; set; }      
10
11         // auto-implemented FirstName property
12         public string FirstName { get; set; } 
13
14         // auto-implemented LastName property 
15         public string LastName { get; set; }  
16
17         // auto-implemented Balance property  
18         public decimal Balance { get; set; }  
19
20         // parameterless constructor sets members to default values
21         public Record()
22            : this( 0, string.Empty, string.Empty, 0M )
23         {
24         } // end constructor
25
26         // overloaded constructor sets members to parameter values
27         public Record( int accountValue, string firstNameValue,
28            string lastNameValue, decimal balanceValue )
29         {
30            Account = accountValue;
31            FirstName = firstNameValue;
32            LastName = lastNameValue;
33            Balance = balanceValue;
34         } // end constructor
35      } // end class Record
36   } // end namespace BankLibrary

Example 17.9. Creating and writing to a sequential-access file.

 1   // Fig. 17.9: CreateFileForm.cs
 2   // Creating a sequential-access file.
 3   using System;
 4   using System.Windows.Forms;
 5   using System.IO;
 6   using BankLibrary;
 7
 8   namespace CreateFile
 9   {
10      public partial class CreateFileForm : BankUIForm
11      {
12         private StreamWriter fileWriter; // writes data to text file
13
14         // parameterless constructor
15         public CreateFileForm()
16         {
17            InitializeComponent();
18         } // end constructor
19
20         // event handler for Save Button
21         private void saveButton_Click( object sender, EventArgs e )
22         {
23            // create and show dialog box enabling user to save file
24            DialogResult result; // result of SaveFileDialog
25            string fileName; // name of file containing data
26
27            using ( SaveFileDialog fileChooser = new SaveFileDialog() )     
28            {                                                               
29               fileChooser.CheckFileExists = false; // let user create file 
30               result = fileChooser.ShowDialog();                           
31               fileName = fileChooser.FileName; // name of file to save data
32            } // end using                                                  
33
34            // ensure that user clicked "OK"
35            if ( result == DialogResult.OK )
36            {
37               // show error if user specified invalid file
38               if ( fileName == string.Empty )
39                  MessageBox.Show( "Invalid File Name", "Error",
40                     MessageBoxButtons.OK, MessageBoxIcon.Error );
41               else
42               {
43                  // save file via FileStream if user specified valid file
44                  try
45                  {
46                     // open file with write access
47                     FileStream output = new FileStream( fileName,
48                        FileMode.OpenOrCreate, FileAccess.Write );
49
50                     // sets file to where data is written
51                     fileWriter = new StreamWriter( output );
52
53                     // disable Save button and enable Enter button
54                     saveButton.Enabled = false;
55                     enterButton.Enabled = true;
56                  } // end try
57                  // handle exception if there is a problem opening the file
58                  catch ( IOException )
59                  {
60                     // notify user if file does not exist
61                     MessageBox.Show( "Error opening file", "Error",
62                        MessageBoxButtons.OK, MessageBoxIcon.Error );
63                  } // end catch
64               } // end else
65            } // end if
66         } // end method saveButton_Click
67
68         // handler for enterButton Click
69         private void enterButton_Click( object sender, EventArgs e )
70         {
71            // store TextBox values string array
72            string[] values = GetTextBoxValues();
73
74            // Record containing TextBox values to output
75            Record record = new Record();
76
77            // determine whether TextBox account field is empty
78            if ( values[ ( int ) TextBoxIndices.ACCOUNT ] != string.Empty )
79            {
80               // store TextBox values in Record and output it
81               try
82               {
83                  // get account-number value from TextBox
84                  int accountNumber = Int32.Parse(
85                     values[ ( int ) TextBoxIndices.ACCOUNT ] );
86
87                  // determine whether accountNumber is valid
88                  if ( accountNumber > 0 )
89                  {
90                     // store TextBox fields in Record
91                     record.Account = accountNumber;
92                     record.FirstName = values[ ( int )
93                        TextBoxIndices.FIRST ];
94                     record.LastName = values[ ( int )
95                        TextBoxIndices.LAST ];
96                     record.Balance = Decimal.Parse(
97                        values[ ( int ) TextBoxIndices.BALANCE ] );
98
99                     // write Record to file, fields separated by commas
100                    fileWriter.WriteLine(                             
101                       record.Account + "," + record.FirstName + "," +
102                       record.LastName + "," + record.Balance );      
103                 } // end if
104                 else
105                 {
106                    // notify user if invalid account number
107                    MessageBox.Show( "Invalid Account Number", "Error",
108                       MessageBoxButtons.OK, MessageBoxIcon.Error );
109                 } // end else
110              } // end try
111              // notify user if error occurs during the output operation
112              catch ( IOException )
113              {
114                 MessageBox.Show( "Error Writing to File", "Error",
115                    MessageBoxButtons.OK, MessageBoxIcon.Error );
116              } // end catch
117              // notify user if error occurs regarding parameter format
118              catch ( FormatException )
119              {
120                 MessageBox.Show( "Invalid Format", "Error",
121                    MessageBoxButtons.OK, MessageBoxIcon.Error );
122              } // end catch
123           } // end if
124
125           ClearTextBoxes(); // clear TextBox values
126        } // end method enterButton_Click
127
128        // handler for exitButton Click
129        private void exitButton_Click( object sender, EventArgs e )
130        {
131           // determine whether file exists
132           if ( fileWriter != null )
133           {
134              try
135              {
136                 // close StreamWriter and underlying file
137                 fileWriter.Close();
138              } // end try
139              // notify user of error closing file
140              catch ( IOException )
141              {
142                 MessageBox.Show( "Cannot close file", "Error",
143                    MessageBoxButtons.OK, MessageBoxIcon.Error );
144              } // end catch
145           } // end if
146
147           Application.Exit();
148        } // end method exitButton_Click
149     } // end class CreateFileForm
150  } // end namespace CreateFile
Creating and writing to a sequential-access file.

a) BankUI graphical user interface with three additional controls

Creating and writing to a sequential-access file.

b) Save File dialog

Creating and writing to a sequential-access file.

c) Account 100, “Nancy Brown”, saved with a balance of -25.54

Class Record contains auto-implemented properties for instance variables Account, FirstName, LastName and Balance (lines 9–18), which collectively represent all the information for a record. The parameterless constructor (lines 21–24) sets these members by calling the four-argument constructor with 0 for the account number, string.Empty for the first and last name and 0.0M for the balance. The four-argument constructor (lines 27–34) sets these members to the specified parameter values.

Using a Character Stream to Create an Output File

Class CreateFileForm (Fig. 17.9) uses instances of class Record to create a sequential-access file that might be used in an accounts-receivable system—i.e., a program that organizes data regarding money owed by a company’s credit clients. For each client, the program obtains an account number and the client’s first name, last name and balance (i.e., the amount of money that the client owes to the company for previously received goods and services). The data obtained for each client constitutes a record for that client. In this application, the account number is used as the record key—files are created and maintained in account-number order. This program assumes that the user enters records in account-number order. However, a comprehensive accounts-receivable system would provide a sorting capability, so the user could enter the records in any order.

Class CreateFileForm either creates or opens a file (depending on whether one exists), then allows the user to write records to it. The using directive in line 6 enables us to use the classes of the BankLibrary namespace; this namespace contains class BankUIForm, from which class CreateFileForm inherits (line 10). Class CreateFileForm’s GUI enhances that of class BankUIForm with buttons Save As, Enter and Exit.

Method saveButton_Click

When the user clicks the Save As button, the program invokes the event handler saveButton_Click (lines 21–66). Line 27 instantiates an object of class SaveFileDialog (namespace System.Windows.Forms). By placing this object in a using statement (lines 27–32), we can ensure that the dialog’s Dispose method is called to release its resources as soon as the program has retrieved user input from it. SaveFileDialog objects are used for selecting files (see the second screen in Fig. 17.9). Line 29 indicates that the dialog should not check if the file name specified by the user already exists. Line 30 calls SaveFileDialog method ShowDialog to display the dialog. When displayed, a SaveFileDialog prevents the user from interacting with any other window in the program until the user closes the SaveFileDialog by clicking either Save or Cancel. Dialogs that behave in this manner are called modal dialogs. The user selects the appropriate drive, directory and file name, then clicks Save. Method ShowDialog returns a DialogResult specifying which button (Save or Cancel) the user clicked to close the dialog. This is assigned to DialogResult variable result (line 30). Line 31 gets the file name from the dialog. Line 35 tests whether the user clicked OK by comparing this value to DialogResult.OK. If the values are equal, method saveButton_Click continues.

You can open files to perform text manipulation by creating objects of class FileStream. In this example, we want the file to be opened for output, so lines 47–48 create a FileStream object. The FileStream constructor that we use receives three arguments—a string containing the path and name of the file to open, a constant describing how to open the file and a constant describing the file permissions. The constant FileMode.OpenOrCreate (line 48) indicates that the FileStream object should open the file if it exists or create the file if it does not exist. Note that the contents of an existing file are overwritten by the StreamWriter. To preserve the original contents of a file, use FileMode.Append. There are other FileMode constants describing how to open files; we introduce these constants as we use them in examples. The constant FileAccess.Write indicates that the program can perform only write operations with the FileStream object. There are two other constants for the third constructor parameter—FileAccess.Read for read-only access and FileAccess.ReadWrite for both read and write access. Line 58 catches an IOException if there’s a problem opening the file or creating the StreamWriter. If so, the program displays an error message (lines 61–62). If no exception occurs, the file is open for writing.

Good Programming Practice 17.1

Good Programming Practice 17.1

When opening files, use the FileAccess enumeration to control user access to these files.

Common Programming Error 17.1

Common Programming Error 17.1

Failure to open a file before attempting to use it in a program is a logic error.

Method enterButton_Click

After typing information into each TextBox, the user clicks the Enter button, which calls event handler enterButton_Click (lines 69–126) to save the data from the TextBoxes into the user-specified file. If the user entered a valid account number (i.e., an integer greater than zero), lines 91–97 store the TextBox values in an object of type Record (created at line 75). If the user entered invalid data in one of the TextBoxes (such as nonnumeric characters in the Balance field), the program throws a FormatException. The catch block in lines 118–122 handles such exceptions by notifying the user (via a MessageBox) of the improper format.

If the user entered valid data, lines 100–102 write the record to the file by invoking method WriteLine of the StreamWriter object that was created at line 51. Method WriteLine writes a sequence of characters to a file. The StreamWriter object is constructed with a FileStream argument that specifies the file to which the StreamWriter will output text. Class StreamWriter belongs to the System.IO namespace.

Method exitButton_Click

When the user clicks Exit, exitButton_Click (lines 129–148) executes. Line 137 closes the StreamWriter, which automatically closes the FileStream. Then, line 147 terminates the program. Note that method Close is called in a try block. Method Close throws an IOException if the file or stream cannot be closed properly. In this case, it’s important to notify the user that the information in the file or stream might be corrupted.

Performance Tip 17.1

Performance Tip 17.1

Close each file explicitly when the program no longer needs to use it. This can reduce resource usage in programs that continue executing long after they finish using a specific file. The practice of explicitly closing files also improves program clarity.

Performance Tip 17.2

Performance Tip 17.2

Releasing resources explicitly when they’re no longer needed makes them immediately available for reuse by other programs, thus improving resource utilization.

Sample Data

To test the program, we entered information for the accounts shown in Fig. 17.10. The program does not depict how the data records are stored in the file. To verify that the file has been created successfully, we create a program in the next section to read and display the file. Since this is a text file, you can actually open it in any text editor to see its contents.

Table 17.10. Sample data for the program of Fig. 17.9.

Account number

First name

Last name

Balance

100

Nancy

Brown

-25.54

200

Stacey

Dunn

314.33

300

Doug

Barker

0.00

400

Dave

Smith

258.34

500

Sam

Stone

34.98

Reading Data from a Sequential-Access Text File

The previous section demonstrated how to create a file for use in sequential-access applications. In this section, we discuss how to read (or retrieve) data sequentially from a file.

Class ReadSequentialAccessFileForm (Fig. 17.11) reads records from the file created by the program in Fig. 17.9, then displays the contents of each record. Much of the code in this example is similar to that of Fig. 17.9, so we discuss only the unique aspects of the application.

Example 17.11. Reading sequential-access files.

 1   // Fig. 17.11: ReadSequentialAccessFileForm.cs
 2   // Reading a sequential-access file.
 3   using System;
 4   using System.Windows.Forms;
 5   using System.IO;
 6   using BankLibrary;
 7
 8   namespace ReadSequentialAccessFile
 9   {
10      public partial class ReadSequentialAccessFileForm : BankUIForm
11      {
12         private StreamReader fileReader; // reads data from a text file
13
14         // parameterless constructor
15         public ReadSequentialAccessFileForm()
16         {
17            InitializeComponent();
18         } // end constructor
19
20         // invoked when user clicks the Open button
21         private void openButton_Click( object sender, EventArgs e )
22         {
23            // create and show dialog box enabling user to open file
24            DialogResult result; // result of OpenFileDialog
25            string fileName; // name of file containing data
26
27            using ( OpenFileDialog fileChooser = new OpenFileDialog() )
28            {
29               result = fileChooser.ShowDialog();                     
30               fileName = fileChooser.FileName; // get specified name 
31            } // end using
32
33            // ensure that user clicked "OK"
34            if ( result == DialogResult.OK )
35            {
36               ClearTextBoxes();
37
38               // show error if user specified invalid file
39               if ( fileName == string.Empty )
40                  MessageBox.Show( "Invalid File Name", "Error",
41                     MessageBoxButtons.OK, MessageBoxIcon.Error );
42               else
43               {
44                  try
45                  {
46                     // create FileStream to obtain read access to file
47                     FileStream input = new FileStream(            
48                        fileName, FileMode.Open, FileAccess.Read );
49
50                     // set file from where data is read
51                     fileReader = new StreamReader( input );
52
53                     openButton.Enabled = false; // disable Open File button
54                     nextButton.Enabled = true; // enable Next Record button
55                  } // end try
56                  catch ( IOException )
57                  {
58                     MessageBox.Show( "Error reading from file",
59                        "File Error", MessageBoxButtons.OK,
60                        MessageBoxIcon.Error );
61                  } // end catch
62               } // end else
63            } // end if
64         } // end method openButton_Click
65
66         // invoked when user clicks Next button
67         private void nextButton_Click( object sender, EventArgs e )
68         {
69            try
70            {
71               // get next record available in file
72               string inputRecord = fileReader.ReadLine();
73               string[] inputFields; // will store individual pieces of data
74
75               if ( inputRecord != null )
76               {
77                  inputFields = inputRecord.Split( ',' );
78
79                  Record record = new Record(
80                     Convert.ToInt32( inputFields[ 0 ] ), inputFields[ 1 ],
81                     inputFields[ 2 ],
82                     Convert.ToDecimal( inputFields[ 3 ] ) );
83
84                  // copy string-array values to TextBox values
85                  SetTextBoxValues( inputFields );
86               } // end if
87               else
88               {
89                  // close StreamReader and underlying file
90                  fileReader.Close();
91                  openButton.Enabled = true; // enable Open File button
92                  nextButton.Enabled = false; // disable Next Record button
93                  ClearTextBoxes();
94
95                  // notify user if no records in file
96                  MessageBox.Show( "No more records in file", string.Empty,
97                     MessageBoxButtons.OK, MessageBoxIcon.Information );
98               } // end else
99            } // end try
100           catch ( IOException )
101           {
102              MessageBox.Show( "Error Reading from File", "Error",
103                 MessageBoxButtons.OK, MessageBoxIcon.Error );
104           } // end catch
105        } // end method nextButton_Click
106     } // end class ReadSequentialAccessFileForm
107  } // end namespace ReadSequentialAccessFile
Reading sequential-access files.

a) BankUI graphical user interface with an Open File button

Reading sequential-access files.

b) OpenFileDialog window

Reading sequential-access files.

c) Reading account 100

Reading sequential-access files.

d) User is shown a messagebox when all records have been read

Method openButton_Click

When the user clicks the Open File button, the program calls event handler open-Button_Click (lines 21–64). Line 27 creates an OpenFileDialog, and line 29 calls its ShowDialog method to display the Open dialog (see the second screenshot in Fig. 17.11). The behavior and GUI for the Save and Open dialog types are identical, except that Save is replaced by Open. If the user selects a valid file name, lines 47–48 create a FileStream object and assign it to reference input. We pass constant FileMode.Open as the second argument to the FileStream constructor to indicate that the FileStream should open the file if it exists or throw a FileNotFoundException if it does not. (In this example, the FileStream constructor will not throw a FileNotFoundException, because the OpenFileDialog is configured to check that the file exists.) In the last example (Fig. 17.9), we wrote text to the file using a FileStream object with write-only access. In this example (Fig. 17.11), we specify read-only access to the file by passing constant FileAccess.Read as the third argument to the FileStream constructor. This FileStream object is used to create a StreamReader object in line 51. The FileStream object specifies the file from which the StreamReader object will read text.

Error-Prevention Tip 17.1

Error-Prevention Tip 17.1

Open a file with the FileAccess.Read file-open mode if its contents should not be modified. This prevents unintentional modification of the contents.

Method nextButton_Click

When the user clicks the Next Record button, the program calls event handler nextButton_Click (lines 67–104), which reads the next record from the user-specified file. (The user must click Next Record after opening the file to view the first record.) Line 72 calls StreamReader method ReadLine to read the next record. If an error occurs while reading the file, an IOException is thrown (caught at line 99), and the user is notified (lines 101–102). Otherwise, line 75 determines whether StreamReader method ReadLine returned null (i.e., there’s no more text in the file). If not, line 77 uses method Split of class string to separate the stream of characters that was read from the file into strings that represent the Record’s properties. These properties are then stored by constructing a Record object using the properties as arguments (lines 79–81). Line 84 displays the Record values in the TextBoxes. If ReadLine returns null, the program closes the StreamReader object (line 90), automatically closing the FileStream object, then notifies the user that there are no more records (lines 96–97).

Case Study: Credit Inquiry Program

To retrieve data sequentially from a file, programs normally start from the beginning of the file, reading consecutively until the desired data is found. It sometimes is necessary to process a file sequentially several times (from the beginning of the file) during the execution of a program. A FileStream object can reposition its file-position pointer (which contains the byte number of the next byte to be read from or written to the file) to any position in the file. When a FileStream object is opened, its file-position pointer is set to byte position 0 (i.e., the beginning of the file)

We now present a program that builds on the concepts employed in Fig. 17.11. Class CreditInquiryForm (Fig. 17.12) is a credit-inquiry program that enables a credit manager to search for and display account information for those customers with credit balances (i.e., customers to whom the company owes money), zero balances (i.e., customers who do not owe the company money) and debit balances (i.e., customers who owe the company money for previously received goods and services). We use a RichTextBox in the program to display the account information. RichTextBoxes provide more functionality than regular TextBoxes—for example, RichTextBoxes offer method Find for searching individual strings and method LoadFile for displaying file contents. Classes RichTextBox and TextBox both inherit from abstract class System.Windows.Forms.TextBoxBase. In this example, we chose a RichTextBox, because it displays multiple lines of text by default, whereas a regular TextBox displays only one. Alternatively, we could have specified that a TextBox object display multiple lines of text by setting its Multiline property to true.

Example 17.12. Credit-inquiry program.

 1   // Fig. 17.12: CreditInquiryForm.cs
 2   // Read a file sequentially and display contents based on
 3   // account type specified by user ( credit, debit or zero balances ).
 4   using System;
 5   using System.Windows.Forms;
 6   using System.IO;
 7   using BankLibrary;
 8
 9   namespace CreditInquiry
10   {
11      public partial class CreditInquiryForm : Form
12      {
13         private FileStream input; // maintains the connection to the file
14         private StreamReader fileReader; // reads data from text file    
15
16         // name of file that stores credit, debit and zero balances
17         private string fileName;
18
19         // parameterless constructor
20         public CreditInquiryForm()
21         {
22            InitializeComponent();
23         } // end constructor
24
25         // invoked when user clicks Open File button
26         private void openButton_Click( object sender, EventArgs e )
27         {
28            // create dialog box enabling user to open file
29            DialogResult result;
30
31            using ( OpenFileDialog fileChooser = new OpenFileDialog() )
32            {
33               result = fileChooser.ShowDialog();
34               fileName = fileChooser.FileName;  
35            } // end using
36
37            // exit event handler if user clicked Cancel
38            if ( result == DialogResult.OK )
39            {
40               // show error if user specified invalid file
41               if ( fileName == string.Empty )
42                  MessageBox.Show( "Invalid File Name", "Error",
43                     MessageBoxButtons.OK, MessageBoxIcon.Error );
44               else
45               {
46                  // create FileStream to obtain read access to file
47                  input = new FileStream( fileName,   
48                     FileMode.Open, FileAccess.Read );
49
50                  // set file from where data is read
51                  fileReader = new StreamReader( input );
52
53                  // enable all GUI buttons, except for Open File button
54                  openButton.Enabled = false;
55                  creditButton.Enabled = true;
56                  debitButton.Enabled = true;
57                  zeroButton.Enabled = true;
58               } // end else
59            } // end if
60         } // end method openButton_Click
61
62         // invoked when user clicks credit balances,
63         // debit balances or zero balances button
64         private void getBalances_Click( object sender, System.EventArgs e )
65         {
66            // convert sender explicitly to object of type button
67            Button senderButton = ( Button ) sender;
68
69            // get text from clicked Button, which stores account type
70            string accountType = senderButton.Text;
71
72            // read and display file information
73            try
74            {
75               // go back to the beginning of the file
76               input.Seek( 0, SeekOrigin.Begin );
77
78               displayTextBox.Text = "The accounts are:
";
79
80               // traverse file until end of file
81               while ( true )
82               {
83                  string[] inputFields; // stores individual pieces of data
84                  Record record; // store each Record as file is read
85                  decimal balance; // store each Record's balance
86
87                  // get next Record available in file
88                  string inputRecord = fileReader.ReadLine();
89
90                  // when at the end of file, exit method
91                  if ( inputRecord == null )
92                     return;
93
94                  inputFields = inputRecord.Split( ',' ); // parse input
95
96                  // create Record from input
97                  record = new Record(
98                     Convert.ToInt32( inputFields[ 0 ] ), inputFields[ 1 ],
99                     inputFields[ 2 ], Convert.ToDecimal(inputFields[ 3 ]));
100
101                 // store record's last field in balance
102                 balance = record.Balance;
103
104                 // determine whether to display balance
105                 if ( ShouldDisplay( balance, accountType ) )
106                 {
107                    // display record
108                    string output = record.Account + "	" +
109                       record.FirstName + "	" + record.LastName + "	";
110
111                    // display balance with correct monetary format
112                    output += String.Format( "{0:F}", balance ) + "
";
113
114                    // copy output to screen
115                    displayTextBox.AppendText( output );
116                 } // end if
117              } // end while
118           } // end try
119           // handle exception when file cannot be read
120           catch ( IOException )
121           {
122              MessageBox.Show( "Cannot Read File", "Error",
123                 MessageBoxButtons.OK, MessageBoxIcon.Error );
124           } // end catch
125        } // end method getBalances_Click
126
127        // determine whether to display given record
128        private bool ShouldDisplay( decimal balance, string accountType )
129        {
130           if ( balance > 0M )
131           {
132              // display credit balances
133              if ( accountType == "Credit Balances" )
134                 return true;
135           } // end if
136           else if ( balance < 0M )
137           {
138              // display debit balances
139              if ( accountType == "Debit Balances" )
140                 return true;
141           } // end else if
142           else // balance == 0
143           {
144              // display zero balances
145              if ( accountType == "Zero Balances" )
146                 return true;
147           } // end else
148
149           return false;
150        } // end method ShouldDisplay
151
152        // invoked when user clicks Done button
153        private void doneButton_Click( object sender, EventArgs e )
154        {
155           if ( input != null )
156           {
157              // close file and StreamReader
158              try
159              {
160                 // close StreamReader and underlying file
161                 fileReader.Close();
162              } // end try
163              // handle exception if FileStream does not exist
164              catch ( IOException )
165              {
166                 // notify user of error closing file
167                 MessageBox.Show( "Cannot close file", "Error",
168                    MessageBoxButtons.OK, MessageBoxIcon.Error );
169              } // end catch
170           } // end if
171
172           Application.Exit();
173        } // end method doneButton_Click
174     } // end class CreditInquiryForm
175  } // end namespace CreditInquiry
Credit-inquiry program.

a)

Credit-inquiry program.

b)

Credit-inquiry program.

c)

Credit-inquiry program.

d)

Credit-inquiry program.

e)

The program displays buttons that enable a credit manager to obtain credit information. The Open File button opens a file for gathering data. The Credit Balances button displays a list of accounts that have credit balances, the Debit Balances button displays a list of accounts that have debit balances and the Zero Balances button displays a list of accounts that have zero balances. The Done button exits the application.

When the user clicks the Open File button, the program calls the event handler openButton_Click (lines 26–60). Line 31 creates an OpenFileDialog, and line 33 calls its ShowDialog method to display the Open dialog, in which the user selects the file to open. Lines 47–48 create a FileStream object with read-only file access and assign it to reference input. Line 51 creates a StreamReader object that we use to read text from the FileStream.

When the user clicks Credit Balances, Debit Balances or Zero Balances, the program invokes method getBalances_Click (lines 64–125). Line 67 casts the sender parameter, which is an object reference to the control that generated the event, to a Button object. Line 70 extracts the Button object’s text, which the program uses to determine which type of accounts to display. Line 76 uses FileStream method Seek to reset the file-position pointer back to the beginning of the file. FileStream method Seek allows you to reset the file-position pointer by specifying the number of bytes it should be offset from the file’s beginning, end or current position. The part of the file you want to be offset from is chosen using constants from the SeekOrigin enumeration. In this case, our stream is offset by 0 bytes from the file’s beginning (SeekOrigin.Begin). Lines 81–117 define a while loop that uses private method ShouldDisplay (lines 128–150) to determine whether to display each record in the file. The while loop obtains each record by repeatedly calling StreamReader method ReadLine (line 88) and splitting the text into tokens (line 94) that are used to initialize object record (lines 97–99). Line 91 determines whether the file-position pointer has reached the end of the file, in which case ReadLine returns null. If so, the program returns from method getBalances_Click (line 92).

Serialization

Section 17.5 demonstrated how to write the individual fields of a Record object to a text file, and Section 17.6 demonstrated how to read those fields from a file and place their values in a Record object in memory. In the examples, Record was used to aggregate the information for one record. When the instance variables for a Record were output to a disk file, certain information was lost, such as the type of each value. For instance, if the value "3" is read from a file, there’s no way to tell if the value came from an int, a string or a decimal. We have only data, not type information, on disk. If the program that’s going to read this data “knows” what object type the data corresponds to, then the data can be read directly into objects of that type. For example, in Fig. 17.11, we know that we are inputting an int (the account number), followed by two strings (the first and last name) and a decimal (the balance). We also know that these values are separated by commas, with only one record on each line. So, we are able to parse the strings and convert the account number to an int and the balance to a decimal. Sometimes it would be easier to read or write entire objects. C# provides such a mechanism, called object serialization. A serialized object is an object represented as a sequence of bytes that includes the object’s data, as well as information about the object’s type and the types of data stored in the object. After a serialized object has been written to a file, it can be read from the file and deserialized—that is, the type information and bytes that represent the object and its data can be used to recreate the object in memory.

Class BinaryFormatter (namespace System.Runtime.Serialization.Formatters.Binary) enables entire objects to be written to or read from a stream. BinaryFormatter method Serialize writes an object’s representation to a file. BinaryFormatter method Deserialize reads this representation from a file and reconstructs the original object. Both methods throw a SerializationException if an error occurs during serialization or deserialization. Both methods require a Stream object (e.g., the FileStream) as a parameter so that the BinaryFormatter can access the correct stream.

In Sections 17.917.10, we create and manipulate sequential-access files using object serialization. Object serialization is performed with byte-based streams, so the sequential files created and manipulated will be binary files. Binary files are not human readable. For this reason, we write a separate application that reads and displays serialized objects.

Creating a Sequential-Access File Using Object Serialization

We begin by creating and writing serialized objects to a sequential-access file. In this section, we reuse much of the code from Section 17.5, so we focus only on the new features.

Defining the RecordSerializable Class

Let’s begin by modifying our Record class (Fig. 17.8) so that objects of this class can be serialized. Class RecordSerializable (Fig. 17.13) is marked with the [Serializable] attribute (line 7), which indicates to the CLR that objects of class RecordSerializable can be serialized. The classes for objects that we wish to write to or read from a stream must include this attribute in their declarations or must implement interface ISerializable.

Example 17.13. RecordSerializable class for serializable objects.

 1   // Fig. 17.13: RecordSerializable.cs
 2   // Serializable class that represents a data record.
 3   using System;
 4
 5   namespace BankLibrary
 6   {
 7      [Serializable]
 8      public class RecordSerializable
 9      {
10         // automatic Account property
11         public int Account { get; set; }
12
13         // automatic FirstName property
14         public string FirstName { get; set; }
15
16         // automatic LastName property
17         public string LastName { get; set; }
18
19         // automatic Balance property
20         public decimal Balance { get; set; }
21
22         // default constructor sets members to default values
23         public RecordSerializable()
24            : this( 0, string.Empty, string.Empty, 0M )
25         {
26         } // end constructor
27
28         // overloaded constructor sets members to parameter values
29         public RecordSerializable( int accountValue, string firstNameValue,
30            string lastNameValue, decimal balanceValue )
31         {
32            Account = accountValue;
33            FirstName = firstNameValue;
34            LastName = lastNameValue;
35            Balance = balanceValue;
36         } // end constructor
37      } // end class RecordSerializable
38   } // end namespace BankLibrary

In a class that’s marked with the [Serializable] attribute or that implements interface ISerializable, you must ensure that every instance variable of the class is also serializable. All simple-type variables and strings are serializable. For variables of reference types, you must check the class declaration (and possibly its base classes) to ensure that the type is serializable. By default, array objects are serializable. However, if the array contains references to other objects, those objects may or may not be serializable.

Using a Serialization Stream to Create an Output File

Next, we’ll create a sequential-access file with serialization (Fig. 17.14). To test this program, we used the sample data from Fig. 17.10 to create a file named clients.ser. Since the sample screen captures are the same as Fig. 17.9, they are not shown here. Line 15 creates a BinaryFormatter for writing serialized objects. Lines 53–54 open the FileStream to which this program writes the serialized objects. The string argument that’s passed to the FileStream’s constructor represents the name and path of the file to be opened. This specifies the file to which the serialized objects will be written.

Example 17.14. Sequential file created using serialization.

 1   // Fig. 17.14: CreateFileForm.cs
 2   // Creating a sequential-access file using serialization.
 3   using System;
 4   using System.Windows.Forms;
 5   using System.IO;
 6   using System.Runtime.Serialization.Formatters.Binary;
 7   using System.Runtime.Serialization;                  
 8   using BankLibrary;
 9
10   namespace CreateFile
11   {
12      public partial class CreateFileForm : BankUIForm
13      {
14         // object for serializing RecordSerializables in binary format
15         private BinaryFormatter formatter = new BinaryFormatter();
16         private FileStream output; // stream for writing to a file
17
18         // parameterless constructor
19         public CreateFileForm()
20         {
21            InitializeComponent();
22         } // end constructor
23
24         // handler for saveButton_Click
25         private void saveButton_Click( object sender, EventArgs e )
26         {
27            // create and show dialog box enabling user to save file
28            DialogResult result;
29            string fileName; // name of file to save data
30
31            using ( SaveFileDialog fileChooser = new SaveFileDialog() )
32            {
33               fileChooser.CheckFileExists = false; // let user create file
34
35               // retrieve the result of the dialog box
36               result = fileChooser.ShowDialog();
37               fileName = fileChooser.FileName; // get specified file name
38            } // end using
39
40            // ensure that user clicked "OK"
41            if ( result == DialogResult.OK )
42            {
43               // show error if user specified invalid file
44               if ( fileName == string.Empty )
45                  MessageBox.Show( "Invalid File Name", "Error",
46                     MessageBoxButtons.OK, MessageBoxIcon.Error );
47               else
48               {
49                  // save file via FileStream if user specified valid file
50                  try
51                  {
52                     // open file with write access
53                     output = new FileStream( fileName,
54                        FileMode.OpenOrCreate, FileAccess.Write );
55
56                     // disable Save button and enable Enter button
57                     saveButton.Enabled = false;
58                     enterButton.Enabled = true;
59                  } // end try
60                  // handle exception if there is a problem opening the file
61                  catch ( IOException )
62                  {
63                     // notify user if file could not be opened
64                     MessageBox.Show( "Error opening file", "Error",
65                        MessageBoxButtons.OK, MessageBoxIcon.Error );
66                  } // end catch
67               } // end else
68            } // end if
69         } // end method saveButton_Click
70
71         // handler for enterButton Click
72         private void enterButton_Click( object sender, EventArgs e )
73         {
74            // store TextBox values string array
75            string[] values = GetTextBoxValues();
76
77            // RecordSerializable containing TextBox values to serialize
78            RecordSerializable record = new RecordSerializable();
79
80            // determine whether TextBox account field is empty
81            if ( values[ ( int ) TextBoxIndices.ACCOUNT ] != string.Empty )
82            {
83               // store TextBox values in RecordSerializable and serialize it
84               try
85               {
86                  // get account-number value from TextBox
87                  int accountNumber = Int32.Parse(
88                     values[ ( int ) TextBoxIndices.ACCOUNT ] );
89
90                  // determine whether accountNumber is valid
91                  if ( accountNumber > 0 )
92                  {
93                     // store TextBox fields in RecordSerializable
94                     record.Account = accountNumber;
95                     record.FirstName = values[ ( int )
96                        TextBoxIndices.FIRST ];
97                     record.LastName = values[ ( int )
98                        TextBoxIndices.LAST ];
99                     record.Balance = Decimal.Parse( values[
100                       ( int ) TextBoxIndices.BALANCE ] );
101
102                    // write RecordSerializable to FileStream
103                    formatter.Serialize( output, record );
104                 } // end if
105                 else
106                 {
107                    // notify user if invalid account number
108                    MessageBox.Show( "Invalid Account Number", "Error",
109                       MessageBoxButtons.OK, MessageBoxIcon.Error );
110                 } // end else
111              } // end try
112              // notify user if error occurs in serialization
113              catch ( SerializationException )
114              {
115                 MessageBox.Show( "Error Writing to File", "Error",
116                    MessageBoxButtons.OK, MessageBoxIcon.Error );
117              } // end catch
118              // notify user if error occurs regarding parameter format
119              catch ( FormatException )
120              {
121                 MessageBox.Show( "Invalid Format", "Error",
122                    MessageBoxButtons.OK, MessageBoxIcon.Error );
123              } // end catch
124           } // end if
125
126           ClearTextBoxes(); // clear TextBox values
127        } // end method enterButton_Click
128
129        // handler for exitButton Click
130        private void exitButton_Click( object sender, EventArgs e )
131        {
132           // determine whether file exists
133           if ( output != null )
134           {
135              // close file
136              try
137              {
138                 output.Close(); // close FileStream
139              } // end try
140              // notify user of error closing file
141              catch ( IOException )
142              {
143                 MessageBox.Show( "Cannot close file", "Error",
144                    MessageBoxButtons.OK, MessageBoxIcon.Error );
145              } // end catch
146           } // end if
147
148           Application.Exit();
149        } // end method exitButton_Click
150     } // end class CreateFileForm
151  } // end namespace CreateFile

This program assumes that data is input correctly and in the proper record-number order. Event handler enterButton_Click (lines 72–127) performs the write operation. Line 78 creates a RecordSerializable object, which is assigned values in lines 94–100. Line 103 calls method Serialize to write the RecordSerializable object to the output file. Method Serialize takes the FileStream object as the first argument so that the BinaryFormatter can write its second argument to the correct file. Only one statement is required to write the entire object. If a problem occurs during serialization, a SerializationException occurs—we catch this exception in lines 113–117.

In the sample execution for the program in Fig. 17.14, we entered information for five accounts—the same information shown in Fig. 17.10. The program does not show how the data records actually appear in the file. Remember that we are now using binary files, which are not human readable. To verify that the file was created successfully, the next section presents a program to read the file’s contents.

Reading and Deserializing Data from a Binary File

The preceding section showed how to create a sequential-access file using object serialization. In this section, we discuss how to read serialized objects sequentially from a file.

Figure 17.15 reads and displays the contents of the clients.ser file created by the program in Fig. 17.14. The sample screen captures are identical to those of Fig. 17.11, so they are not shown here. Line 15 creates the BinaryFormatter that will be used to read objects. The program opens the file for input by creating a FileStream object (lines 49–50). The name of the file to open is specified as the first argument to the FileStream constructor.

Example 17.15. Sequential file read using deserialization.

 1   // Fig. 17.15: ReadSequentialAccessFileForm.cs
 2   // Reading a sequential-access file using deserialization.
 3   using System;
 4   using System.Windows.Forms;
 5   using System.IO;
 6   using System.Runtime.Serialization.Formatters.Binary;
 7   using System.Runtime.Serialization;
 8   using BankLibrary;
 9
10   namespace ReadSequentialAccessFile
11   {
12      public partial class ReadSequentialAccessFileForm : BankUIForm
13      {
14         // object for deserializing RecordSerializable in binary format
15         private BinaryFormatter reader = new BinaryFormatter();
16         private FileStream input; // stream for reading from a file
17
18         // parameterless constructor
19         public ReadSequentialAccessFileForm()
20         {
21            InitializeComponent();
22         } // end constructor
23
24         // invoked when user clicks the Open button
25         private void openButton_Click( object sender, EventArgs e )
26         {
27            // create and show dialog box enabling user to open file
28            DialogResult result; // result of OpenFileDialog
29            string fileName; // name of file containing data
30
31            using ( OpenFileDialog fileChooser = new OpenFileDialog() )
32            {
33               result = fileChooser.ShowDialog();
34               fileName = fileChooser.FileName; // get specified name
35            } // end using
36
37            // ensure that user clicked "OK"
38            if ( result == DialogResult.OK )
39            {
40               ClearTextBoxes();
41
42               // show error if user specified invalid file
43               if ( fileName == string.Empty )
44                  MessageBox.Show( "Invalid File Name", "Error",
45                     MessageBoxButtons.OK, MessageBoxIcon.Error );
46               else
47               {
48                  // create FileStream to obtain read access to file
49                  input = new FileStream(
50                     fileName, FileMode.Open, FileAccess.Read );
51
52                  openButton.Enabled = false; // disable Open File button
53                  nextButton.Enabled = true;  // enable Next Record button
54               } // end else
55            } // end if
56         } // end method openButton_Click
57
58         // invoked when user clicks Next button
59         private void nextButton_Click( object sender, EventArgs e )
60         {
61            // deserialize RecordSerializable and store data in TextBoxes
62            try
63            {
64               // get next RecordSerializable available in file
65               RecordSerializable record =                           
66                  ( RecordSerializable ) reader.Deserialize( input );
67
68               // store RecordSerializable values in temporary string array
69               string[] values = new string[] {
70                  record.Account.ToString(),
71                  record.FirstName.ToString(),
72                  record.LastName.ToString(),
73                  record.Balance.ToString()
74               };
75
76               // copy string-array values to TextBox values
77               SetTextBoxValues( values );
78            } // end try
79            // handle exception when there are no RecordSerializables in file
80            catch ( SerializationException )
81            {
82               input.Close(); // close FileStream
83               openButton.Enabled = true; // enable Open File button
84               nextButton.Enabled = false; // disable Next Record button
85
86               ClearTextBoxes();
87
88               // notify user if no RecordSerializables in file
89               MessageBox.Show( "No more records in file", string.Empty,
90                  MessageBoxButtons.OK, MessageBoxIcon.Information );
91            } // end catch
92         } // end method nextButton_Click
93      } // end class ReadSequentialAccessFileForm
94   } // end namespace ReadSequentialAccessFile

The program reads objects from a file in event handler nextButton_Click (lines 59–92). We use method Deserialize (of the BinaryFormatter created in line 15) to read the data (lines 65–66). Note that we cast the result of Deserialize to type RecordSerializable (line 66)—this cast is necessary, because Deserialize returns a reference of type object and we need to access properties that belong to class RecordSerializable. If an error occurs during deserialization, a SerializationException is thrown, and the FileStream object is closed (line 82).

Wrap-Up

In this chapter, you learned how to use file processing to manipulate persistent data. You learned that data is stored in computers as 0s and 1s, and that combinations of these values are used to form bytes, fields, records and eventually files. We overviewed several file-processing classes from the System.IO namespace. You used class File to manipulate files, and classes Directory and DirectoryInfo to manipulate directories. Next, you learned how to use sequential-access file processing to manipulate records in text files. We then discussed the differences between text-file processing and object serialization, and used serialization to store entire objects in and retrieve entire objects from files.

In Chapter 18, we begin our discussion of databases, which organize data in such a way that the data can be selected and updated quickly. We introduce Structured Query Language (SQL) for writing simple database queries. We then introduce LINQ to SQL, which allows you to write LINQ queries that are automatically converted into SQL queries. These SQL queries are then used to query the database.

Summary

Section 17.1 Introduction

  • Files are used for long-term retention of large amounts of data.

  • Data stored in files often is called persistent data.

  • Computers store files on secondary storage devices.

Section 17.2 Data Hierarchy

  • All data items that computers process are reduced to combinations of 0s and 1s.

  • The smallest data item that computers support is called a bit and can assume the value 0 or 1.

  • Digits, letters and special symbols are referred to as characters. The set of all characters used to write programs and represent data items on a particular computer is that computer’s character set.

  • Bytes are composed of eight bits

  • C# uses the Unicode character set which uses two bytes to represent each character.

  • A field is a group of characters that conveys meaning. Typically, a record is composed of several related fields.

  • A file is a group of related records.

  • At least one field in each record is chosen as a record key, which identifies a record as belonging to a particular person or entity and distinguishes that record from all others.

  • The most common type of file organization is a sequential file, in which records typically are stored in order by record-key field.

Section 17.3 Files and Streams

  • C# views each file as a sequential stream of bytes.

  • Files are opened by creating an object that has a stream associated with it.

  • Streams provide communication channels between files and programs.

  • To perform file processing in C#, the System.IO namespace must be imported.

  • Class Stream provides functionality for representing streams as bytes. This class is abstract, so objects of this class cannot be instantiated.

  • Classes FileStream, MemoryStream and BufferedStream inherit from class Stream.

  • Class FileStream can be used to read data to and write data from sequential-access files.

  • Class MemoryStream enables the transfer of data directly to and from memory—this is much faster than other types of data transfer (e.g., to and from disk).

  • Class BufferedStream uses buffering to transfer data to or from a stream. Buffering enhances I/O performance by directing each output operation to a buffer that’s large enough to hold the data from many outputs. Then the actual transfer to the output device is performed in one large physical output operation each time the buffer fills. Buffering can also be used to speed input operations.

Section 17.4 Classes File and Directory

  • Information on computers is stored in files, which are organized in directories. Classes File and Directory enable programs to manipulate files and directories on disk.

  • Class File provides static methods for determining information about files and can be used to open files for reading or writing.

  • Class Directory provides static methods for manipulating directories.

  • The DirectoryInfo object returned by Directory method CreateDirectory contains information about a directory. Much of the information contained in class DirectoryInfo also can be accessed via the methods of class Directory.

  • File method Exists determines whether a string is the name and path of an existing file.

  • A StreamReader reads text from a file. Its constructor takes a string containing the name of the file to open and its path. StreamReader method ReadToEnd reads the entire contents of a file.

  • Directory method Exists determines whether a string is the name of an existing directory.

  • Directory method GetDirectories obtains a string array containing the names of subdirectories in the specified directory.

  • Directory method GetFiles returns a string array containing file names in the specified directory.

  • Path method GetExtension obtains the extension for the specified file name.

  • A Dictionary (namespace System.Collections.Generic) is a collection of key/value pairs, in which each key has a corresponding value. Class Dictionary is a generic class like class List.

  • Dictionary method ContainsKey determines whether the specified key exists in the Dictionary.

  • Dictionary method Add inserts a key/value pair into a Dictionary.

  • File method Delete removes the specified file from disk.

  • Dictionary property Keys returns all the keys in a Dictionary.

  • Dictionary method Clear deletes the contents of a Dictionary.

Section 17.5 Creating a Sequential-Access Text File

  • C# imposes no structure on files. You must structure files to meet your application’s requirements.

  • A SaveFileDialog is a modal dialog.

  • A StreamWriter’s constructor receives a FileStream that specifies the file in which to write text.

Section 17.6 Reading Data from a Sequential-Access Text File

  • Data is stored in files so that it can be retrieved for processing when it’s needed.

  • To retrieve data sequentially from a file, programs normally start from the beginning of the file, reading consecutively until the desired data is found. It sometimes is necessary to process a file sequentially several times during the execution of a program.

  • An OpenFileDialog allows a user to select files to open. Method ShowDialog displays the dialog.

Section 17.7 Case Study: Credit Inquiry Program

  • Stream method Seek moves the file-position pointer in a file. You specify the number of bytes it should be offset from the file’s beginning, end or current position. The part of the file you want to be offset from is chosen using constants from the SeekOrigin enumeration.

Section 17.8 Serialization

  • A serialized object is represented as a sequence of bytes that includes the object’s data, as well as information about the object’s type and the types of data stored in the object.

  • After a serialized object has been written to a file, it can be read from the file and deserialized (recreated in memory).

  • Class BinaryFormatter (namespace System.Runtime.Serialization.Formatters.Binary), which supports the ISerializable interface, enables entire objects to be read from or written to a stream.

  • BinaryFormatter methods Serialize and Deserialize write objects to and read objects from streams, respectively.

  • Both method Serialize and method Deserialize require a Stream object (e.g., the FileStream) as a parameter so that the BinaryFormatter can access the correct file.

Section 17.9 Creating a Sequential-Access File Using Object Serialization

  • Classes that are marked with the Serializable attribute or implement the iSerializable interface indicate to the CLR that objects of the class can be serialized. Objects that we wish to write to or read from a stream must include this attribute or implement the iSerializable interface in their class definitions.

  • In a serializable class, you must ensure that every instance variable of the class is also serializable. By default, all simple-type variables are serializable. For reference-type variables, you must check the declaration of the class (and possibly its superclasses) to ensure that the type is serializable.

Section 17.10 Reading and Deserializing Data from a Binary File

  • Method Deserialize (of class BinaryFormatter) reads a serialized object from a stream and reforms the object in memory.

  • Method Deserialize returns a reference of type object which must be cast to the appropriate type to manipulate the object.

  • If an error occurs during deserialization, a SerializationException is thrown.

Terminology

Self-Review Exercises

17.1

State whether each of the following is true or false. If false, explain why.

  1. Creating instances of classes File and Directory is impossible.

  2. Typically, a sequential file stores records in order by the record-key field.

  3. Class StreamReader inherits from class Stream.

  4. Any class can be serialized to a file.

  5. Method Seek of class FileStream always seeks relative to the beginning of a file.

  6. Classes StreamReader and StreamWriter are used with sequential-access files.

  7. You cannot instantiate objects of type Stream.

17.1

  1. True.

  2. True.

  3. False. Class StreamReader inherits from class TextReader.

  4. False. Only classes that implement interface ISerializable or are declared with the Serializable attribute can be serialized.

  5. False. It seeks relative to the SeekOrigin enumeration member that’s passed as one of the arguments.

  6. True.

  7. True.

17.2

Fill in the blanks in each of the following statements:

  1. Ultimately, all data items processed by a computer are reduced to combinations of ___________ and ___________.

  2. The smallest data item a computer can process is called a(n) ___________.

  3. A(n) ___________ is a group of related records.

  4. Digits, letters and special symbols are collectively referred to as ___________.

  5. A group of related files is called a(n) ___________.

  6. StreamReader method ___________ reads a line of text from a file.

  7. StreamWriter method ___________ writes a line of text to a file.

  8. Method Serialize of class BinaryFormatter takes a(n) ___________ and a(n) ___________ as arguments.

  9. The ___________ namespace contains most of C#’s file-processing classes.

  10. The ___________ namespace contains the BinaryFormatter class.

17.2

  1. 0s, 1s.

  2. bit.

  3. file.

  4. characters.

  5. database.

  6. ReadLine.

  7. WriteLine.

  8. Stream, object.

  9. System.IO.

  10. System.Runtime.Serialization.Formatters.Binary.

Answers to Self-Review Exercises

Exercises

17.3

(File of Student Grades) Create a program that stores student grades in a text file. The file should contain the name, ID number, class taken and grade of every student. Allow the user to load a grade file and display its contents in a read-only TextBox. The entries should be displayed in the following format:

LastName, FirstName: ID# Class Grade

We list some sample data below:

Jones, Bob: 1 "Introduction to Computer Science" "A-"
Johnson, Sarah: 2 "Data Structures" "B+"
Smith, Sam: 3 "Data Structures" "C"

17.4

(Serializing and Deserializing) Modify the previous program to use objects of a class that can be serialized to and deserialized from a file.

17.5

(Extending StreamReader and StreamWriter) Extend classes StreamReader and StreamWriter. Make the class that derives from StreamReader have methods ReadInteger, ReadBoolean and ReadString. Make the class that derives from StreamWriter have methods WriteInteger, WriteBoolean and WriteString. Think about how to design the writing methods so that the reading methods will be able to read what was written. Design WriteInteger and WriteBoolean to write strings of uniform size so that ReadInteger and ReadBoolean can read those values accurately. Make sure ReadString and WriteString use the same character(s) to separate strings.

17.6

(Reading and Writing Account Information) Create a program that combines the ideas of Fig. 17.9 and Fig. 17.11 to allow a user to write records to and read records from a file. Add an extra field of type bool to the record to indicate whether the account has overdraft protection.

17.7

(Telephone-Number Word Generator) Standard telephone keypads contain the digits zero through nine. The numbers two through nine each have three letters associated with them (Fig. 17.16). Many people find it difficult to memorize phone numbers, so they use the correspondence between digits and letters to develop seven-letter words that correspond to their phone numbers. For example, a person whose telephone number is 686-2377 might use the correspondence indicated in Fig. 17.16 to develop the seven-letter word “NUMBERS.” Every seven-letter word corresponds to exactly one seven-digit telephone number. A restaurant wishing to increase its takeout business could surely do so with the number 825-3688 (i.e., “TAKEOUT”).

Table 17.16. Letters that correspond to the numbers on a telephone keypad.

Digit

Letter

2

A B C

3

D E F

4

G H I

5

J K L

6

M N O

7

P R S

8

T U V

9

W X Y Z

Every seven-letter phone number corresponds to many different seven-letter words. Unfortunately, most of these words represent unrecognizable juxtapositions of letters. It’s possible, however, that the owner of a barbershop would be pleased to know that the shop’s telephone number, 424-7288, corresponds to “HAIRCUT.” The owner of a liquor store would no doubt be delighted to find that the store’s number, 233-7226, corresponds to “BEERCAN.” A veterinarian with the phone number 738-2273 would be pleased to know that the number corresponds to the letters “PETCARE.” An automotive dealership would be pleased to know that its phone number, 639-2277, corresponds to “NEWCARS.”

Write a GUI program that, given a seven-digit number, uses a StreamWriter object to write to a file every possible seven-letter word combination corresponding to that number. There are 2,187 (37) such combinations. Avoid phone numbers with the digits 0 and 1.

17.8

(Student Poll) Figure 8.8 contains an array of survey responses that’s hard-coded into the program. Suppose we wish to process survey results that are stored in a file. First, create a Windows Form that prompts the user for survey responses and outputs each response to a file. Use StreamWriter to create a file called numbers.txt. Each integer should be written using method Write. Then add a TextBox that will output the frequency of survey responses. You should modify the code in Fig. 8.8 to read the survey responses from numbers.txt. The responses should be read from the file by using a StreamReader. Class string’s split method should be used to split the input string into separate responses, then each response should be converted to an integer. The program should continue to read responses until it reaches the end of file. The results should be output to the TextBox.

Making a Difference Exercise

17.9

(Phishing Scanner) Phishing is a form of identity theft in which, in an e-mail, a sender posing as a trustworthy source attempts to acquire private information, such as your user names, passwords, credit-card numbers and social security number. Phishing e-mails claiming to be from popular banks, credit-card companies, auction sites, social networks and online payment services may look quite legitimate. These fraudulent messages often provide links to spoofed (fake) websites where you’re asked to enter sensitive information.

Visit McAfee® (www.mcafee.com/us/threat_center/anti_phishing/phishing_top10.html), Security Extra (www.securityextra.com/), www.snopes.com and other websites to find lists of the top phishing scams. Also check out the Anti-Phishing Working Group (www.antiphishing.org/), and the FBI’s Cyber Investigations website (www.fbi.gov/cyberinvest/cyberhome.htm), where you’ll find information about the latest scams and how to protect yourself.

Create a list of 30 words, phrases and company names commonly found in phishing messages. Assign a point value to each based on your estimate of its likeliness to be in a phishing message (e.g., one point if it’s somewhat likely, two points if moderately likely, or three points if highly likely). Write a program that scans a file of text for these terms and phrases. For each occurrence of a keyword or phrase within the text file, add the assigned point value to the total points for that word or phrase. For each keyword or phrase found, output one line with the word or phrase, the number of occurrences and the point total. Then show the point total for the entire message. Does your program assign a high point total to some actual phishing e-mails you’ve received? Does it assign a high point total to some legitimate e-mails you’ve received? [Note: If you search online for “sample phishing emails,” you’ll find many examples of text that you can test with this program.]



[1] Generally, a file can contain arbitrary data in arbitrary formats. In some operating systems, a file is viewed as nothing more than a collection of bytes, and any organization of the bytes in a file (such as organizing the data into records) is a view created by the application programmer.

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

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