Using File I/O

Desktop Framework developers will be aware that programmatic access to file I/O is found in the System.IO namespace. This is also true of the Compact Framework, where I/O is encapsulated by abstracting the concept of a stream used to read and write data from the "backing store," or a medium used to store the data. Because of this abstraction, you can think of the System.IO namespace as consisting of three logical components, as shown in Figure 3-1.

Figure 3-1. File I/O. The major classes of the System.IO namespace are broken down into components, including (a) streams, (b) readers and writers, and (c) file system classes.

graphics/03fig01.gif

For eVB developers, this programming model may take a little getting used to because the Compact Framework does not support the FileSystem class in the Microsoft.VisualBasic namespace that includes analogs to many of the statements and functions historically used by VB developers, such as FileOpen, Input, LineInput, and so on.

graphics/key point_icon.gif

Foundational not only to file access but to dealing with all types of I/O is the Stream class found in the System.IO namespace. Stream is a base class that represents the stream of bytes to be read from or written to a backing store. As a result, it includes methods to perform these operations both synchronously (Read, Write) and asynchronously (BeginRead, BeginWrite, EndRead, EndWrite). However, while these methods are present, if developers attempt to use the asynchronous methods in the Compact Framework, a NotSupportedException will be thrown. The Stream also exposes methods that manipulate the current position in the Stream, such as Seek, and a variety of properties to interrogate the capabilities of the Stream, such as CanRead, CanSeek, and CanWrite. Because Stream is a base class, the System.IO namespace includes two classes that inherit from it to support specific backing stores. The FileStream class supports stream operations against physical files, whereas the MemoryStream class supports stream access to physical memory. In addition, the System.Net.Sockets namespace implements the NetworkStream class to provide the underlying stream of data for network access.[2] Obviously, by deriving these classes from Stream, developers can take advantage of polymorphism to write more reusable and maintainable code.

[2] The BufferedStream class is not supported in the Compact Framework.

The second component of System.IO includes the Reader and Writer classes. As the names imply, these classes are used to read and write bytes to and from a Stream in a particular way. Although developers can use the Read and Write methods of the Stream classes directly, doing so means having to read and write data as byte arrays using offsets.

There are two basic divisions in the Reader/Writer classes that include the TextReader and TextWriter classes and the BinaryReader and BinaryWriter classes. TextReader and TextWriter are base classes that read and write individual text characters to a stream, while their analogs read and write primitive types in binary. In turn, the TextReader and TextWriter serve as the base classes for the StreamReader and StringReader and the StreamWriter and StringWriter classes, respectively. The StreamReader and StreamWriter classes read and write a variety of data types (including text) to a Stream in a particular encoding, whereas the StringReader and StringWriter simply read and write from strings using a StringBuilder from the System.Text namespace.

The final component of System.IO includes the various classes that deal specifically with the file system and interact with the FileStream class. As Figure 3-1 shows, these include the DirectoryInfo and FileInfo classes derived from FileSystemInfo used to manipulate files and directories in conjunction with a FileStream. In addition, the sealed Directory, File, and Path classes aid in the creation of file system objects, in addition to providing methods to copy, delete, open, and move files and directories.[3]

[3] The FileSystemWatcher class that accompanies these classes in the desktop Framework is not included in the Compact Framework.

Reading and Writing Text Files

To illustrate the use of the System.IO namespace to read and write individual files, consider the code in Listings 3-1 and 3-2, where the methods write and read baseball box score information to and from a comma-delimited text file, respectively. This is the kind of code that a developer would write for a stand-alone application that allowed users to score a baseball game.

Listing 3-1 Writing to a Text File. This listing shows how a developer would use the FileStream and StreamWriter classes to write to the file system.
Public Sub SaveToCSV(ByVal fileName As String)

    ' Save the current box score to a CSV file

    Dim fs As FileStream
    Dim sr As StreamWriter

    Try
        ' Overwrite file if exists
        fs = New FileStream(fileName, FileMode.Create, _
          FileAccess.Write, FileShare.None)
        ' Associate the stream writer with the file
        sr = New StreamWriter(fs, System.Text.Encoding.Default, 1024)
    Catch e As IOException
        Throw New Exception("I/O error. Cannot access the file " & _
          fileName & " :" & e.Message)
    End Try

    Try
        ' Write the header
        sr.WriteLine(fileName & " created on " & _
          DateTime.Now.ToShortDateString)
        sr.WriteLine(Me.GameDate & "," & Me.GameTime)

        ' Write visiting team
        sr.WriteLine(Me.Visitor)
        Dim p As PlayerLine
        For Each p In Me.VisitingPlayers
            sr.WriteLine(p.ToString(","))
        Next
        sr.WriteLine("END")

        ' Write home team
        sr.WriteLine(Me.Home)
        For Each p In Me.HomePlayers
            sr.WriteLine(p.ToString(","))
        Next
        sr.WriteLine("END")

        ' Write the line score
        Dim i As Integer
        For i = 1 To Me.VisitingLine.Count
            sr.Write(VisitingLine(i))
            If i < Me.VisitingLine.Count Then
                sr.Write(",")
            End If
        Next
        sr.WriteLine()
        For i = 1 To Me.HomeLine.Count
            sr.Write(HomeLine(i))
            If i < Me.HomeLine.Count Then
                sr.Write(",")
            End If
        Next

    Catch e As Exception
        Throw New ApplicationException("Could not write box score", e)
    Finally
        sr.Close()
    End Try
End Sub
Listing 3-2 Reading from a Text File. This listing shows how a developer would use the FileStream and StreamReader classes to read to a file on the file system.
Public Sub LoadFromCSV(ByVal fileName As String)
    ' Read the box score from a CSV file

    Dim fs As FileStream
    Dim sr As StreamReader

    Try
        ' Read the file
        fs = New FileStream(fileName, FileMode.Open, _
          FileAccess.Read, FileShare.Read)
        ' Associate the stream writer with the file
        sr = New StreamReader(fs, True)
    Catch e As IOException
        Throw New Exception("I/O error. Cannot access the file " & _
          fileName & " :" & e.Message)
    End Try

    Try
        ' Skip the header
        sr.ReadLine()

        Dim info As String = sr.ReadLine()
        Dim gameInfo() As String = info.Split(",")
        Me.GameDate = gameInfo(0)
        Me.GameTime = gameInfo(1)

        ' Read visiting team
        Me.Visitor = sr.ReadLine()
        Dim pstr As String = sr.ReadLine()
        Do While pstr <> "END"
            Dim p As New PlayerLine(pstr, ",")
            Me.VisitingPlayers.Add(p)
            pstr = sr.ReadLine()
        Loop

        ' Read home team
        Me.Home = sr.ReadLine()
        pstr = sr.ReadLine()
        Do While pstr <> "END"
            Dim p As New PlayerLine(pstr, ",")
            Me.HomePlayers.Add(p)
            pstr = sr.ReadLine()
        Loop

        ' Read the line score
        Dim line As String = sr.ReadLine()
        Dim lines() As String = line.Split(",")
        Dim i As Integer
        For i = 0 To lines.Length - 1
            Me.VisitingLine.Add(i + 1, lines(i))
        Next
        line = sr.ReadLine()
        lines = line.Split(",")
        For i = 0 To lines.Length - 1
            Me.HomeLine.Add(i + 1, lines(i))
        Next

    Catch e As Exception
        Throw New ApplicationException( _
          "Could not read in the box score", e)
    Finally
        sr.Close()
    End Try

End Sub

In Listing 3-1 you can see that the SaveToCSV method accepts the filename as a parameter and uses it to overwrite an existing file of the same name by passing the FileMode.Create value to the constructor of the FileStream class. A StreamWriter that points to the stream to write to is then instantiated. Note that the default encoding (in this case UTF-8) is used along with a buffer size of 1K, the default being 4K. If the file cannot be accessed, an IOException will be thrown and the method terminates.

The remainder of the SaveToCSV method uses the overloaded Write and WriteLine methods of the StreamWriter class to write out box score data. In this case the SaveToCSV method exists in a class called Scoresheet that exposes the following public fields:


Public VisitingPlayers As ArrayList
Public HomePlayers As ArrayList
Public HomeLine As ListDictionary
Public VisitingLine As ListDictionary
Public Home, Visitor, GameDate, GameTime As String

The individual player's statistics are stored as instances of the PlayerLine class in the VisitingPlayers and HomePlayers collections. The PlayerLine class contains a ToString method that accepts a delimiter that then creates a delimited string with all of the player's statistics. The end result is a comma-delimited file that contains the entire box score for a baseball game.

NOTE

Calling the Close method of the StreamWriter class in the Finally block ensures that all data is written to the stream and, hence, to the file before it is closed.


In Listing 3-2 the reverse process occurs in the LoadFromCSV method, and the comma-delimited file is loaded into a FileStream object and read with the StreamReader class. In this case developers can rely on the Split method of the String class to create arrays from the delimited data and then parse the arrays into the correct data structure. In fact, the PlayerLine class includes an overloaded constructor that accepts the delimited string, along with the delimiter, and then parses it and loads it into its public properties.

Asynchronous File Access

graphics/key point_icon.gif

As mentioned previously, the Stream class and its descendants, such as FileStream, expose four methods used in the desktop Framework for asynchronous reading and writing. However, these methods throw a NotSupportedException when used in the Compact Framework.

As an alternative, developers can manipulate threads directly using the classes of the System.Threading namespace. Although discussed in more detail in the next chapter, the entire SaveToCSV method shown in Listing 3-1 could be invoked on a background thread as follows:


Dim t As New Thread(AddressOf MySaveToCSV)
outFile = "boxscore.txt"
t.Start()

Doing so allows the user to continue with other useful work while the file is being saved. Of course, because the method used as the address at which to begin the thread cannot accept arguments, the MySaveToCSV method actually makes the call to the SaveToCSV method, passing in the outFile variable as an argument, as shown here:


Public Sub MySaveToCSV()
    s.SaveToCSV(outFile)
End Sub

Unfortunately, the Compact Framework does not support the IsAlive, IsBackground, or ThreadState properties or the Interrupt and Join methods, which could all be used to determine whether the thread was still executing. However, even if all open windows in the application are closed, the thread will continue to execute and the application will not be unloaded until execution completes. If the Exit method of the Application class is called, all windows and the application itself will not be unloaded until the thread completes. This behavior is different from the desktop Framework where threads set with low priorities (BelowNormal or Lowest) will not finish executing if the application is shut down.

Synchronizing Access to Resources

While the SaveToCSV method is executing on the background thread, the developer would also need to ensure that other threads do not access instance data that is critical to the execution of the method. To do so, the developer can use the Monitor class in the System.Threading namespace or the SyncLock and lock statements in VB and C# respectively. However, the recommended way to execute processes on separate threads that do not require access to shared resources is as follows:

  1. Encapsulate the process that is to be run in a class that exposes an entry point used to start the process and instance variables to handle the state.

  2. Create a separate instance of the class.

  3. Set any instance variables required by the process.

  4. Invoke the entry point on a separate thread.

  5. Do not reference the instance variables of the class.


graphics/key point_icon.gif

In many cases it is also desirable to notify the main window running on a foreground thread when a background thread such as that shown earlier has completed. This would be the case, for example, if the LoadFromCSV method were invoked on a background thread that needed to update the UI when the Scoresheet class was loaded. While this functionality was built into asynchronous delegates not accessible in the Compact Framework, this can easily be accomplished with delegates directly. For example, assume that the LoadFromCSV method is to be called on a background thread. The form that makes the call can include a form-level EventHandler delegate declared as follows:


Private UICallback As EventHandler

Then, before the load method is invoked on the thread, the delegate is instantiated to point to a method called LoadUI on the form. This method is responsible for updating controls on the UI with the score sheet data.


UICallback = New EventHandler(AddressOf LoadUI)
Dim t As New Thread(AddressOf MyLoadFromCSV)
inFile = "boxscore.txt"
t.Start()

Finally, when the loading is completed, the MyLoadFromCSV method can invoke the delegate that points to the LoadUI method. The only caveat is that the LoadUI method must use the standard EventHandler delegate that accepts an object (the sender) and an object of type EventArgs.


Public Sub MyLoadFromCSV()
    s.LoadFromCSV(inFile)
    Me.Invoke(UICallback)
End Sub

By invoking the delegate on Me (this in C#, meaning the current form), the Compact Framework ensures that the LoadUI method will be executed safely on the foreground thread. If a developer attempts to update the UI running on the foreground thread directly from code running on the background thread, the application will hang.

Unfortunately, the Compact Framework does not support the overloaded Invoke method, which accepts arguments. However, a developer could wrap this functionality in his or her own class, such as the Invoker class shown in Listing 3-3.

Listing 3-3 Updating the UI with an Invoker Class. This class can be used to update the UI of a form in the Compact Framework and pass it arguments.
Public Delegate Sub UIUpdate(ByVal args() As Object)
Public Class Invoker

    Private _control As Control
    Private _uiUpdate As UIUpdate
    Private _args() As Object

    Public Sub New(ByVal c As Control)
        ' Store the control that is to run the method on its thread
        _control = c
    End Sub

    Public Sub Invoke(ByVal UIDelegate As UIUpdate, _
         ByVal ParamArray args() As Object)
        ' called by the client and passed the delgate that
        ' points to the method to run
        ' as well as the arguments
        _args = args
        _uiUpdate = UIDelegate
        _control.Invoke(New EventHandler(AddressOf _invoke))
    End Sub

    Private Sub _invoke(ByVal sender As Object, ByVal e As EventArgs)
        ' this is now running on the same thread as the control
        ' so freely call the delegate
        _uiUpdate.Invoke(_args)
    End Sub

End Class

Here the client simply needs to create an instance of Invoker and pass it the control (such as the form) on which to execute the method.


Private inv As Invoker
inv = New Invoker(Me)

Then, when the method running on the other thread completes, it can simply call the Invoke method, passing in the delegate that contains the method to update the UI, along with the arguments.


inv.Invoke(New UIUpdate(AddressOf LoadUI), "1", "2")

Obviously, the asynchronous techniques discussed in this section could also apply to working with XML and relational data, covered in the following sections.

Manipulating Files and Directories

The Compact Framework also supports manipulating files and folders directly using the File, FileInfo, DirectoryInfo, and Directory classes in the System.IO namespace. Like their desktop Framework equivalents, these classes allow a developer to enumerate and inspect files and directories and copy, move, and delete them. For example, the method in Listing 3-4 uses these classes to move all the files matching specific criteria to an archive directory relative to the path.

Listing 3-4 Manipulating Files and Directories. This method uses the classes of System.IO to move files from one directory to another.
Public Sub ArchiveFiles(ByVal filePath As String, _
  ByVal criteria As String)

    Dim f As FileInfo
    Dim dDir As DirectoryInfo
    Dim dArchive As DirectoryInfo

    ' Make sure directory exists
    If Not Directory.Exists(filePath) Then
        Throw New ApplicationException("Directory " & filePath & _
           " does not exist")
    Else
        dDir = New DirectoryInfo(filePath)
    End If

    ' Create the archive directory
    dArchive = Directory.CreateDirectory(filePath & _
      Path.DirectorySeparatorChar & "Archive")

    ' Get all the files in the directory
    If criteria Is Nothing Then
       criteria = "*.*"
    End If

    For Each f In dDir.GetFiles(criteria)
        Try
            ' Move the file and delete
            f.MoveTo(dArchive.FullName & _
              Path.DirectorySeparatorChar & f.Name)
         Catch e As Exception
            Throw New ApplicationException("Error on file " & f.Name, e)
        End Try
    Next
End Sub

It is interesting to note that the File and Directory classes are used statically to manipulate objects in the file system, whereas the DirectoryInfo and FileInfo classes represent individual file system entries. In other words, the methods of File and Directory can be used to perform operations on files and directories, whereas classes derived from FileSystemInfo represent specific instances of files and directories. In addition, the File and Directory classes accept String arguments and return arrays of strings when queried for data, for example, using the GetFiles method shown in Listing 3-4, whereas the FileSystemInfo classes accept and return other instances of a FileSystemInfo class. The Path class also is used statically to return platform-independent delimiters and other information as shown through the use of the DirectorySeparatorChar property.[4]

[4] There is also some overlap between the File and FileInfo classes and the FileStream discussed previously. For example, a developer can use the shared methods OpenText, CreateText, or AppendText of the File and FileInfo classes to open a text file as well.

graphics/key point_icon.gif

Although the file and directory classes implement most of the functionality of the desktop Framework, the Directory class's GetCurrentDirectory method throws a NotSupportedException instead of returning the working directory of the application. However, the current directory and other system folders can be retrieved using a simple wrapper class like that shown in Listing 3-5.

Listing 3-5 Finding System Folders. This wrapper class allows easy access to Windows CE system folders and the folder that the application is executing from.
Namespace Atomic.CEUtils

 Public Enum ceFolders As Integer
    PROGRAMS = 2          ' \Windows\Start Menu\Programs
    PERSONAL = 5          ' \My Documents
    STARTUP = 7           ' \Windows\StartUp
    STARTMENU = &HB       ' \Windows\Start Menu
    FONTS = &H14          ' \Windows\Fonts
    FAVORITES = &H16      ' \Windows\Favorites
 End Enum

 Public Class FileSystem

    Private Sub New()
    End Sub

    Private Const MAX_PATH As Integer = 260
    Private Shared _specialFolderPath As String
    Private Shared _documentsFolder As String
    Private Shared _windowsFolder As String
    Private Shared _assemblyFolder As String

    <DllImport("coredll.dll")> _
     Private Shared Function SHGetSpecialFolderPath( _
            ByVal hwndOwner As Integer, _
            ByVal lpszPath As String, _
            ByVal nFolder As ceFolders, _
            ByVal fCreate As Boolean _
            ) As Boolean
     End Function

    Public Shared ReadOnly Property WindowsFolder() As String
        Get
          If _windowsFolder Is Nothing Then
              _windowsFolder = _getWindowsFolder()
          End If
          Return _windowsFolder
        End Get
    End Property

    Public Shared ReadOnly Property DocumentsFolder() As String
        Get
          If _documentsFolder Is Nothing Then
             _documentsFolder = GetSpecialFolderPath(ceFolders.PERSONAL)
          End If
          Return _documentsFolder
        End Get
    End Property

    Public Shared ReadOnly Property RuntimeFolder() As String
        Get
          If _assemblyFolder Is Nothing Then
              Dim a As [Assembly]
              a = System.Reflection.Assembly.GetExecutingAssembly()
              _assemblyFolder = a.GetName().CodeBase
              _assemblyFolder = _assemblyFolder.Substring(0, _
                  _assemblyFolder.LastIndexOf("\"))
            End If
            Return _assemblyFolder
        End Get
    End Property

    Public Shared Function GetSpecialFolderPath( _
       ByVal folder As ceFolders) As String
        Dim sPath As String = New String(" "c, MAX_PATH)
        Dim i As Integer

        Try
            SHGetSpecialFolderPath(0, sPath, folder, False)
            i = sPath.IndexOf(Chr(0))
            If i > -1 Then
                sPath = sPath.Substring(0, i)
            End If
        Catch ex As Exception
            sPath = ex.Message
        End Try
        Return sPath
    End Function

    Private Shared Function _getWindowsFolder() As String
        Dim s As String

        s = GetSpecialFolderPath(ceFolders.STARTMENU)
        Dim i As Integer = s.LastIndexOf("\")
        If i > -1 Then
            s = s.Substring(0, i)
        End If
        Return s
    End Function


 End Class
End Namespace

As you can see in Listing 3-5 the FileSystem class in the Atomic.CEUtils namespace includes shared properties to return the documents and windows folders by calling the Windows CE API function SHGetSpecialFolderPath found in coredll.dll. This is an example of using the PInvoke functionality of the Compact Framework to make direct calls to the underlying operating system. This function is also exposed through the GetSpecialFolderPath method that accepts one of the ceFolders enumerated types. The runtime folder, however, is accessed through the GetExecutingAssembly method of the System.Reflection namespace.

Finally, it is also possible to use the OpenFileDialog class of the System.Windows.Forms namespace to open a dialog from which the user can select a file and return it. There are several differences, however, in its operation in the Compact Framework. For example, although it supports the InitialDirectory property, setting it has no effect, and the dialog will always display all of the documents in the My Documents folder.[5] The folder dropdown list can then be used to filter based on the folder within My Documents. Also, the Compact Framework does not support multifile selection, filtering on read-only files, checking for the file's existence, and opening the file when selected. As a result, typical usage of this class is as follows:

[5] This is also the behavior that occurs when calling the GetOpenFileName Windows CE API function, even if a directory path is specified in the structure passed to the function.


Dim f As New OpenFileDialog

f.Filter = "All files (*.*)|*.*|Scoresheet files (*.scr)|*.scr"
If f.ShowDialog() = DialogResult.OK Then
     _doSomeWork (f.FileName)
End If