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 |
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.
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.
To use classes FileStream
and BinaryFormatter
to read objects from and write objects to files.
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.
Ultimately, all data items that computers process are reduced to combinations of 0
s and 1
s. 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 0
s and 1
s.
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
).
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., A
–Z
and a
–z
) 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 0
s and 1
s, every character in a computer’s character set is represented as a pattern of 0
s and 1
s. 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.
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.
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:
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.
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.
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.
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.
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.
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.
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).
Description | |
---|---|
| Returns a |
| Copies a file to a new file. |
| Creates a file and returns its associated |
| Creates a text file and returns its associated |
| Deletes the specified file. |
| Returns |
| Returns a |
| Returns a |
| Returns a |
| Moves the specified file to a specified location. |
| Returns a |
| Returns a read-only |
| Returns a |
| Returns a write |
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.
| Description |
---|---|
| Creates a directory and returns its associated |
| Deletes the specified directory. |
| Returns |
| Returns a |
| Returns a |
| Returns a |
| Returns a |
| Returns a |
| 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
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.
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
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.
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 string
s and int
s, 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.
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
.
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.”
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 Label
s and four TextBox
es. 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 TextBox
es, 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
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.
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
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.
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.
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.
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 TextBox
es 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 TextBox
es (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.
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.
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 |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
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.
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 string
s 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 TextBox
es. 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).
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. RichTextBox
es provide more functionality than regular TextBox
es—for example, RichTextBox
es 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
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).
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 string
s (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.9–17.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.
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.
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 string
s 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.
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.
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).
In this chapter, you learned how to use file processing to manipulate persistent data. You learned that data is stored in computers as 0
s and 1
s, 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.
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.
All data items that computers process are reduced to combinations of 0
s and 1
s.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
Copy
method of class File
Create
method of class File
CreateText
method of class File
fixed-length records
GetCreationTime
method of class Directory
GetLastAccessTime
method of class Directory
GetLastWriteTime
method of class Directory
Move
method of class Directory
Move
method of class File
OpenRead
method of class File
OpenText
method of class File
OpenWrite
method of class File
transaction-processing system
Write
method of class StreamWriter
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 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 |
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 |
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”). 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 |
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 |
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.