Almost every program at some point must interact with files or persisted data in some way. The foundation of nearly all such functionality is located in the System.IO
namespace.
There are generally two modes to access files: text and binary. In text mode, the raw contents of a file are converted to System.String
for easy manipulation in .NET. Binary files are just that—you get access to the raw, unfiltered bytes, and you can do what you want with them. We’ll look at two simple programs that examine each type of file.
Most I/O in .NET is handled with the concept of streams, which are conceptual buffers of data that you can read from and write to in a linear fashion. Some streams (such as files) allow you to jump to an arbitrary location in the stream. Others (such as network streams) do not.
Solution: If you tell .NET that you are opening a text file, the underlying stream is wrapped in a text decoder called StreamReader
or StreamWriter
that can translate between UTF-8 text and the raw bytes.
Here is a sample program that writes its command-line arguments to a text file that you specify and then reads it back:
For this and all succeeding code examples in this chapter, make sure you have using System.IO;
at the top of your file, as it will not always be explicit in every code sample.
Files are shared resources, and you should take care to close them when done to return the resource back to the operating system. This is done using the Dispose Pattern, which is described in Chapter 22, “Memory Management.” The using
statements in the preceding code sample ensure that the files are closed at the end of each code block, regardless of any exceptions that occur within the block. You should consider this a best practice that you should always follow whenever possible.
Solution: When you open files in binary mode, you get back a stream that you can control more precisely than with text. Here is very naive file copy program, but it demonstrates the point:
Solution: The File
class has a number of static methods you can use, one of them being Delete()
.
File.Delete("Filename.txt");
If the file does not exist, no exception will be thrown. However, DirectoryNotFoundException
is thrown if the directory does not exist.
Solution: You can combine streams to accomplish rich behavior that can perform powerful transformations. Listing 11.1 shows a quick-and-dirty file compression utility.
Solution: The FileInfo
class encapsulates data from the file system.
The FileInfo
class contains many useful properties, such as creation and modification times, the directory, and file attributes such as hidden and archive in the Attributes
property (an enumeration of FileAttributes
).
Solution: The System.Security namespace contains .NET classes related to security. The following code sample display security information about a file whose name is passed on the command line.
You can run the program from the command line like this:
It produces output similar to the following:
Solution: This snippet uses the static Exists()
methods on File
and Directory
to check for existence:
Solution: The DriveInfo
class provides static methods to retrieve this information.
This code produces the following output on my system (edited to show only unique drives):
Solution: DirectoryInfo
can retrieve a list of subdirectories and files inside of it.
Be aware that you can easily get a SecurityException
while enumerating files and folders, especially if you’re not running as an administrator.
Solution: Use the FolderBrowserDialog
class in System.Windows.Forms
. This example is from the BrowseForDirectories project in the sample code for this chapter.
There is no WPF-equivalent of this dialog, but you can use it from WPF as well.
Solution: The DirectoryInfo
object contains many useful functions, among them the ability to search the file system for files and folders containing a search pattern. The search pattern can contain wildcards, just as if you were using the command prompt.
Listing 11.2 provides a simple example that searches a directory tree for any file or directory that matches the input pattern.
Here’s the program output (your results will likely vary):
You may get an “access denied” error while running this program when the search runs across a folder you don’t have permission to access. One way to handle this is to change the search options to SearchOption.TopDirectoryOnly
, track the subdirectories in a stack, and perform the recursive search yourself, handling the possible exceptions along the way.
Solution: You should almost never need to manually parse a path in C#. Instead, use the System.IO.Path
class to perform your manipulation. This class has only static methods.
Table 11.1 details most of the methods and properties available to you.
There are two additional useful methods. Path.Combine()
will take two or more strings and combine them into a single path, inserting the correct directory separator characters where necessary. Here’s an example:
string path = Path.Combine(@"C:Windows", "System32", "xcopy.exe ");
This results in the path having the value "C:WindowsSystem32xcopy.exe "
.
The second method, Path.ChangeExtension()
, does just what you think it would. Here’s an example:
The new value of path is "C:WindowsSystem32xcopy.bin"
. Note that this does not change the file on the disk—it’s just in the string.
Solution: Rather than trying to figure out where the user’s temporary directory is, generating a random filename, and then creating a file, you can just use the following:
On one run on my system, GetTempFileName()
produced the following value:
C:UsersBenAppDataLocalTemp mp96B7.tmp
If you don’t need the file to actually be created, you can get just a random filename (no path) by using the following:
string fileName = Path.GetRandomFileName();
//fileName: fup5cwk4.355
Solution: Many file systems provide ways for the operating system to notify programs when files and directories change. .NET provides the System.IO.FileSystemWatcher
class to listen to these events.
Here’s some sample output (during a create, rename, and delete of the file temp.txt):
You can also call watcher.WaitForChange()
, which will wait until something happens before returning, but usually the event-based mechanism will work for you.
Solution: You should never hard-code the paths. Not only can different versions of the OS have different folder names, but the user can change these folders to be in whatever location he wants. Use Environment.GetFolder()
with the Environment.SpecialFolder
enumeration to retrieve the actual folder on the disk. The following code will print the entire list:
In general, you should put your application’s data in the LocalApplicationData (nonroaming) or ApplicationData (roaming) folder. That is what these folders are meant for. Examine them on your own computer to see how other software uses them. You should never write program data to ProgramFiles or anywhere file security is an issue. The ApplicationData directory will be replicated on all computers to which the user logs on in a domain, so avoid putting unnecessary data there. Starting with Windows Vista, the operating system enforces file security much more strongly, and an application that doesn’t pay attention to security issues can break.
Solution: The easy way to allow a type to be serialized is to just put the attribute [Serializable]
in front of the type definition. .NET provides three built-in serialization formatters: SOAP, binary, and XML. XML serialization is handled a little differently and will be discussed in its own chapter (see Chapter 14, “XML”). For the examples in this chapter, I chose SOAP because it can be represented in text. But you can easily substitute the BinaryFormatter
class.
SerializableAttribute
Let’s do this to our Vertex3d
class from Chapter 2, “Creating Versatile Types.” For simplicity, I’ve created a simplified version of the struct
for use here:
The following code can serialize and output it for us:
All of that, with no effort other than adding [Serializable]
. Actually, not quite true. This version of Vertex3d doesn’t contain the int? _id field that was present in the original code.
Serialization formatters don’t know what to do with that. If you want to be able to serialize classes that contain data that can’t be automatically formatted, you have to do a little more work, as you’ll see in the next section.
ISerializable
In order to serialize more complex data (often such as collections), you need to take control of the serialization process. We’ll do this by implementing the ISerializable
interface and also defining a serializing constructor.
Modify the code from the previous section with the following additions:
Solution: The preceding serialization example did this, but I’ll cover it in this section as well.
Solution: Use Isolated Storage. This is kind of like a virtual file system that .NET associates with an assembly, user, application domain, application (when using ClickOnce only), or a combination of these items. By using Isolated Storage, you can give programs the ability to store information without giving them access to the real file system.
Here’s a simple example that creates a subdirectory and a text file: