Introduction

In modern software development, efficient data management is essential. While relational database management systems like SQL servers are often the go-to solution, simpler scenarios sometimes require a more lightweight approach. For example, applications that do not demand the complexity of relational databases can benefit from using text files for data storage. This is the case with our Phone Book application, which manages data using a simple 2D table. To streamline the process of saving and retrieving data, we developed the ArrayIOExtensionsLib, a library that provides easy-to-use methods for handling data stored in text files.

Overview of ArrayIOExtensionsLib

ArrayIOExtensionsLib is a .NET library designed to simplify working with arrays stored in text files. It provides extension methods for saving and loading both single and multi-dimensional arrays of simple types (such as strings and primitives) to and from text files. This library is ideal for applications that do not require a relational database but still need reliable and efficient data storage and retrieval.

Key Features

Important Notice. All array fields must be properly initialized before using the extension methods in this library. Uninitialized arrays or null values may lead to unexpected behavior during serialization and deserialization.

Installation

To use this library in your project.

  1. Download ArrayIOExtensionsLib open-source archive
  2. Build the project to generate the .dll file.
  3. Reference the generated .dll in your own project.
  4. In Visual Studio, Right-click your project > Add > Reference > Browse to select the generated .dll.
  5. In .NET CLI, Run the following command.
    dotnet add reference /path/to/ArrayIOExtensions.dll

Usage

Save an Array to a Text File.

int[,] myArray = new int[,] 
{ 
    { 1, 2 }, 
    { 3, 4 }, 
    { 5, 6 } 
};

myArray.SaveToTextFile("output.txt");

Load an Array from a Text File.

int[,] myArray = new int[3, 2];
myArray.LoadFromTextFile("output.txt");

Saving Custom Types.

public class Customer
{
    public string? Name { get; set; }
    public string? Address { get; set; }
}

Customer[] customers = new Customer[2];
customers[0] = new Customer 
{ 
    Name = "John", 
    Address = "123 Street" 
};

customers[1] = new Customer 
{ 
    Name = "Jane", 
    Address = "456 Avenue" 
};

customers.SaveToTextFile("customers.txt");

Loading Custom Types.

Customer[] customers = new Customer[2];
customers.LoadFromTextFile("customers.txt");

Code Breakdown

Here is a detailed explanation of the ArrayIOExtensions class code, method by method.

using System.IO;
using System.Text;
using System;

namespace ArrayIOExtensionsLib
{
    /// <summary>
    /// Provides extension methods for saving and loading 
    /// single or multi-dimensional arrays to and from text files.
    /// IMPORTANT: Values in the array must be properly initialized
    /// before calling the save methods, especially when working with
    /// arrays of complex or custom types. If not properly initialized, 
    /// null values in objects may lead to incorrect written values 
    /// in the text file, as the object will not be serialized with 
    /// all its properties.
    /// </summary>
    public static class ArrayIOExtensions
    {
        #region Properties

        /// <summary>
        /// Represents the default encoding used for saving arrays to text files.
        /// </summary>
        private static readonly Encoding _defaultEncoding = Encoding.UTF8;

        /// <summary>
        /// Represents the default word used for null value.
        /// </summary>
        private static readonly string _nullRepresentation = "null";

        #endregion
    }
}

Properties

The ArrayIOExtensionsLib library relies on a couple of key properties to ensure smooth operation during file I/O processes:

Saving Arrays to Text Files

The primary method for saving arrays is SaveToTextFile.

Important. Values in the array must be properly initialized before calling the save methods, especially when working with arrays of complex or custom types. If not properly initialized, null values in objects may lead to incorrect written values in the text file, as the object will not be serialized with all its properties.

#region Save To Text File

/// <summary>
/// Saves a single or multi-dimensional array to a specified text file.
/// Each element of the array is written to a new line in the file.
/// IMPORTANT: Values in the array must be properly initialized before
/// calling the save methods, especially when working with arrays
/// of complex or custom types. If not properly initialized, 
/// null values in objects may lead to incorrect written values
/// in the text file, as the object will not be serialized 
/// with all its properties.
/// </summary>
/// <param name="array">The single or multi-dimensional array to save.</param>
/// <param name="filePath">The path of the file where the array will be saved.</param>
/// <param name="encoding">Encoding used for saving arrays to text files.</param>
/// <exception cref="ArgumentNullException">Thrown when the array is null.</exception>
/// <exception cref="ArgumentException">Thrown when the file path is null or whitespace.</exception>
public static void SaveToTextFile(this Array array,
                                  string filePath,
                                  Encoding? encoding = null)
{
    // Validate the array and file path before proceeding.
    ValidateArrayAndFilePath(array, filePath);

    try
    {
        // Check encoding and set the default encoding if not specified.
        encoding ??= _defaultEncoding;

        // Create a StreamWriter to write to the specified file using UTF-8 encoding.
        using var writer = new StreamWriter(filePath, false, encoding);

        // Start the process of saving the array to the text file.
        SaveArrayToTextFile(array, writer);
    }
    catch (Exception ex)
    {
        // Throw an exception if an error occurs during file writing.
        throw new ArgumentException(
            $"An error occurred while saving to file: {ex.Message}");
    }
}

SaveToTextFile Method

Internal Method for Saving

/// <summary>
/// Initializes the dimensions array and starts the recursive saving of the array.
/// </summary>
/// <param name="array">The single or multi-dimensional array to save.</param>
/// <param name="writer">The StreamWriter used for writing to the file.</param>
private static void SaveArrayToTextFile(Array array, StreamWriter writer)
{
    // Create an array to hold the length
    // of each dimension of the array.
    int[] dimensions = new int[array.Rank];
    for (int i = 0; i < array.Rank; i++)
    {
        dimensions[i] = array.GetLength(i);
    }

    // Get the element type of the array.
    var elementType = array.GetType().GetElementType();

    // Begin the recursive method to save
    // the array elements to the text file.
    SaveArrayToTextFileRecursive(array,
                                 writer,
                                 dimensions,
                                 new int[array.Rank],
                                 0,
                                 elementType!);
}

SaveArrayToTextFile Method

Recursive Saving Logic

/// <summary>
/// Recursively writes the contents of 
/// the single or multi-dimensional array to the StreamWriter.
/// </summary>
/// <param name="array">The single or multi-dimensional array to save.</param>
/// <param name="writer">The StreamWriter used for writing to the file.</param>
/// <param name="dimensions">The lengths of each dimension of the array.</param>
/// <param name="indices">The current indices in the array being processed.</param>
/// <param name="currentIndex">The current dimension index being processed.</param>
/// <param name="elementType">The type of the object to serialize.</param>
private static void SaveArrayToTextFileRecursive(Array array,
                                                 StreamWriter writer,
                                                 int[] dimensions,
                                                 int[] indices,
                                                 int currentIndex,
                                                 Type elementType)
{
    // Shortcut to handle single-dimensional arrays directly
    if (array.Rank == 1)
    {
        for (int i = 0; i < array.Length; i++)
        {
            // Get the value of the single-dimensional array
            var value = array.GetValue(i);

            // Serialize the value
            SerializeObject(writer, value, elementType);
        }
        return; 
        // Exit early since we've already processed the entire array
    }

    // If we are at the last dimension, write the values to the file.
    if (currentIndex == array.Rank - 1)
    {
        for (int i = 0; i < dimensions[currentIndex]; i++)
        {
            indices[currentIndex] = i; // Set the index for the last dimension.
            var value = array.GetValue(indices);

            SerializeObject(writer, value, elementType);
        }
    }
    else
    {
        // Iterate through the current dimension
        // and recurse into the next dimension.
        for (int i = 0; i < dimensions[currentIndex]; i++)
        {
            indices[currentIndex] = i;
            SaveArrayToTextFileRecursive(array,
                                         writer,
                                         dimensions,
                                         indices,
                                         currentIndex + 1,
                                         elementType);
        }
    }
}

#endregion

SaveArrayToTextFileRecursive Method

Serialization

/// <summary>
/// Serializes an object to the StreamWriter.
/// </summary>
/// <param name="writer">The StreamWriter used for writing to the file.</param>
/// <param name="obj">The object to serialize.</param>
/// <param name="elementType">The type of the object to serialize.</param>
private static void SerializeObject(StreamWriter writer,
                                    object? obj,
                                    Type elementType)
{
    // Check if the object is null
    if (obj == null)
    {
        if (elementType == typeof(string) || elementType.IsPrimitive)
        {
            writer.WriteLine(_nullRepresentation);
            return;
        }

        // Attempt to create a new instance if it is a complex type
        // Assuming you know the type or have a way to get it
        obj = Activator.CreateInstance(elementType);
    }

    // Now serialize the object
    if (obj is string || obj!.GetType().IsPrimitive)
    {
        writer.WriteLine(obj.ToString());
    }
    else
    {
        // Write each property on a new line
        var properties = obj.GetType().GetProperties();
        foreach (var property in properties)
        {
            var value = 
                property.GetValue(obj)?.ToString() ?? _nullRepresentation;
            writer.WriteLine(value);
        }
    }
}

SerializeObject Method

Important. Values in the array must be properly initialized before calling the save methods, especially when working with arrays of complex or custom types. If not properly initialized, null values in objects may lead to incorrect written values in the text file, as the object will not be serialized with all its properties.

Loading Arrays from Text Files

The method for loading arrays is LoadFromTextFile.

#region Load From Text File

/// <summary>
/// Loads data from a specified text file and populates the single or multi-dimensional array.
/// Each line in the file corresponds to a single element of the array.
/// </summary>
/// <param name="array">The single or multi-dimensional array to populate.</param>
/// <param name="filePath">The path of the file from which to load the array.</param>
/// <param name="encoding">Encoding used for reading the text file.</param>
/// <exception cref="ArgumentNullException">Thrown when the array is null.</exception>
/// <exception cref="ArgumentException">Thrown when the file path is null or whitespace.</exception>
public static void LoadFromTextFile(this Array array,
                                    string filePath,
                                    Encoding? encoding = null)
{
    // Validate the array and file path before proceeding.
    ValidateArrayAndFilePath(array, filePath);

    try
    {
        // Start the process of loading the array from the text file.
        LoadArrayFromTextFile(array, filePath, encoding);
    }
    catch (Exception ex)
    {
        // Throw an exception if an error occurs during file reading.
        throw new ArgumentException(
            $"An error occurred while loading from file: {ex.Message}");
    }
}

LoadFromTextFile Method

Internal Method for Loading

 /// <summary>
 /// Loads the single or multi-dimensional array from the specified file.
 /// Each line in the file corresponds to a single element of the array.
 /// </summary>
 /// <param name="array">The single or multi-dimensional array to populate.</param>
 /// <param name="filePath">The path of the file from which to load the array.</param>
 /// <param name="encoding">Encoding used for reading the text file.</param>
 private static void LoadArrayFromTextFile(Array array,
                                           string filePath,
                                           Encoding? encoding)
 {
     try
     {
         // Check encoding and set the default encoding if not specified.
         // Default encoding is UTF-8.
         encoding ??= Encoding.UTF8;

         // Read all lines from the specified file using the specified encoding.
         var lines = File.ReadAllLines(filePath, encoding);

         // Get the element type of the array.
         var elementType = array.GetType().GetElementType();

         // Validate that the number of lines read matches
         // the total number of elements in the array.
         // This also accounts for complex types with multiple properties.
         ValidateArrayAndFileLength(array.Length, lines.Length, elementType!);

         // Fill the array with the values from the lines.
         var lineIndex = 0;
         FillArray(array,
                   lines,
                   ref lineIndex,
                   new int[array.Rank],
                   0,
                   elementType!);
     }
     catch (Exception ex)
     {
         // Throw an exception if an error occurs during file reading.
         throw new ArgumentException(
             $"An error occurred while loading from file: {ex.Message}");
     }
 }

LoadArrayFromTextFile Method

Filling the Array with Loaded Values

 /// <summary>
 /// Fills the single or multi-dimensional array with values from the lines.
 /// </summary>
 /// <param name="array">The single or multi-dimensional array to populate.</param>
 /// <param name="lines">The lines from the file.</param>
 /// <param name="lineIndex">The current line index being processed.</param>
 /// <param name="indices">Current indices for the single or multi-dimensional array.</param>
 /// <param name="currentDimension">The current dimension being processed.</param>
 /// <param name="elementType">The type of elements in the array.</param>
 private static void FillArray(Array array,
                               string[] lines,
                               ref int lineIndex,  // Pass lineIndex by reference
                               int[] indices,
                               int currentDimension,
                               Type elementType)
 {
     // If we reach the last dimension, fill the values directly.
     if (currentDimension == array.Rank - 1)
     {
         for (int i = 0; i < array.GetLength(currentDimension); i++)
         {
             indices[currentDimension] = i;
             if (lineIndex < lines.Length)
             {
                 // Deserialize object from consecutive lines
                 object? deserializedValue = DeserializeObject(lines,
                                                               ref lineIndex,
                                                               elementType);

                 array.SetValue(deserializedValue, indices);
             }
         }
     }
     else
     {
         // Recur for the next dimension.
         for (int i = 0; i < array.GetLength(currentDimension); i++)
         {
             indices[currentDimension] = i;
             FillArray(array,
                       lines,
                       ref lineIndex,  // Pass lineIndex by reference
                       indices,
                       currentDimension + 1,
                       elementType);
         }
     }
 }

 #endregion

FillArray Method

Deserialization

/// <summary>
/// Deserializes an object from the lines.
/// </summary>
/// <param name="lines">The lines from the file.</param>
/// <param name="lineIndex">The current line index being processed.</param>
/// <param name="targetType">The type of the object to deserialize.</param>
/// <returns>The deserialized object.</returns>
private static object? DeserializeObject(string[] lines,
                                         ref int lineIndex,
                                         Type targetType)
{
    if (lineIndex >= lines.Length)
    {
        return null; 
        // or throw an exception, based on your design choice
    }

    if (targetType == typeof(string))
    {
        // Check if the current line is "null", and return null if it is
        if (lines[lineIndex] == _nullRepresentation)
        {
            lineIndex++; // Move to the next line
            return null;
        }

        return lines[lineIndex++];
    }

    if (targetType.IsPrimitive)
    {
        // Check if the current line is "null", and return null if it is
        if (lines[lineIndex] == _nullRepresentation)
        {
            lineIndex++; // Move to the next line
            return null;
        }

        return Convert.ChangeType(lines[lineIndex++], targetType);
    }

    // Create an instance of the target object
    var obj = Activator.CreateInstance(targetType);
    var properties = targetType.GetProperties();

    foreach (var property in properties)
    {
        if (lineIndex < lines.Length)
        {
            // Read the current line
            string lineValue = lines[lineIndex++];

            // If the value is represented as 'null',
            // assign null to the property
            if (lineValue == _nullRepresentation)
            {
                property.SetValue(obj, null);
            }
            else
            {
                // Get the property type
                Type propertyType = property.PropertyType;

                // Check if the property type is nullable
                bool isNullable = Nullable.GetUnderlyingType(propertyType) != null;

                // Handle non-null values
                Type? targetTypeToConvert = isNullable
                    ? Nullable.GetUnderlyingType(propertyType)
                    : propertyType;

                // Convert the value to the correct type
                var convertedValue = Convert.ChangeType(lineValue, targetTypeToConvert!);

                // Set the property value
                property.SetValue(obj, convertedValue);
            }
        }
        else
        {
            throw new IndexOutOfRangeException(
                $"Not enough lines to deserialize all properties.");
        }
    }

    return obj;
}

DeserializeObject Method

The DeserializeObject method plays a critical role in converting lines from the text file back into the appropriate types for the array.

Validation

Two validation methods ensure data consistency.

Validation of Array Length

Before populating the array, the method ValidateArrayAndFileLength ensures that the number of lines read matches the total expected elements in the array. This validation is crucial for preventing data corruption, especially when dealing with complex objects that may have multiple properties.

/// <summary>
/// Validates the number of lines read from the file 
/// against the total elements in the array, 
/// accounting for custom object properties.
/// </summary>
/// <param name="totalElements">Total number of elements in the array.</param>
/// <param name="lines">Number of lines read from the file.</param>
/// <param name="elementType">The type of elements in the array.</param>
/// <exception cref="ArgumentException">
/// Thrown when the number of lines does not match the number of elements in the array.
/// </exception>
private static void ValidateArrayAndFileLength(int totalElements,
                                               int lines,
                                               Type elementType)
{
    int expectedLines = totalElements;

    // Check if the element type is a custom class (i.e., not primitive or string).
    if (!elementType.IsPrimitive && elementType != typeof(string))
    {
        // For custom types, calculate the expected number of lines
        // as the total number of elements multiplied by the number of properties.
        int propertyCount = elementType.GetProperties().Length;
        expectedLines *= propertyCount;
    }

    // Compare the number of lines in the file with the expected number of lines.
    if (lines != expectedLines)
    {
        throw new ArgumentException(
            $"The number of lines in the file ({lines}) " +
            $"does not match the expected number of lines " +
            $"({expectedLines}) for the array.");
    }
}

Validation of Array and File Path

Before populating or saving the array, the method ValidateArrayAndFilePath ensures that the array and file path are not null.

/// <summary>
/// Validates the input array and file path for null or invalid values.
/// </summary>
/// <param name="array">The array to validate.</param>
/// <param name="filePath">The file path to validate.</param>
/// <exception cref="ArgumentNullException">Thrown when the array is null.</exception>
/// <exception cref="ArgumentException">Thrown when the file path is null or whitespace.</exception>
private static void ValidateArrayAndFilePath(Array array, string filePath)
{
    if (array == null)
    {
        throw new ArgumentNullException(
            nameof(array),
            "Array cannot be null.");
    }

    if (string.IsNullOrWhiteSpace(filePath))
    {
        throw new ArgumentException(
            "File path cannot be null or whitespace.",
            nameof(filePath));
    }
}

Using ArrayIOExtensionsLib in Phone Book

Download PhoneBook Open Source Solution

The PhoneBook application effectively demonstrates how to utilize the ArrayIOExtensionsLib to manage a simple phone book using a file-based approach. By organizing data in a 2D array and handling persistence through text files, the application remains lightweight and straightforward, avoiding the complexities of database management. This design allows users to easily edit and save contact information while ensuring data integrity and accessibility.

Database management

The code implements a Windows Forms application for managing a phone book, utilizing a 2D string array for in-memory storage and a text file for persistent data management, leveraging ArrayIOExtensionsLib for efficient serialization.

using ArrayIOExtensionsLib;

namespace PhoneBook
{
    public partial class Form1 : Form
    {
        #region Properties

        /// <summary>
        /// Path to the phone book file where data is stored.
        /// </summary>
        private const string PhoneBookFilePath = "PhoneBook.txt";

        /// <summary>
        /// Maximum number of entries allowed in the phone book.
        /// </summary>
        private const int MaxEntries = 10000;

        /// <summary>
        /// Number of columns representing the data fields: 
        /// FirstName, LastName, PhoneNumber, and Address.
        /// </summary>
        private const int ColumnCount = 4;

        /// <summary>
        /// 2D array to hold phone book data temporarily.
        /// </summary>
        private string?[,] PhoneBookData = new string?[MaxEntries, ColumnCount];

        #endregion

        /// <summary>
        /// Initializes the form 
        /// and loads the phone book data into the DataGridView.
        /// </summary>
        public Form1()
        {
            InitializeComponent();
            InitializePhoneBook();
        }

        /// <summary>
        /// Loads the phone book data from a file into the DataGridView. 
        /// If the file does not exist, a new file is created from
        /// the 2D array with default values.
        /// </summary>
        private void InitializePhoneBook()
        {
            // Initialize the array
            // with correct initial or default values
            for (int i = 0; i < MaxEntries; i++)
            {
                for (int j = 0; j < ColumnCount; j++)
                {
                    PhoneBookData[i, j] = default!;
                }
            }

            Table_DataGridView.Rows.Clear();
            Table_DataGridView.RowCount = MaxEntries;

            if (File.Exists(PhoneBookFilePath))
            {
                LoadPhoneBookData();
            }
            else
            {
                // If no file exists,
                // initialize a new phone book file
                SavePhoneBookData();
            }

            DisplayDataInGridView();
        }

        /// <summary>
        /// Saves the phone book data to the text file.
        /// </summary>
        private void SavePhoneBookData()
        {
            PhoneBookData.SaveToTextFile(filePath: PhoneBookFilePath);
        }

        /// <summary>
        /// Loads the phone book data from text file.
        /// </summary>
        private void LoadPhoneBookData()
        {
            PhoneBookData.LoadFromTextFile(filePath: PhoneBookFilePath);
        }

        /// <summary>
        /// Updates the 2D array with data from the DataGridView 
        /// and saves the updated data to the file.
        /// Triggered when a user finishes editing a cell in the DataGridView.
        /// </summary>
        private void Table_DataGridView_CellEndEdit(object sender, DataGridViewCellEventArgs e)
        {
            // Save the edited cell value to the array
            PhoneBookData[e.RowIndex, e.ColumnIndex] =
                Table_DataGridView.Rows[e.RowIndex].Cells[e.ColumnIndex].Value?.ToString();

            SavePhoneBookData();
        }

        /// <summary>
        /// Copies the data from the 2D array into the DataGridView for display.
        /// </summary>
        private void DisplayDataInGridView()
        {
            for (int row = 0; row < MaxEntries; row++)
            {
                for (int col = 0; col < ColumnCount; col++)
                {
                    Table_DataGridView.Rows[row].Cells[col].Value = PhoneBookData[row, col];
                }
            }
        }
    }
}

Moving from 2D String Arrays to Object-Oriented Data Management

In the above version of our Phone Book application, we relied on a simple 2D string array to store and manipulate Phone Book data. Each entry (row) in the array represented a record with values for FirstName, LastName, PhoneNumber, and Address. This approach worked but had limitations, particularly when it came to maintaining code readability, flexibility, and scalability.

In the updated version, we have transitioned to using a PhoneBookEntry class, which represents a single entry in the phone book as an object. This shift provides several key benefits:

  1. Code Readability: With named properties like FirstName, LastName, PhoneNumber, and Address, it's much easier to understand what each field represents. There's no need to remember which column index refers to which piece of data, as was the case with the 2D array.
  2. Improved Flexibility: By using a class, we can easily add new fields (e.g., an email address or birthdate) without having to modify array-based logic across the entire application. This makes the solution more adaptable for future needs.
  3. Better Data Handling: Managing individual entries as objects allows for more sophisticated operations, like validation, sorting, or filtering, which would have been cumbersome with raw string arrays.
  4. IMPORTANT: Values in the array must be properly initialized before calling the save methods, especially when working with arrays of complex or custom types. If not properly initialized, null values in objects may lead to incorrect written values in the text file, as the object will not be serialized with all its properties.

Here is the definition of the new PhoneBookEntry class.

namespace PhoneBook
{
    public class PhoneBookEntry
    {
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        public string? PhoneNumber { get; set; }
        public string? Address { get; set; }
    }
}

And updated code for the main class.

using ArrayIOExtensionsLib;

namespace PhoneBook
{
    public partial class Form1 : Form
    {
        #region Properties

        /// <summary>
        /// Path to the phone book file where data is stored.
        /// </summary>
        private const string PhoneBookFilePath = "PhoneBook.txt";

        /// <summary>
        /// Maximum number of entries allowed in the phone book.
        /// </summary>
        private const int MaxEntries = 10000;

        /// <summary>
        /// 1D array to hold phone book data temporarily.
        /// </summary>
        private PhoneBookEntry[] PhoneBookData = new PhoneBookEntry[MaxEntries];

        #endregion

        /// <summary>
        /// Initializes the form 
        /// and loads the phone book data into the DataGridView.
        /// </summary>
        public Form1()
        {
            InitializeComponent();
            InitializePhoneBook();
        }

        /// <summary>
        /// Loads the phone book data from a file into the DataGridView. 
        /// If the file does not exist, a new file is created.
        /// </summary>
        private void InitializePhoneBook()
        {
            // Initialize the 1D array with empty PhoneBookEntry objects
            for (int i = 0; i < MaxEntries; i++)
            {
                PhoneBookData[i] = new PhoneBookEntry();
            }

            Table_DataGridView.Rows.Clear();
            Table_DataGridView.RowCount = MaxEntries;

            if (File.Exists(PhoneBookFilePath))
            {
                LoadPhoneBookData();
            }
            else
            {
                // If no file exists,
                // initialize a new phone book file
                SavePhoneBookData();
            }

            DisplayDataInGridView();
        }

        /// <summary>
        /// Saves the phone book data to the text file.
        /// </summary>
        private void SavePhoneBookData()
        {
            PhoneBookData.SaveToTextFile(filePath: PhoneBookFilePath);
        }

        /// <summary>
        /// Loads the phone book data from the text file.
        /// </summary>
        private void LoadPhoneBookData()
        {
            PhoneBookData.LoadFromTextFile(filePath: PhoneBookFilePath);
        }

        /// <summary>
        /// Updates the 1D array with data from the DataGridView 
        /// and saves the updated data to the file.
        /// Triggered when a user finishes editing a cell in the DataGridView.
        /// </summary>
        private void Table_DataGridView_CellEndEdit(object sender, DataGridViewCellEventArgs e)
        {
            // Save the edited cell value to the array
            switch (e.ColumnIndex)
            {
                case 0:
                    PhoneBookData[e.RowIndex].FirstName = 
                        Table_DataGridView[e.ColumnIndex, e.RowIndex].Value?.ToString();
                    break;
                case 1:
                    PhoneBookData[e.RowIndex].LastName = 
                        Table_DataGridView[e.ColumnIndex, e.RowIndex].Value?.ToString();
                    break;
                case 2:
                    PhoneBookData[e.RowIndex].PhoneNumber = 
                        Table_DataGridView[e.ColumnIndex, e.RowIndex].Value?.ToString();
                    break;
                case 3:
                    PhoneBookData[e.RowIndex].Address = 
                        Table_DataGridView[e.ColumnIndex, e.RowIndex].Value?.ToString();
                    break;
            }

            SavePhoneBookData();
        }

        /// <summary>
        /// Copies the data from the 2D array into the DataGridView for display.
        /// </summary>
        private void DisplayDataInGridView()
        {
            for (int row = 0; row < MaxEntries; row++)
            {
                Table_DataGridView[0, row].Value = PhoneBookData[row].FirstName;
                Table_DataGridView[1, row].Value = PhoneBookData[row].LastName;
                Table_DataGridView[2, row].Value = PhoneBookData[row].PhoneNumber;
                Table_DataGridView[3, row].Value = PhoneBookData[row].Address;
            }
        }
    }
}

By encapsulating the data for each entry in this class, we now have a more robust and maintainable codebase. This is especially useful for saving and loading the phone book data, where each entry can be serialized or deserialized as an object rather than handling each piece of data individually as a string.

This update is a key step toward improving the overall structure and maintainability of the Phone Book application, setting the stage for future enhancements.

Conclusion

The ArrayIOExtensionsLib class provides a robust and efficient way to manage data in applications like the Phone Book by simplifying the saving and loading of single and multi-dimensional arrays. This approach eliminates the need for complex database setups while still allowing for effective data storage and retrieval.

With its straightforward extension methods, the library ensures that developers can easily handle array data, including the management of null values, through a simple and intuitive API. By leveraging this library, applications can maintain data integrity and provide a seamless user experience.