Team LiB
Previous Section Next Section

File-Based Data

File-based data includes normal text files, .ini files, binary files, XML files, and so forth. Basically, file-based data is anything that resides on a disk and is not highly specialized like a database file.

Since Windows 95 came out with its registry-based storage system, .ini files have been officially declared obsolete by Microsoft. As you well know, though, .ini files have not been abandoned by programmers, and even Microsoft uses them extensively. .NET and XML may actually put .ini files to bed for good, however.

XML and .ini configuration files are a subject unto themselves, and I discuss them in detail in Chapter 9. The files I discuss in this chapter are normal text and binary files.

You saw the use of text-based files in Chapter 5. That was just the tip of the iceberg.

File I/O and Streams

What is a stream? If you are thinking about fishing, you need to concentrate harder on this book. The water analogy is not that far off, however. If you were to put a piece of wood in an unknown stream, where would it end up? Would it end up in a lake, a pond, or perhaps the ocean? You would not know, and the action of throwing the piece of wood in the stream has nothing to do with where it ends up.

The same is true for a stream in .NET. A stream is a baseless form of storage. You can write to a stream, and the information could end up in a database, a file, memory, or perhaps the Internet. So far, you have seen file-based I/O; I show you stream-based I/O next.

Start a new Windows project in either C# or VB. Mine is called "Union." You hard-core C programmers should get a premonition about this example by the name I used for it. Anyway, this is basically a GUI-less example. I show you how to write data to a stream and read data from a stream. The stream will be attached to a few different methods of storage. For this example I use binary I/O. Text I/O is much the same, if not easier.

First make sure you have the following namespaces.

C#

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.InteropServices;
using System.IO;

VB

Imports System
Imports System.Drawing
Imports System.Collections
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Data
Imports System.Runtime.InteropServices
Imports System.IO

Start out with some class variables, as follows.

C#

      #region class local variables

      public struct SalaryBenefits
      {
        private int mVacationDays;
        private int mSickDays;
        public int VacationDays
      {
        get{return mVacationDays;}
        set{mVacationDays = value;}
      }
      public int SickDays
      {
        get{return mSickDays;}
        set{mSickDays = value;}
      }
      public int DaysOff {get{return mSickDays + mVacationDays;}}
    }

    FileStream FS;
    BinaryReader BR;
    BinaryWriter BW;

    private byte[] Buffer = new byte[1000];

    #endregion

VB

#Region "class local variables"

  Public Structure SalaryBenefits
    Private mVacationDays As Integer
    Private mSickDays As Integer
    Public Property VacationDays() As Integer
      Get
        Return mVacationDays
      End Get
      Set(ByVal Value As Integer)
        mVacationDays = Value
      End Set
    End Property
    Public Property SickDays() As Integer
      Get
        Return mSickDays
      End Get
      Set(ByVal Value As Integer)
        mSickDays = Value
      End Set
    End Property
    Public ReadOnly Property DaysOff() As Integer
      Get
        Return mSickDays + mVacationDays
      End Get
    End Property
  End Structure

  Dim FS As FileStream
  Dim BR As BinaryReader
  Dim BW As BinaryWriter
  Dim SR As StreamReader
  Dim SW As StreamWriter

  Private Buffer(1000) As Byte

#End Region

I include a structure here as a way to store internal data. I will be reading and writing vacation and sick days for an employee. The last variable in this list is an array of bytes. You experienced programmers can probably guess what this variable is for. The rest of you will have to wait and see.

Add the following region of code to your form. This code reads and writes binary data to a file.

C#

    #region Read/Write File via normal way

    private void WriteNormalBinary()
    {
      FS = new FileStream("SalaryFile", FileMode.OpenOrCreate);
      BW = new BinaryWriter(FS);

      SalaryBenefits x = new SalaryBenefits();
      x.VacationDays = 82;
      x.SickDays = 31;
      BW.Write(x.VacationDays);
      BW.Write(x.SickDays);
      BW.Flush();
      BW.Close();

    }
    private void ReadNormalBinary()
    {
      FS = new FileStream("SalaryFile", FileMode.Open);
      BR = new BinaryReader(FS);

      SalaryBenefits y = new SalaryBenefits();
      y.VacationDays = BR.ReadInt32();
      y.SickDays = BR.ReadInt32();
    }

    #endregion

VB

#Region "Read/Write File via normal way"

  Private Sub WriteNormalBinary()
    FS = New FileStream("SalaryFile", FileMode.OpenOrCreate)
    BW = New BinaryWriter(FS)

    Dim x As SalaryBenefits = New SalaryBenefits()
    x.VacationDays = 82
    x.SickDays = 31
    BW.Write(x.VacationDays)
    BW.Write(x.SickDays)
    BW.Flush()
    BW.Close()
  End Sub

  Private Sub ReadNormalBinary()
    FS = New FileStream("SalaryFile", FileMode.Open)
    BR = New BinaryReader(FS)

    Dim y As SalaryBenefits = New SalaryBenefits()
    y.VacationDays = BR.ReadInt32()
    y.SickDays = BR.ReadInt32()
  End Sub

#End Region

Add this code to your Form_Load routine.

C#

    private void Form1_Load(object sender, System.EventArgs e)
    {
      WriteNormalBinary();
      ReadNormalBinary();

    }

VB

  Private Sub Form1_Load(ByVal sender As System.Object, _
                         ByVal e As System.EventArgs) Handles MyBase.Load

    WriteNormalBinary()
    ReadNormalBinary()

  End Sub

Step through the code with your debugger. Here is what happens during the write:

  1. A file stream is created and a binary writer is wrapped around it.

  2. An instance of the Benefits structure is created and filled with data.

  3. Each property of the structure is written to the file.

The read operation is just the reverse. I wrap a file stream in a binary reader and read each value into the structure. What would happen if I read the values in reverse order? Well, because both values are the same data type (integers), the numbers would just be switched around. However, if one of the values was an integer and the other was a double and you read them in reverse, you would not get anything predictable. In fact, you may get an error.

Remember, you need to know what you are doing with binary flat files like this. The advantage of a binary flat file over a database for small amounts of data is speed. Reading and writing flat binary files is incredibly fast.

Want to go faster? How about reading and writing to memory? Nothing is faster than that.[4] Add the following region of code to your form.

C#

    #region read/write to memory stream normal way

    private void WriteMemNormalBinary()
    {
      MemoryStream MS = new MemoryStream(Buffer);
      BW = new BinaryWriter(MS);

      SalaryBenefits x = new SalaryBenefits();
      x.VacationDays = 36;
      x.SickDays = 17;
      BW.Write(x.VacationDays);
      BW.Write(x.SickDays);
      BW.Flush();
      BW.Close();

    }

    private void ReadMemNormalBinary()
    {
      MemoryStream MS = new MemoryStream(Buffer);
      BR = new BinaryReader(MS);

      SalaryBenefits y = new SalaryBenefits();
      y.VacationDays = BR.ReadInt32();
      y.SickDays = BR.ReadInt32();
    }
#endregion

VB

#Region "read/write to memory stream normal way"

  Private Sub WriteMemNormalBinary()
    Dim MS As MemoryStream = New MemoryStream(Buffer)
    BW = New BinaryWriter(MS)

    Dim x As SalaryBenefits = New SalaryBenefits()
    x.VacationDays = 36
    x.SickDays = 17
    BW.Write(x.VacationDays)
    BW.Write(x.SickDays)
    BW.Flush()
    BW.Close()
  End Sub

  Private Sub ReadMemNormalBinary()
    Dim MS As MemoryStream = New MemoryStream(Buffer)
    BR = New BinaryReader(MS)

    Dim y As SalaryBenefits = New SalaryBenefits()
    y.VacationDays = BR.ReadInt32()
    y.SickDays = BR.ReadInt32()
  End Sub
#End Region

Call these two functions in the Form_Load event. Here is what it should look like now.

C#

    private void Form1_Load(object sender, System.EventArgs e)
    {
      WriteNormalBinary();
      ReadNormalBinary();

      WriteMemNormalBinary();
      ReadMemNormalBinary();
    }

VB

  Private Sub Form1_Load(ByVal sender As System.Object, _
                        ByVal e As System.EventArgs) Handles MyBase.Load

    WriteNormalBinary()
    ReadNormalBinary()

    WriteMemNormalBinary()
    ReadMemNormalBinary()
  End Sub

The buffer that I created in the variables section is what I use to store my data. This is very interesting because it acts as virtual storage. Remember the RAM disk days of DOS?

Note 

What is DOS, you say? For those of us old enough to remember, a RAM disk was used extensively to hold data for quick retrieval. I used to know a guy who would transfer a goodly chunk of his 20MB hard drive to RAM disk upon start-up just to get the extra speed out of his IBM 20MHz 286.

Note that the code to read and write is exactly the same once I have instantiated the stream. In fact, if I called the memory stream the same name as the file stream, the code would differ in only one line. This is the power of streams. Once you have instantiated it, your code does not need to care where the data is going.

I encourage you to use streams over basic file I/O, as streams provide a more flexible way of handling data. It is no big deal to encapsulate the reading and writing of data in a class and just instantiate the class with the stream you want the data to go to.

A Better Database

Let me tell you a story. A long time ago, a young programmer (yours truly) wanted a faster database than was available at the time (the time being the days of the 386 computer). This programmer decided to write his own binary database using routines written in C. After much trial and error, this programmer used Unions to perform high-speed data transfer to and from the database. When I was playing around with file I/O in .NET, I wanted to see if I could do the same sort of thing. The answer is kind of.

For those of you who do not know what a Union is, here is the long answer. Remember the Benefits structure? It had two variables. Whenever I wanted to write the data in the structure to the stream, I needed to call the write function twice, once for each variable. What if I had 20 variables? The code to write this stream would get very large.

Suppose I could arrange the structure so that I only needed to write one variable to the stream, and this variable would automatically contain the information in all the other variables. A Union is a data structure in which each variable resides in the same address space. Figure 6-11 shows a normal memory map of sequential data stored in a structure.

Click To expand
Figure 6-11: Sequential data stored in a structure

A Union allows the kind of data mapping shown in Figure 6-12.

Click To expand
Figure 6-12: An overlapping data structure

If I was able to achieve the structure shown in Figure 6-12, I would need to save only one piece of data to the stream. This would be the double value. If I read back the double value, I would then get the two integer values.

To achieve this type of data structure, I need to know quite a bit about the sizes of internal data. I also need to be sure that the size of an integer does not change from language to language as I use this structure. For instance, I know that an integer in .NET is 4 bytes and a double is 8 bytes. This is very convenient, as I can fit two integers in the space of a double. What if I used this .NET structure in a VB 6.0 program? An integer in VB 6.0 is 2 bytes. I would need to pass in a VB 6.0 long data type, which is 4 bytes.

So now that I have explained a Union, how do you go about making one? Add this structure to your class variables section.

C#

    //Must use only value types. Cannot mix in reference types
    [ StructLayout( LayoutKind.Explicit )]
      public struct UnionBenefits
    {
      [ FieldOffset(0)]
      public int      VacationDays;
      [ FieldOffset(4)]
      public int      SickDays;
      [ FieldOffset(0)]
      public double   DaysOff;
    }

VB

  <StructLayout(LayoutKind.Explicit)> _
  Public Structure UnionBenefits
    <FieldOffset(0)> Public VacationDays As Integer
    <FieldOffset(4)> Public SickDays As Integer
    <FieldOffset(0)> Public DaysOff As Double
  End Structure

Interesting, eh? The Interop services namespace defines the StructLayout attributes. As you can see here, I lay out the VacationDays variable at offset 0. Because this is an integer, I lay out the next variable, SickDays, at offset 4. An integer takes 4 bytes. This makes them sequential. The next variable is a double, which is 8 bytes. I lay this out at offset 0. This variable overlays the two integers exactly, just like what is shown in Figure 6-12.

Add the following code regions to your form.

C#

    #region Read/Write File via fake union

    private void WriteViaUnion()
    {
      UnionBenefits x = new UnionBenefits();
      x.SickDays = 5;
      x.VacationDays = 15;

      FS = new FileStream("UnionFile", FileMode.OpenOrCreate);
      BW = new BinaryWriter(FS);
      BW.Write(x.DaysOff);
      BW.Flush();
      BW.Close();

    }

    private void ReadViaUnion()
    {
      FS = new FileStream("UnionFile", FileMode.Open);

      UnionBenefits y = new UnionBenefits();
      BR = new BinaryReader(FS);
      y.DaysOff = BR.ReadDouble();

    }
    #endregion

    #region read/write to memory stream via fake union

    private void WriteMemViaUnion()
    {
      UnionBenefits x = new UnionBenefits();
      x.SickDays = 33;
      x.VacationDays = 66;

      MemoryStream MS = new MemoryStream(Buffer);
      BW = new BinaryWriter(MS);
      BW.Write(x.DaysOff);
      BW.Flush();
      BW.Close();

    }

    private void ReadMemViaUnion()
    {
      MemoryStream MS = new MemoryStream(Buffer);

      UnionBenefits y = new UnionBenefits();
      BR = new BinaryReader(MS);
      y.DaysOff = BR.ReadDouble();

    }

    #endregion

VB

#Region "Read/Write File via fake union"

  Private Sub WriteViaUnion()
    Dim x As UnionBenefits = New UnionBenefits()
    x.SickDays = 5
    x.VacationDays = 15

    FS = New FileStream("UnionFile", FileMode.OpenOrCreate)
    BW = New BinaryWriter(FS)
    BW.Write(x.DaysOff)
    BW.Flush()
    BW.Close()
  End Sub

  Private Sub ReadViaUnion()
    FS = New FileStream("UnionFile", FileMode.Open)

    Dim y As UnionBenefits = New UnionBenefits()
    BR = New BinaryReader(FS)
    y.DaysOff = BR.ReadDouble()

  End Sub

#End Region

#Region "read/write to memory stream via fake union"

  Private Sub WriteMemViaUnion()
    Dim x As UnionBenefits = New UnionBenefits()
    x.SickDays = 33
    x.VacationDays = 66

    Dim MS As MemoryStream = New MemoryStream(Buffer)
    BW = New BinaryWriter(MS)
    BW.Write(x.DaysOff)
    BW.Flush()
    BW.Close()

  End Sub

  Private Sub ReadMemViaUnion()
    Dim MS As MemoryStream = New MemoryStream(Buffer)

    Dim y As UnionBenefits = New UnionBenefits()
    BR = New BinaryReader(MS)
    y.DaysOff = BR.ReadDouble()

  End Sub

#End Region

Of course, you will need to call the following methods.

C#

    private void Form1_Load(object sender, System.EventArgs e)
    {
      WriteNormalBinary();
      ReadNormalBinary();

      WriteViaUnion();
      ReadViaUnion();

      WriteMemNormalBinary();
      ReadMemNormalBinary();

      WriteMemViaUnion();
      ReadMemViaUnion();

    }

VB

  Private Sub Form1_Load(ByVal sender As System.Object, _
                        ByVal e As System.EventArgs) Handles MyBase.Load

    WriteNormalBinary()
    ReadNormalBinary()

    WriteViaUnion()
    ReadViaUnion()

    WriteMemNormalBinary()
    ReadMemNormalBinary()

    WriteMemViaUnion()
    ReadMemViaUnion()

  End Sub

Here is what happens:

  • SickDays and VacationDays are initialized.

  • The DaysOff variable is saved to the stream.

That's it. I needed to save and retrieve only one variable. Figure 6-13 shows the variable list as I am about to save the structure.

Click To expand
Figure 6-13: The overlapped benefits structure

You can see from Figure 6-13 that the DaysOff variable means nothing at all if you interpret it as a double.

There are some caveats to this method of data storage. The main one is that you cannot overlay value data types with reference data types. That is, you cannot overlay a string over several integers. This is the only major drawback that I see. There is actually a way around this that involves using two structures and overloaded methods. If you are interested in this using this type of data storage, I leave it up to you to figure out the workaround.

[4]Except maybe The Flash!


Team LiB
Previous Section Next Section