Team LiB
Previous Section Next Section

What Is Persistence?

Whenever we run a program such as our SRS application, any objects (or value types, for that matter) that we declare and instantiate "live" in memory. When the program terminates, all of the memory allocated to the program is released back to the operating system, and the internal states of all of the objects created by the application are lost unless they have been saved—persisted—in some fashion.

Using various APIs, C# provides a wealth of options with regard to persisting data.

We'll be illustrating the most basic form of data persistence—record-oriented ASCII file persistence—in our SRS application code; but many of the same design issues are applicable to the other forms of persistence as well, such as

and so forth.

Of course, there are two "sides" to the file persistence "coin": writing an object's state out to a file, and reading it back in again later. We'll talk briefly about a C# approach for each.

The FileStream Class

A FileStream object is a type of C# object that knows how to open a file and either read data from the file or write data to the file, one byte at a time.

The constructor for the FileStream class has the following header:

public FileStream(string filename, int mode)

e.g.,

FileStream fs = new FileStream("data.dat", FileMode.Open);

where mode is one of several constants defined by the FileMode class:

  • FileMode.Open opens an existing file for either reading or writing; if the file in question doesn't exist or can't be opened, a FileNotFoundException is thrown.

  • FileMode.Create opens a brand-new file for writing; if a file by the specified name already exists, or if a file can't be created for some reason—e.g., if the target directory is write-protected—then an IOException is thrown.

  • FileMode.Append opens an existing file for writing, so that additional data may be appended to the end of the file; if the specified file isn't found, a FileNotFoundException is thrown.

  • FileMode.CreateOrAppend will attempt to open an existing file for writing, if one is found with the specified name; otherwise, it will create a new file for writing. If for some reason a file can't be opened or created—e.g., if the target directory is write-protected—then an IOException is thrown.

The FileStream class is defined in the System.IO namespace.

Reading from a File

The basic C# approach that we're going to use for reading records one-by-one from an ASCII file involves two types C# objects—a FileStream object and a StreamReader object (see Figure 15-2).

  1. First, we'll create an object of type FileStream, which as we've already mentioned knows how to open a file and read data from the file one byte at a time.

  2. Next, we'll pass a reference to that FileStream object as an argument to the constructor for a StreamReader, a more sophisticated type of object that is effectively "wrapped around" the FileStream. The StreamReader's ReadLine method knows how to internally collect up, or buffer, individual characters as read by the FileStream until an end-of-line character is detected, at which point the StreamReader hands back a complete line/record of data to the client code.

The StreamReader class is also defined in the System.IO namespace.

Click To expand
Figure 15-2: Reading from a file, C# style

Here is an example to illustrate the general process of reading from a file; we've left out some details, but will see this process carried out in earnest in the various SRS classes that we'll be reviewing later on in this chapter. We'll present the example in its entirety first, and then we'll highlight some noteworthy points afterward.

using System.IO;

public class IOExample
{
    static void Main() {
     // Declare references to the objects that we'll need
     // in order to read from a  file.
     FileStream fs;
     StreamReader srIn;

     // Read operations should be placed in a  try-catch block.
     try {
       // Create a  FileStream ...
       fs = new FileStream("data.dat", FileMode.Open);

       // ... and a  StreamReader based on that FileStream.
       srIn = new StreamReader(fs);

       // Read the first line from the file.
       string line = srIn.ReadLine();

       // As long as the line isn't null, keep going!
       while (line != null) {
         // Pseudocode.
         process the most recently read line

         // Read another line (will be set to null when
         // the file has been exhausted).
         line = srIn.ReadLine();
       }

       // Close the StreamReader, which causes the FileStream to
       // also be closed.
       srIn.Close();
     }
     catch (IOException ioe) {
       // Perform exception handling ... details omitted.
     }
    }

}

Narrating our example:

  • Because so many things can potentially go wrong when attempting to perform file I/O—a file that we're trying to open may not exist, a file that we want to write data to may have read-only protection, and so forth—we must place our code within a try block, and provide code to catch and respond to potential IOExceptions.

  • We want to read data from an existing file, and so the FileMode.Open constant is being passed to the FileStream constructor:

              fs = new FileStream("data.dat", FileMode.Open);
    
  • We then "wrap" the FileStream in a new instance of a StreamReader to allow us to read data from the file a line at a time using the ReadLine method of the StreamReader class:

              srIn = new StreamReader(fs);
    
  • We use the StreamReader's ReadLine method to read one line/record's worth of data at a time:

              string line = srIn.ReadLine();
    

    and as long as this method doesn't return a value of nullnull signals that the end of file has been reached—then we know that we've read in a legitimate record from the file:

              // As long as the line isn't null, keep going!
              while (line != null) { ... }
    
  • We must remember to read another record's worth of data from within the while loop so that we don't wind up creating an infinite loop:

              line = srIn.ReadLine();
    
  • Finally, we must remember to close the StreamReader, which also closes the FileStream:

              srIn.Close();
    

    It's important to remember to close a StreamReader when we're finished with a file for several reasons:

  • So that the file won't remain open/locked to subsequent access

  • So that the application as a whole doesn't exceed the (platform-dependent) maximum allowable open file limit

  • For the general good of freeing up unused objects so that they will be subject to garbage collection

Writing to a File

The basic C# approach that we'll be using for writing records to an ASCII file is similar, but of course in reverse, to what it takes to read from a file (see Figure 15-3).

  1. We again create an object of type FileStream; by specifying the appropriate mode to the FileStream constructor, the FileStream can be used to overwrite an existing file (FileMode.Open), append data to an existing file (FileMode.Append), create a new file (FileMode.Create), or combine the previous two approaches (FileMode.OpenOrCreate).

  2. Then, we pass that FileStream object as an argument to the constructor for a StreamWriter, a more sophisticated type of object that is "wrapped around" the FileStream. The StreamWriter's WriteLine method knows how to pass an entire record/line's worth of data, one character at a time, to its encapsulated FileStream object, which then outputs the data one byte at a time to the file.

Click To expand
Figure 15-3: Writing to a file, C# style

The StreamWriter class inherits the WriteLine method from the TextWriter class. The method works in a similar fashion to the Console.WriteLine method that you're already familiar with, the only difference being that the former causes text to be written to a file or output stream, whereas the latter causes text to be displayed in the command-line window. StreamWriter also defines a Write method, which works exactly like the Console.Write method.

The StreamWriter class is defined in the System.IO namespace.

We'll see this carried out in earnest in the Student class, which we'll be reviewing in detail later in this chapter, but for now, here's the general approach; because this code is so similar to the previous IOExample, we'll present it without narration here—please refer to the in-line comments:

using System.IO;

public class IOExample2
{
    static void Main() {
     FileStream fs;
     StreamWriter sw;

     // Write operations should be placed in a  try-catch block.
     try {
       // Create a  FileStream ...
       fs = new FileStream("data.dat", FileMode.OpenOrCreate);

       // ... and a  StreamWriter based on that FileStream.
       sw = new StreamWriter(fs);

       // Pseudocode.
       while (still want to print more) {
         sw.WriteLine(whatever string data we wish to output);
       }
       sw.Close();
     }
     catch (IOException ioe) {
       // perform some exception handling
     }
   }
 }

We'll use this basic approach when we persist the results of a student registration session to a file; this will be discussed in detail toward the end of the chapter.

Populating the Main SRS Collections

In Chapter 14, we introduced the ScheduleOfClasses class as a means of encapsulating a collection of Section objects, but all of the work necessary to populate this collection was performed in the Main method of the SRS class. By way of review, we first instantiated a ScheduleOfClasses object as a public static attribute of the SRS class:

  public static ScheduleOfClasses scheduleOfClasses =
    new ScheduleOfClasses("SP2004");

Next, we called the ScheduleSection method on various Course objects named c1 through to c5 from the SRS Main method to instantiate seven Section objects named sec1 through to sec7, using hard-coded attribute values:

// Schedule sections of each Course by calling the
// ScheduleSection method of Course (which internally
// invokes the Section constructor).

sec1 = c1.ScheduleSection('M', "8:10 - 10:00 PM", "GOVT101", 30);
sec2 = c1.ScheduleSection('W', "6:10 - 8:00 PM", "GOVT202", 30);
sec3 = c2.ScheduleSection('R', "4:10 - 6:00 PM", "GOVT105", 25);
sec4 = c2.ScheduleSection('T', "6:10 - 8:00 PM", "SCI330", 25);
sec5 = c3.ScheduleSection('M', "6:10 - 8:00 PM", "GOVT101", 20);
sec6 = c4.ScheduleSection('R', "4:10 - 6:00 PM", "SCI241", 15);
sec7 = c5.ScheduleSection('M', "4:10 - 6:00 PM", "ARTS25", 40);

We then invoked the AddSection method on the scheduleOfClasses object numerous times to add these Sections to its encapsulated collection:

// Add these to the Schedule of Classes.

scheduleOfClasses.AddSection(sec1);
scheduleOfClasses.AddSection(sec2);
scheduleOfClasses.AddSection(sec3);
scheduleOfClasses.AddSection(sec4);
scheduleOfClasses.AddSection(sec5);
scheduleOfClasses.AddSection(sec6);
scheduleOfClasses.AddSection(sec7);

Ideally, rather than hard-coding the information about these Sections in the Main method of the SRS class, we'd like to acquire this information dynamically from an ASCII file. In fact, while we're at it, it would be preferable to acquire all of the data needed to initialize the SRS application's primary object collections from ASCII files. This includes

  • The schedule of classes itself

  • The course catalog: that is, a list of courses on which the schedule of classes is based, along with information about which course is a prerequisite of which other(s)

  • The faculty roster, along with information regarding which professor is scheduled to teach which section(s)

These latter two collections haven't appeared in our object model before, because they weren't necessary for fulfilling the use cases that we came up with for the SRS back in Chapter 9. These collections represent what we've spoken of before as implementation classes; looking ahead, we know that we're going to need these when the time comes to build our SRS user interface, so we'll go ahead and implement them now. (We're not worrying about creating a StudentBody collection to house Student objects, for reasons that will become apparent later.) We'll define five data files to "feed" these three collections, as follows:

  • CourseCatalog.dat: This file contains records consisting of three tab-delimited fields: a course number, a course title, and the number of credits that the course is worth, represented as a floating point number.

    In other words, this data file represents the attributes of the Course class in our domain model. It will "feed" the CourseCatalog collection.

    Here are the test data contents of the file that we'll use for all of the work that we'll do in this chapter; <tab> represents the presence of an otherwise invisible tab character.

           CMP101<tab>Beginning Computer Technology<tab>3.0
           OBJ101<tab>Object Methods for Software Development<tab>3.0
           CMP283<tab>Higher Level Languages (C#)<tab>3.0
           CMP999<tab>Living Brain Computers<tab>3.0
           ART101<tab>Beginning Basketweaving<tab>3.0
    
  • Faculty.dat: This file contains records consisting of four tab-delimited fields, representing a professor's name, SSN, title, and the department that they work for.

    In other words, this file represents the attributes of the Professor class in our domain model. It will "feed" the Faculty collection.

    Here is the test data that we'll use:

           Jacquie Barker<tab>123-45-6789<tab>Asst. Professor<tab>Info. Technology
           John Carson<tab>567-81-2345<tab>Full Professor<tab>Info. Technology
           Jackie Chan<tab>987-65-4321<tab>Full Professor<tab>Info. Technology
    
  • SoC_SP2004.dat: This file contains the Schedule of Classes (SoC) information for the Spring 2004 (SP2004) semester; each tab-delimited record consists of six fields representing the course number, section number, day of the week, time of day, room, and seating capacity for the section in question.

    This file represents the attributes of the Section class, combined with the courseNo attribute of Course, which the Section class is able to "pull" by virtue of its one-to-many association with Course (recall our discussion of "data flowing along an association line" from Chapter 10). In other words, it simultaneously represents Section objects as a whole as well as links that Section objects maintain to Course objects.Here is the test data that we'll use:

           CMP101<tab>1<tab>M<tab>8:10 - 10:00 PM<tab>GOVT101<tab>30
           CMP101<tab>2<tab>W<tab>6:10 - 8:00 PM<tab>GOVT202<tab>30
           OBJ101<tab>1<tab>R<tab>4:10 - 6:00 PM<tab>GOVT105<tab>25
           OBJ101<tab>2<tab>T<tab>6:10 - 8:00 PM<tab>SCI330<tab>25
           CMP283<tab>1<tab>M<tab>6:10 - 8:00 PM<tab>GOVT101<tab>20
           CMP999<tab>1<tab>R<tab>4:10 - 6:00 PM<tab>SCI241<tab>15
           ART101<tab>1<tab>M<tab>4:10 - 6:00 PM<tab>ARTS25<tab>40
    
  • Prerequisites.dat: This file contains information about which Course, listed in the first column, is a prerequisite for which other Course, listed in the second column.

    In other words, this file represents the reflexive prerequisite association that exists on the Course class, and the records themselves represent links between specific Course objects.

    Here is the test data that we'll use:

           CMP101<tab>OBJ101
           OBJ101<tab>CMP283
           CMP283<tab>CMP999
    
  • TeachingAssignments.dat: This file pairs up a Professor (whose ssn is reflected in the first column) with the Course/Section number that the professor is going to be teaching (listed in the second column).

    In other words, this file represents the teaches association between a Professor and a Section.

    Here is our test data:

           987-65-4321<tab>CMP101 - 1
           567-81-2345<tab>CMP101 - 2
           123-45-6789<tab>OBJ101 - 1
           987-65-4321<tab>OBJ101 - 2
           123-45-6789<tab>CMP283 - 1
           567-81-2345<tab>CMP999 - 1
           987-65-4321<tab>ART101 - 1
    
Note?/td>

All five of these data files are provided with the accompanying SRS code for download from the Apress web site (http://www.apress.com).

Persisting Student Data

One key difference between the way that we plan on handling Student data as compared with data for the other classes mentioned previously is that we're going to store each Student object's data in its own separate file, versus lumping all of the data about all Students into a single StudentBody.dat file. This will enable us to retrieve the information for just one student at a time—namely, whichever student is currently logged on to the SRS—and to easily save any changes that occur to that Student object's information during his or her SRS session when he or she logs off. (We'll see how a student logs on in Chapter 16, when we add a GUI to our application.)

  • The naming convention for a student's data file will be to use the student's Social Security Number (ssn) with a suffix of .dat; for example, 111-11-1111.dat.

  • At a minimum, a student's data file will contain a single primary record, comprised of four tab-delimited fields representing the student's SSN, name, major department, and degree sought. In other words, this record represents the attributes of the Student class in our object model.

  • If the student has already registered for one or more sections in a previous SRS session, then the student's data file will also contain one or more secondary records, each consisting of a single field representing the full section number (that is, the course number followed by a hyphen, followed by the section number as an integer) of a section that the student is currently enrolled in. In other words, a secondary record represents the attends association in our object model, and any one record implies a link between this Student and a Section object.

We'll simulate three students in this fashion:

  • 111-11-1111.dat: This student will be simulated as already having enrolled in two sections; the contents of this data file are as follows:

           111-11-1111<tab>Joe Blow<tab>Math<tab>M.S.
           CMP101 - 1
           ART101 - 1
    
  • 222-22-2222.dat: This student will be simulated as not yet having enrolled in any sections; the contents of this data file are as follows:

           222-22-2222<tab>Gerson Lopez<tab>Information Technology<tab>Ph. D.
    
  • 333-33-3333.dat: This student will also be simulated as not yet having enrolled in any sections; the contents of this data file are as follows:

           333-33-3333<tab>Mary Smith<tab>Physics<tab>B.S.
    
Note?/td>

As with the previous data files, all three of these are provided with the accompanying SRS code for download from the Apress web site.

Why Aren't We Going to Persist Other Object Types?

We're not worried about persisting information about any other object type besides Student, because we assume that the rest of the data is "static": that is, during a particular login session, the user won't be able to alter Professor or Course information. All the student user will be able to do is to choose classes (sections) from the ScheduleOfClasses to register for, and/or to drop classes (sections); this merely changes the status of the links between a Student and various Section objects, which are stored as secondary records in the student's data file. In fact, we aren't even giving student users the ability to change their own primary information—name, ssn, etc.—via this application.


Team LiB
Previous Section Next Section