W hen 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.
Using the
FileStream Class 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
Append |
Opens an existing file and moves to the end of
it, or creates a new file. Use this mode when the file is
opened for writing |
Create |
Creates a new file if the specified file exists,
or overwrites the existing file. |
CreateNew |
Creates a new file. If the path argument
specifies an existing file, an exception will be
thrown. |
Open |
Opens an existing file. If the path argument
specifies a file that doesn't exist, an exception will be
thrown. |
OpenOrCreate |
Opens the specified file if it exists, or
creates a new one. |
Truncate |
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.
More Flexible
I/O Operations 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.
Object
Serialization 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.
Common File
I/O Scenarios 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:
Example:
Saving Text to a 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:
Example:
Reading Text from a File 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
Persisting
Objects 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:\test.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:
Example:
A Simple Serializable Class <Serializable()> 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:
Example:
Serializing a Custom Object using the BinaryFormatter Class 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:
Example:
Deserializing Custom Objects Dim P As New Person()
P = BinFormatter.Serialize(FS, Person)
Dim R As New Rectangle
R = BinFormatter.Serialize(FS, Rectangle)
Persisting
Collections 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:
Example:
Persisting a Collection of Custom Objects Dim FS As New System.IO.FileStream _
("c:\test.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:
Example:
Deserializing Custom Objects FS = New System.IO.FileStream _
("c:\test.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:
Example:
Deserializing a Collection of Custom Objects FS = New System.IO.FileStream("c:\test.txt", IO.FileMode.OpenOrCreate)
Dim obj As Object
Dim Persons As New ArrayList
obj = CType(BinFormatter.Deserialize(FS), ArrayList)
FS.Close()
Downloading
Internet Resources 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:
Example:
Reading a File from a Remote Web Server 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:
Example:
Creating a Copy of a Custom Object 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.
Evangelos Petroutsos is a long time VB developer. When
he's not writing code, he writes programming books (most published
by Sybex), and articles. Reach him by e-mail at pevangelos@yahoo.com.
|