Welcome to Linking an Object Application to Simple Files!
Have you ever created a slick Graphical User Interface (GUI) in C#, typed in a bunch of student records or customer orders, closed the application, and then watched in horror as all your data disappeared into thin air? Don't worry, every programmer has been there! When your application runs, everything is held temporarily in your computer's RAM (Random Access Memory). As soon as the program terminates, that memory is wiped clean.
In this chapter of AS 2: Event Driven Programming, we will learn how to give our applications a long-term memory by connecting them to simple sequential files (like .txt and .csv files). By the end of this guide, you will master reading from files, writing to files, turning raw text into live C# objects, and writing bulletproof code that won't crash when things go wrong.
1. Understanding Data Persistence & Simple Files
What is Data Persistence?
Data Persistence refers to saving application state and entity data to non-volatile storage (such as a hard drive or SSD) so that it outlives the execution of the program. Think of RAM as a whiteboard that gets wiped clean every evening, while a text file on disk is like a permanent notebook where your data stays safe forever.
Simple & Sequential Files vs. Relational Databases
In Unit AS 2, you will frequently work with simple sequential files. Here is how they work:
• Plain Text Files (.txt): Unstructured or semi-structured streams of character data.
• Comma-Separated Values (.csv): Delimited text files where each row represents a record and individual fields are separated by a delimiter (most commonly a comma ,).
• Sequential Access: Unlike databases where you can jump straight to row number \(500\), sequential files must be processed from start to finish in order—from line 1 down to the very last line.
Architectural Golden Rule: Separation of Concerns
When building an event-driven GUI application in C#, it is tempting to write file-reading code straight inside a button click event handler. Avoid doing this!
Good software design requires a clear Separation of Concerns:
• GUI Layer (View): Captures user clicks and displays information (e.g., textboxes, listboxes, buttons).
• Business / File I/O Layer: Handles file streams, reading, writing, and parsing data.
The event handler in your form should simply call a method on a dedicated class or file handler rather than performing complex file operations directly inside the visual form.
Key Takeaway: Simple sequential files allow our object applications to persist data across sessions by writing and reading formatted text streams one line after another.
2. The .NET Stream Architecture (`System.IO`)
What is a Stream?
Imagine a water pipe connecting your program to a file on the storage drive. A Stream is an abstraction that represents a continuous sequence of bytes or characters traveling through that pipe. To use stream operations in C#, you must always import the correct namespaces at the very top of your code file:
using System.IO;
using System.Collections.Generic;
Key Classes You Need to Know:
• StreamWriter: Writes characters to a stream in a specific format.
• StreamReader: Reads characters from a byte stream sequentially.
• File: A static utility class providing helper methods like File.Exists(path).
• FileInfo: An object providing instance properties and methods for inspecting file metadata.
Key Takeaway: The System.IO namespace contains the stream tools needed to pipe characters into and out of sequential files.
3. Writing Data to Files (Serialization & Output)
Serialization is the process of converting an in-memory object (with its properties and values) into a formatted string of text so that it can be stored in a file.
Append Mode vs. Overwrite Mode
When creating a StreamWriter, its constructor accepts a boolean parameter called append:
• Overwrite Mode (append = false): new StreamWriter(filePath, false)
This replaces the entire file. If the file already exists, all old data is deleted and replaced with your new data.
• Append Mode (append = true): new StreamWriter(filePath, true)
This opens the existing file and jumps straight to the end, adding new records to the bottom without erasing existing entries.
Buffer Flushing and the Golden `using` Statement
When you write data using writer.WriteLine(...), the operating system doesn't immediately write every single character directly to the disk. Instead, it stores them in a temporary holding area called a buffer.
If your program finishes or crashes before this buffer is pushed to disk (flushed) and the file is closed, two terrible things happen:
1. Your data is lost or corrupted.
2. The operating system places an exclusive file lock on the file, preventing any other part of your program from opening it again!
The Solution: Always enclose your streams inside a C# using statement. The using block guarantees that .Close() and .Dispose() are automatically called—even if an unexpected crash occurs!
Example: Writing a List of Objects to a CSV File
Imagine we have a simple Student class with properties ID, Name, and Score:
// Method in your data handling class
public void SaveStudents(string filePath, List<Student> studentList)
{
using (StreamWriter writer = new StreamWriter(filePath, false))
{
foreach (Student s in studentList)
{
string csvLine = s.ID + "," + s.Name + "," + s.Score;
writer.WriteLine(csvLine);
}
} // The stream is automatically flushed, closed, and unlocked here!
}
Key Takeaway: Choose the correct append flag (true to add, false to overwrite) and always wrap StreamWriter in a using block to prevent file locks.
4. Reading Data & Rebuilding Objects (Deserialization & Input)
Deserialization is the exact reverse of writing: we read lines of raw text from a file, split the text into separate values, convert each value back into its correct data type, and construct a brand-new object in memory.
Step-by-Step Deserialization Recipe:
Step 1: Open the file using a StreamReader wrapped in a using block.
Step 2: Loop through the file line-by-line until reaching the end.
Step 3: Read a line as a raw string using reader.ReadLine().
Step 4: Break the string apart into an array using line.Split(',').
Step 5: Parse each field into its proper data type (e.g., int.Parse(), double.Parse()).
Step 6: Call the class constructor to instantiate the object and add it to your List<T>.
Loop Termination Conditions
There are two common ways to loop through a sequential file in C#:
• Method A (EndOfStream property):
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
// Process line...
}
• Method B (Null check):
string line;
while ((line = reader.ReadLine()) != null)
{
// Process line...
}
Example: Reading CSV Records into a Generic List
public List<Student> LoadStudents(string filePath)
{
List<Student> loadedList = new List<Student>();
using (StreamReader reader = new StreamReader(filePath))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
if (!string.IsNullOrWhiteSpace(line))
{
string[] parts = line.Split(',');
int id = int.Parse(parts[0]);
string name = parts[1];
int score = int.Parse(parts[2]);
Student studentObj = new Student(id, name, score);
loadedList.Add(studentObj);
}
}
}
return loadedList;
}
Key Takeaway: Read sequentially, split your delimited string into an array, parse values to appropriate types, and instantiate your domain objects.
5. Defensive Coding & Exception Handling
File I/O operations are unpredictable. Files can be deleted, moved, corrupted, or locked by other software. To make your AS 2 portfolio robust, you must implement defensive checks and structured exception handling.
1. Defensive Pre-Check (`File.Exists`)
Before ever attempting to open a file for reading, always check whether it actually exists:
if (!File.Exists(filePath))
{
MessageBox.Show("Error: Target data file could not be found.");
return;
}
2. Structured Exception Handling (`try-catch-finally`)
When handling exceptions, catch specific exceptions first before catching general errors. This allows you to give helpful, tailored feedback to the user.
Key I/O Exception Hierarchy:
• FileNotFoundException: Triggered when the specified file path does not point to a real file on disk.
• DirectoryNotFoundException: Triggered when part of the folder directory path is invalid or missing.
• FormatException: Occurs when type conversion fails during parsing (e.g., trying to parse text like "Ten" into an integer with int.Parse()).
• IndexOutOfRangeException: Occurs when splitting a corrupted or incomplete line that does not contain all expected columns.
• IOException: General stream or hard drive failure, or the file is locked by another running program.
• Exception: The parent exception class. Catches any remaining unexpected runtime faults.
Exception Handling Structure in Action:
try
{
// Perform file stream read or write operations here
}
catch (FileNotFoundException ex)
{
MessageBox.Show("The file was not found: " + ex.Message);
}
catch (FormatException ex)
{
MessageBox.Show("Data is corrupt or invalid format: " + ex.Message);
}
catch (IOException ex)
{
MessageBox.Show("Disk I/O error or file currently locked: " + ex.Message);
}
catch (Exception ex)
{
MessageBox.Show("An unexpected error occurred: " + ex.Message);
}
finally
{
// Optional cleanup code that ALWAYS runs, whether an error occurred or not
}
Key Takeaway: Order your catch blocks from most specific to least specific. Catching specific errors makes debugging simple and keeps your application user-friendly.
6. Common Pitfalls & CCEA Exam Tips
Watch out for these common traps when completing your Unit AS 2 coursework portfolio:
• Pitfall 1: Hardcoding Absolute File Paths
Bad: "C:\\Users\\JohnSmith\\Documents\\Data.txt"
Why it fails: When the examiner or moderator opens your project on their computer, your user folder will not exist, and the program will crash immediately!
Fix: Use relative paths (such as "Data.txt" or AppDomain.CurrentDomain.BaseDirectory + "Data.txt") so the file travels seamlessly with your compiled application.
• Pitfall 2: Forgetting Delimiter Clashes
If you use a comma (,) as your CSV delimiter, what happens when a user types an address like "12 High Street, Belfast" into a textbox? The Split(',') method will see two separate columns instead of one! Consider validating user input to remove rogue commas or using alternative unique delimiters.
• Pitfall 3: Not Handling Empty Trailing Lines
Text editors and stream writers often leave a blank line at the very end of a file. If you pass an empty line into line.Split(',') and try to parse parts[0], your program will crash with an IndexOutOfRangeException. Always check if (!string.IsNullOrWhiteSpace(line)) before parsing!
• Pitfall 4: Leaving Streams Open
Failing to call .Close() or neglecting to use a using statement locks the file in the Windows OS. The next time you click "Save" or "Load", the app will throw an unhandled IOException.
7. Quick Review Summary
• Persistence: Saves object state beyond runtime memory (RAM) onto disk storage.
• StreamWriter: Writes lines to file. append = true adds to the bottom; append = false overwrites.
• StreamReader: Reads lines sequentially using .ReadLine() inside a loop until EndOfStream.
• Serialization / Deserialization: Turn objects into delimited strings to save; split and parse text strings back into instantiated objects to load.
• Resource Management: Always enclose StreamReader and StreamWriter in using statements to prevent persistent file locks.
• Robustness: Check File.Exists() and wrap reading/parsing routines in structured try-catch blocks with specific exception handlers.