When you first start learning VB.NET, one of the first things you may notice is the absence of “traditional” file I/O support in .NET. Microsoft has replaced the classic IO operations by stream operations. A stream is a simple concept that originated in the Unix world.
You can think of stream as a channel through which data flows from your application to a sequential data store (such as a file, a string, a byte array, or another stream), or vice versa. To understand why the traditional file I/O operations were replaced by streams, you must consider that not all data reside in files. Modern applications acquire data from many different data stores, including files, in-memory buffers and the Internet. The stream analogy enables applications to access all these data stores with the same programming model. There’s no need to learn how to use Sockets to access a file on a remote Web server. You can establish a stream between your application and a remote resource and read the bytes as the server sends them.
A stream encapsulates all the operations you can perform against a data store. The big advantage is that after you learn how to deal with streams for one data source, you can apply the same techniques to widely differing data sources. This article primarily focuses on using streams with files, but you’ll see a few examples of using streams with other data stores, such as resources on a remote server, toward the end of this article.
Types of Streams
The Stream class is abstract; you can’t declare a new instance of type Stream in your code. There are five classes in the .NET Framework that derive from the Stream class. These are:
- FileStream. Supports sequential and random access to files
- MemoryStream. Supports sequential and random access to memory buffers
- NetworkStream. Supports sequential access to Internet resources. The NetworkStream resides in the System.Net.Sockets namespace.
- CryptoStream. Supports data encryption and decryption. The CryptoStream resides in the System.Security.Cryptography namespace.
- BufferedStream. Supports buffered access to stream that do not support buffering on their own
Not all streams support exactly the same operations. A stream for reading a local file, for example, can report the length of the file and the current position in the file, with the Length and Position properties, respectively. You can jump to any location in the file with the Seek method. In contrast, a stream for reading a remote file doesn’t support those features. But the stream classes help you differentiate Streams programmatically, by providing CanSeek, CanRead and CanWrite properties. Despite some data-store-dependent differences, the basic methods of all Stream classes let you write data to or read data from the underlying data store.
To work with a local disk file, you use the FileStream class, which lets you move data to and from the stream as arrays of bytes. To make it easier to read and write basic data types, you can use the methods of the BinaryReader and BinaryWriter classes, or the equivalent methods of the StreamReader and StreamWriter classes. All these classes wrap an underlying FileStream and provide methods that make it easier to read and write data in the appropriate format. The BinaryReader/Writer classes use the native form of the basic data types and produce binary files that are not readable by humans. The StreamReader/Writer classes convert basic data types into XML format and produce text files. All the classes work with any type of data, so the distinction between text and binary files is no longer as important as it used to be in classic VB. You can store numbers either as text (in XML format), or in their native format.
VB.NET supports traditional random access files, but it doesn’t really need them.?You can still create files that store structures, and access them by record numbers, as you did with previous versions of Visual Basic using the FileOpen and FileGet functions, but for the most part, the functionality of random access files has been replaced by XML and/or databases. If you are designing new applications and don’t need compatible random access capability you should use the newer .NET capabilities.
No matter which class you decide to use to access a file, you must first create a FileStream object. There are several ways to do that. The simplest method is to specify the file and how it will be opened in the FileStream object’s constructor, which has the following syntax:
Dim fStream As New FileStream(path, _ fileMode, fileAccess)
The path argument contains the full pathname of the file you want to open. The fileMode argument is a member of the FileMode enumeration (see Table 1) that determines how to open (or create) the specified file. The fileAccess argument is a member of the FileAccess enumeration: Read (for reading only), ReadWrite (for reading and writing), and Write (for writing only).determines the read/write access to the file.
Table 1: The Members of the FileMode Enumeration
Opens an existing file and moves to the end of it, orcreates a new file. Use this mode when the file is opened for writing
Creates a new file if the specified file exists, oroverwrites the existing file.
Creates a new file.? If the path argument specifies anexisting file, an exception will be thrown.
Opens an existing file. If the path argument specifies afile that doesn’t exist, an exception will be thrown.
Opens the specified file if it exists, or creates a new one.
Opens the specified? file and resets its size to 0 bytes.
Creating a FileStream object is not the only way to open a file. You can also use one of the various Open methods of the File object (Open, OpenRead, OpenText, OpenWrite). These methods accept the file’s path as argument and return a Stream object:
Dim FS As New FileStream = IO.File.OpenWrite("c:Stream.txt")
Another way to open a file is to use the OpenFile method of the OpenFileDialog and SaveFileDialog controls. With the OpenFile method of these two controls you need not specify any arguments; both methods open the file selected by the user in the dialog. The OpenFile method of the OpenFileDialog control opens the file in read-only mode, whereas the OpenFile method of the SaveFileDialog control opens the file in read/write mode.
The FileStream class supports only the most basic file operation?moving data into or out of files as bytes or arrays of bytes. To use a FileStream instance to write something to a file, you must first convert the data to an array of bytes and then pass it as argument to the FileStream object’s Write method. Likewise, the FileStream object’s Read method returns an array of bytes. You must also specify how many bytes should be read from the file. You probably will not use the FileStream class methods directly often, but it’s worth exploring briefly to see the base capabilities.
After creating a FileStream object, you can call its WriteByte to write a single byte or its Write method to write an array of bytes to the file. The WriteByte method accepts a byte as argument and writes it to the file, and the Write method accepts three arguments: an array of bytes, an offset in the array and the number of bytes to be written to the file. The syntax of the Stream.Write method is:
Write(buffer, offset, count)
The buffer argument is the array containing the bytes to be written to the file, offset is the index of the first byte you want to write from the array, and count is the number of bytes to write. The syntax of the Read method is identical, except that the Read method fills the array buffer with count characters from the file.
Converting even basic data types to bytes is not trivial and you should usually avoid using FileStreams directly; however, if you do plan to use the Stream object to write to a file, you should investigate the GetBytes and GetChars methods of the ASCIIEncoding and UnicodeEncoding classes (part of the System.Text namespace). For example, you can convert a string to an array of bytes with the following code:
Dim buffer() As Byte Dim encoder As New System.Text.ASCIIEncoding() Dim str As String = "This is a line of text" ReDim buffer(str.Length - 1) encoder.GetBytes(str, 0, str.Length, buffer, 0) FS.Write(buffer, 0, buffer.Length)
Notice that you must resize the buffer array to the length of the string you want to convert. To convert an array of bytes returned by the FileStream.Read method, use the GetChars method of the encoder variable.
As you can see, converting data to and from byte arrays is cumbersome. To avoid the conversions and simplify your code, you can use the StreamReader/StreamWriter classes to access text files, and the BinaryReader/BinaryWriter classes to access binary files. The BinaryReader/BinaryWriter classes derive from the Stream class, because they write binary data (bytes) to an underlying stream, but the StreamReader/ StreamWriter classes derive from the TextReader/TextWriter classes respectively, and perform byte encoding conversions automatically.
To read data from a binary file, create an instance of the BinaryReader class. The BinaryReader class’s constructor accepts one argument?a FileStream object representing the file you want to open. You obtain the FileStream by building on the ways you’ve already seen to open a file, such as the File.OpenRead or File.OpenWrite methods:
Dim BR As New IO.BinaryReader(IO.File.OpenRead(path))
The syntax for the BinaryWriter class’s constructor is similar:
Dim BW As New IO.BinaryWriter(IO.File.OpenWrite(path))
The BinaryWriter class exposes Write and WriteLine methods. Both methods accept any of the basic data types as arguments and write the data to the file (the WriteLine method appends a newline character to the end of the data). The BinaryReader class exposes numerous methods for reading data back. The class stores data values in their native format, with no indication of their type, so the program that reads them back should use the appropriate overloaded Read method. The following statements assume that BW is a properly initialized BinaryWriter object, and show how you might write a string, an integer, and a double value to a file:
BW.WriteLine("A String") BW.WriteLine(12345) BW.WriteLine(123.456789999999)
To read the values back, you must use the appropriate methods of a properly initialized BinaryReader object:
Dim s As String = BR.ReadString() Dim i As Int32 = BR.ReadInt32() Dim dbl As Double = BR.ReadDouble()
To access text files, use the StreamReader/StreamWriter classes. The methods are nearly identical. To write text to a file, use either the Write or the WriteLine method. To read the data back, use the Read, ReadLine or ReadToEnd methods. The Read method reads a single character from the stream, ReadLine reads the next text line (up to a carriage-return/line-feed) and ReadToEnd reads all the characters to the end of the file.
You can find more examples of reading from and writing to binary and text files in the section of this article entitled Common File I/O Scenarios.
So far you’ve seen how to save simple data types to a file, and read them back. Most applications don’t store their data in simple variables. Instead, they use complicated structures to store their data, such as arrays, ArrayLists, HashTables and so on. It’s possible to store an entire array to a file with a process called serialization. To do that, you convert the array values to a sequence of bytes, which you can then store to a file. The opposite process is called deserialization.
Serialization is a big topic in .NET, but here’s the basic information you need.. To save an object to file and read it back, you use the Serialize and Deserialize methods of the BinaryFormatter class. First, import the System.RunTime.Serialization.Formatters namespace into your project to avoid typing excessively long statements. The Formatters namespace contains the BinaryFormatter class, which knows how to serialize basic data types in binary format. Create an instance of the BinaryFormatter class and then call its Serialize method, passing two arguments: a writeable FileStream instance for the file where you want to store the serialized object, and the object itself:
Dim BinFormatter As New Binary.BinaryFormatter() Dim R As New Rectangle(10, 20, 100, 200) BinFormatter.Serialize(FS, R)
The Deserialize method of the BinaryFormatter class accepts a single argument?a FileStream instance?deserializes the object at the current position in the FileStream and returns it as an object. You usually cast the deserialized object to the proper type with the CType function. For example, the following statement returns the serialized Rectangle object saved in the preceding code snippet:
Dim R As New Rectangle() R = CType(BinFormatter.Deserialize(FS), Rectangle)
You can also persist objects in text format using the XmlFormatter object. To do so, add a reference to the System.Runtime.Serialization.Formatters.Soap namespace with the Project?> Add Reference command. After doing that, you can create an instance of the SoapFormatter object, which exposes the same methods as the BinaryFormatter object, but serializes objects in XML format. The following statements serialize a Rectangle object in XML format:
Dim FS As New IO.FileStream("c:Rect.xml", IO.FileMode.Create, IO.FileAccess.Write) Dim XMLFormatter As New SoapFormatter() Dim R As New Rectangle(8, 8, 299, 499) XMLFormatter.Serialize(FS, R)
Double-click the file in which the Rectangle object was persisted to open it with Internet Explorer, as shown in Figure 1.
The examples you’ve just seen persist and reinstantiate a Rectangle?a built-in framework object, but the sequence of commands to persist and reinstantiate custom objects is almost identical. See the Persisting Objects section in the examples at the end of this article for an example.
In the last section of the article you’ll find code prototypes for the file operations you’re likely to use most frequently. The simplest, and most common, operation is moving text in and out of text files. Binary files are not commonly used to store individual values; instead, modern applications most often use them to store objects, collections of objects, and other machine-readable data. In the following sections you’ll find code examples for each of these scenarios.
Writing and Reading Text Files
To save text to a file, create a StreamReader object based on a FileStream object for the specific file and then call its Write method passing the text to be written to the file as argument. The following statements prompt the user to specify a file name with a SaveFileDialog instance, and then write the contents of the TextBox1 control to the selected file:
SaveFileDialog1.Filter = _ "Text Files|*.txt|All Files|*.*" SaveFileDialog1.FilterIndex = 0 If SaveFileDialog1.ShowDialog = DialogResult.OK Then Dim FS As FileStream = SaveFileDialog1.OpenFile Dim SW As New StreamWriter(FS) SW.Write(TextBox1.Text) SW.Close() FS.Close() End If
To read a text file and display it on a TextBox control, use a similar set of statements and call the ReadToEnd method of a StreamReader object. This method will read the entire file and return its contents as a string:
OpenFileDialog1.Filter = _ "Text Files|*.txt|All Files|*.*" OpenFileDialog1.FilterIndex = 0 If OpenFileDialog1.ShowDialog = DialogResult.OK Then Dim FS As FileStream FS = OpenFileDialog1.OpenFile Dim SR As New StreamReader(FS) TextBox1.Text = SR.ReadToEnd SR.Close() FS.Close() End If
You can serialize individual objects in binary form with the BinaryFormatter class, or as XML-formatted text with the SoapFormatter class. If you replace all references to the BinaryFormatter class with reference to the SoapFormatter class, you can serialize objects in XML without making any other changes to the code.
Start by creating an instance of the BinaryFormatter class:
Dim BinFormatter As New Binary.BinaryFormatter()
Then create a FileStream instance based on the file where you want to serialize the object:
Dim FS As New System.IO.FileStream("c: est.txt", IO.FileMode.Create)
After creating the BinFormatter and the FS variables, call the Serialize method to serialize any serializable framework object:
R = New Rectangle(rnd.Next(0, 100), _ rnd.Next(0, 300), rnd.Next(10, 40), _ rnd.Next(1, 9)) BinFormatter.Serialize(FS, R)
To serialize your own objects, add the Serializable attribute to the class:
Public Structure Person Dim Name As String Dim Age As Integer Dim Income As Decimal End Structure
To serialize an instance of the Person class, create an instance of the class and initialize it. Then serialize the Person object by creating a formatter and calling its Serialize method:
P = New Person() P.Name = "Joe Doe" P.Age = 35 P.Income = 28500 BinFormatter.Serialize(FS, P)
You can continue serializing additional objects serialized on the same stream, and then later read them back in the same order. For example, to serialize a Rectangle object immediately after the Person object in the same stream, use a statement like this:
BinFormatter.Serialize(FS, New Rectangle _ (0, 0, 100, 200))
To deserialize the Person object, create a BinaryFormatter object, call its Deserialize method and then cast the method’s return value to the appropriate type. The Deserialize method deserializes the next available object in the stream.
Suppose you’ve serialized a Person and a Rectangle object, in that order. To deserialize them, open the FileStream for reading, and use the following statements:
Dim P As New Person() P = BinFormatter.Serialize(FS, Person) Dim R As New Rectangle R = BinFormatter.Serialize(FS, Rectangle)
Most applications deal with collections of objects rather than individual object variables. To work with sets of data, you can create an array (or any other collection, such as an ArrayList or a HashTable), populate it with objects and then serialize the entire collection with a single call to the Serialize method. The following statements create an ArrayList with two Person objects and serialize the entire collection:
Dim FS As New System.IO.FileStream _ ("c: est.txt", IO.FileMode.Create) Dim BinFormatter As New Binary.BinaryFormatter() Dim P As New Person() Dim Persons As New ArrayList P = New Person() P.Name = "Person 1" P.Age = 35 P.Income = 32000 Persons.Add(P) P = New Person() P.Name = "Person 2" P.Age = 50 P.Income = 72000 Persons.Add(P) BinFormatter.Serialize(FS, Persons)
To read the instances of the Person class you’ve stored to a file, create an instance of the BinaryFormatter class and call its Deserialize method, passing a FileStream object that represents the file as an argument. The Deserialize method returns an Object variable, which you can cast to the appropriate type. The following statements deserialize all the objects persisted in a file and process the objects of the Person type:
FS = New System.IO.FileStream _ ("c: est.txt", IO.FileMode.OpenOrCreate) Dim obj As Object Dim P As Person(), R As Rectangle() Do obj = BinFormatter.Deserialize(FS) If obj.GetType Is GetType(Person) Then P = CType(obj, Person) ' Process the P objext End If Loop While FS.Position < FS.Length - 1 FS.Close()
To deserialize an entire collection call the Deserialize method and then cast the method's return value to the appropriate type. The following statements deserialize the Persons array:
FS = New System.IO.FileStream("c: est.txt", IO.FileMode.OpenOrCreate) Dim obj As Object Dim Persons As New ArrayList obj = CType(BinFormatter.Deserialize(FS), ArrayList) FS.Close()
To connect to a remote Web server and request a file, you must create a WebRequest object and call its GetResponse method. The GetResponse method returns a Stream object, which you can use to read the remote file almost as if it were local. The following statements create a WebRequest object, which represents a request you make from within your application to a remote file. To create a WebRequest object call the Create method of the WebRequest class, passing the URL of the remote resource as argument. To retrieve the file, which in this case is the response from the remote Web server, you call the GetResponse method of the WebRequest object that represents the request. The GetResponse method returns a WebResponse object, which you can then pass as an argument to the StreamReader constructor. The following statements show how to request a file from a Web server and display it in a TextBox control:
Dim url As New Uri = _ "http://www.your_server.com/your_file.txt" Dim Req As WebRequest Req = WebRequest.Create(url) Dim Resp As WebResponse Try Resp = Req.GetResponse Catch exc As Exception MsgBox(exc.Message) Exit Sub End Try Dim netStream As StreamReader netStream = New StreamReader(Resp.GetResponseStream) TextBox2.Text = netStream.ReadToEnd
The MemoryStream Class
The MemoryStream represents a stream in memory, effectively letting you treat your computer's memory as a file. One common use of the MemoryStream class is to create clones (copies) of objects. If you serialize an object to a MemoryStream and then deserialize the stream and assign the resulting object to a new variable, you'll get back a copy of the original object?an exact duplicate, a clone. The following statements outline the process. The public Clone function could be a method of the Person class shown earlier in this article:
Public Function Clone() As Person Dim BinFormatter As New Binary.BinaryFormatter() Dim memStream As New System.IO.MemoryStream() BinFormatter.Serialize(memStream, Me) memStream.Position = 0 Return CType(BinFormatter.Deserialize _ (memStream), Person1) End Function
To test the Clone method, create a Person instance and initialize its fields. Then declare another Person variable and assign the clone of the first variable to it:
Dim P1 As New Person Dim P2 As New Person P1.Name = "my name" P1.Age = 35 P1.Income = 40000 P2 = P1.Clone()
Note that if you assign P2 to P1, both variables will point to the same object and every change you make to P1 will also affect P2. By cloning an object, you have created two instances of the Person class and you can manipulate them individually.
The point to take away from this article is that by abstracting the operations to read and write objects of all types to any medium the .NET Stream classes unify and simplify the process of reading and writing to all types of sequential data stores.