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.
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.
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. Obviously, by deriving these classes from Stream, developers can take advantage of polymorphism to write more reusable and maintainable code.
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.
 The FileSystemWatcher class that accompanies these classes in the desktop Framework is not included in the Compact Framework.
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.
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
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.
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.
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:
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.
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.
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.
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.
 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.
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.
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. 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:
 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