Overview

Previous versions of Microsoft Visual Basic have always been underweight when it came to file management. Most Visual Basic 6.0 programmers used legacy functions such as Open and Input that were built into the language and identified files using numbers. More adventurous developers could use the File Scripting Objects (FSO) model, which provided an object-oriented way to manipulate files, but lacked important features such as the ability to read and write binary files. In Microsoft .NET, the story is completely different—for the first time, Visual Basic developers have a rich set of objects that allow them to retrieve file system information, move and rename files and directories, create text and binary files, and even monitor a specific path for changes.

The first batch of recipes in this chapter describes the basics for manipulating files and directories. Later recipes include advanced techniques, such as selecting files with wildcards (recipe 5.6), performing recursive searches (recipes 5.7 and 5.8), reading just-in-time file information (recipe 5.9), and using isolated stores to allow file creation in low-security contexts (recipe 5.15). You'll even learn the starting points for dealing with some more specialized formats, including MP3 files (recipe 5.18) and ZIP files (recipe 5.19).

Note

Some of the example applications require command-line arguments. If you are using Visual Studio .NET, you can enter these arguments in the project properties (under the Configuration Properties | Debugging node). Keep in mind that if you need to enter directory or file names that incorporate spaces, you will need to place the full name in quotation marks. Also, most of the code examples in this chapter assume that you have imported the System.IO namespace.

Manipulate a File

Problem

You want to delete, rename, or check if a file exists. Or, you want to retrieve information about a file such as its attribute or creation date.

Solution

Create a FileInfo instance for the file, and use its properties and methods.

Discussion

To create a FileInfo object, you simply supply a relative or fully qualified path to the FileInfo constructor. This file doesn't necessarily need to exist. You can then use the FileInfo properties and methods to retrieve file information or manipulate the file.

Table 5-1 lists some useful FileInfo members that are also exposed, in more or less the same form, by the DirectoryInfo object described in recipe 5.2. Table 5-2 lists members that are exclusive to the FileInfo class.

Table 5-1: Common FileInfo and DirectoryInfo Members

Member

Description

Exists

Exists returns True or False, depending on whether a file or directory exists at the specified location. Some other FileInfo or DirectoryInfo properties might return an error if the file or directory doesn't exist.

Attributes

Returns one or more values from the FileAttributes enumeration, which represents the attributes of the file or directory.

CreationTime,

LastAccessTime, and LastWriteTime

Return DateTime instances that describe when a file or directory was created, last accessed, and last updated, respectively.

FullName, Name, and Extension

Returns a string that represents the fully qualified name, the directory or file name (with extension), and the extension on its own.

Delete

Removes the file or directory, if it exists. If you want to delete a directory that contains other directories, you must use the overloaded Delete method that accepts a parameter named recursive and set it to True.

Refresh

Updates the object so that it's synchronized with any file system changes that have taken place since the FileInfo or DirectoryInfo object was created (for example, if an attribute was changed manually using Windows Explorer).

MoveTo

Copies the directory and its contents or copies the file. For a DirectoryInfo object, you need to specify the new path. For a FileInfo object you specify a path and filename. MoveTo can also be used to rename a file or directory without changing its location.

Table 5-2: FileInfo Members

Member

Description

Length

Length returns the file size as a number of bytes.

DirectoryName and Directory

DirectoryName returns the name of the parent directory, whereas Directory returns a full DirectoryInfo object (see recipe 5.2) that represents the parent directory and allows you to retrieve more information about it.

CopyTo

Copies a file to the new path and filename specified as a parameter. It also returns a new FileInfo object that represents the new (copied) file. You can supply an optional additional parameter of True to allow overwriting.

Create andCreateText

Create creates the specified file and returns a FileStream object that you can use to write to it. CreateText performs the same task, but returns a StreamWriter object that wraps the stream.

Open, OpenRead, OpenText, andOpenWrite

Open a file (provided it exists). OpenRead and OpenText open a file in read-only mode, returning a FileStream or StreamReader. OpenWrite opens a file in write-only mode, returning a FileStream.

The following Console application takes a filename from a supplied parameter argument and displays information about that file.

Most of the functionality provided by the FileInfo object can be accessed using shared methods of the File class. Generally, you should use FileInfo if you want to retrieve more than one piece of information at a time because it performs security checks once (when you create the FileInfo instance) rather than every time you call a method. The File object also lacks a Length property.

Manipulate a Directory

Problem

You want to delete, rename, or check if a directory exists. Or, you want to retrieve information about a directory such as its attributes or creation date.

Solution

Create a DirectoryInfo instance for the directory, and use its properties and methods.

Discussion

The DirectoryInfo object works almost the same as the FileInfo object. You can use the same properties for retrieving attributes, names, and file system timestamps. You can also use the same methods for moving, deleting, and renaming directories as you would with files. These members are described in Table 5-1. In addition, the DirectoryInfo object provides some directory-specific members, which are shown in Table 5-3.

Table 5-3: DirectoryInfo Members

Member

Description

Create

Creates the specified directory. If the path specifies multiple directories that don't exist, they will all be created at once.

Parent and Root

Returns a DirectoryInfo object that represents the parent or root directory.

CreateSubdirectory

Creates a directory with the specified name in the directory represented by the DirectoryInfo object. It also returns a new DirectoryInfo object that represents the subdirectory.

GetDirectories

Returns an array of DirectoryInfo objects, with one for each subdirectory contained in this directory.

GetFiles

Returns an array of FileInfo objects, with one for each file contained in this directory.

The following Console application takes a directory path from a supplied parameter argument and displays information about that directory.

Retrieve File Version Information

Problem

You want to retrieve file version information (such as the publisher of a file, its revision number, associated comments, and so on).

Solution

Use the GetVersionInfo method of the System.Diagnostics.FileVersionInfo class.

Discussion

In previous versions of Visual Basic, you needed to call Windows API functions to retrieve file version information. With the .NET Framework, you simply need to use the FileVersionInfo class and call the GetVersionInfo method with the filename as a parameter. You can then retrieve extensive information through the FileVersionInfo properties.

The FileVersionInfo properties are too numerous to list here, but the following code snippet shows an example of what you might retrieve:

Use Bitwise Arithmetic with File Attributes

Problem

You want to correct examine or modify file attribute information.

Solution

Use bitwise arithmetic with the And and Or keywords.

Discussion

The FileInfo.Attributes and DirectoryInfo.Attributes properties represent file attributes such as archive, system, hidden, read-only, compressed, and encrypted. (Refer to the MSDN reference for the full list.) Because a file can possess any combination of attributes, the Attributes property accepts a combination of enumerated values. To individually test for a single attribute, or change a single attribute, you need to use bitwise arithmetic.

For example, consider the following code:

' This file has the archive, read-only, and encrypted attributes.
Dim MyFile As New FileInfo("data.txt")
' This displays the string "ReadOnly, Archive, Encrypted"
Console.WriteLine(MyFile.Attributes.ToString())
' This test fails, because other attributes are set.
If MyFile.Attributes = FileAttributes.ReadOnly Then
Console.WriteLine("File is read-only.")
End If
' This test succeeds, because it filters out just the read-only attribute.
' The parentheses are required.
If (MyFile.Attributes And FileAttributes.ReadOnly) = _
FileAttributes.ReadOnly Then
Console.WriteLine("File is read-only.")
End If

Essentially, the Attributes setting is made up (in binary) of a series of ones and zeros, such as 00010011. Each 1 represents an attribute that is present, while each 0 represents an attribute that is not. When you use the And operation with an enumerated value, it automatically performs a bitwise And, which compares each individual digit against each digit in the enumerated value. For example, if you combine a value of 00100001 (representing an individual file's archive and read-only attributes) with the enumerated value 00000001 (which represents the read-only flag), the resulting value will be 00000001—it will only have a 1 where it can be matched in both values. You can then test this resulting value against the FileAttributes.ReadOnly enumerated value using the equals sign.

Similar logic allows you to verify that a file does not have a specific attribute:

If Not (MyFile.Attributes And FileAttributes.Compressed) = _
FileAttributes.Compressed Then
Console.WriteLine("File is not compressed.")
End If

When setting an attribute, you must also use bitwise arithmetic. In this case, it's needed to ensure that you don't inadvertently wipe out the other attributes.

' This adds just the read-only attribute.
MyFile.Attributes = MyFile.Attributes Or FileAttributes.ReadOnly
' This removes just the read-only attribute.
MyFile.Attributes = MyFile.Attributes And Not FileAttributes.ReadOnly

Read to and Write from a Binary File

Problem

You want to read or write data from a binary file.

Solution

Use the BinaryReader or BinaryWriter to wrap the underlying FileStream.

Discussion

The BinaryReader and BinaryWriter classes provide an easy way to work with binary data. The BinaryWriter class provides an overloaded Write method that takes any basic string or number data type, converts it to a set of bytes, and writes it to a file stream. The BinaryReader performs the same task in reverse—you call methods such as ReadString or ReadInt32, and it retrieves the data from the current position in the file stream and converts it to the desired type.

Here's a simple code snippet that writes data to a binary file, and reads it back.

' Define the sample data.
Dim MyString As String = "Sample Value"
Dim MySingle As Single = 88.21
' Write the data to a new file using a BinaryWriter.
Dim fs As New FileStream("data.bin", FileMode.Create)
Dim w As New BinaryWriter(fs)
w.Write(MyString)
w.Write(MySingle)
w.Close()
' Read the data with a BinaryReader.
fs = New FileStream("data.bin", FileMode.Open)
Dim r As New BinaryReader(fs)
Console.WriteLine(r.ReadString())
Console.WriteLine(r.ReadSingle)
r.Close()

Remember when writing data using BinaryWriter to store the data in an intermediate variable rather than write the data directly. This way, you can know if numeric types are being written as integers, decimals, singles, and so on. Otherwise, you won't know whether to call a method such as ReadInt32 or ReadSingle when retrieving the information, and the wrong choice will generate an error!

Note

To convert more complex objects into binary representation, you'll need to use object serialization, as discussed in recipe 4.9.

Filter Files with Wildcards

Problem

You need to process multiple files based on a filter expression (such as *.txt or rec03??.bin).

Solution

Use the overloaded version of the DirectoryInfo.GetFiles method that accepts a filter expression.

Discussion

The DirectoryInfo and Directory objects both provide a way to search the current directories for files that match a specific filter expression. These search expressions can use the standard ? and * wildcards.

For example, the following code snippet retrieves the names of all the files in the c: emp directory that have the extension .txt. The code then iterates through the retrieved FileInfo collection of matching files and displays the name and size of each one.

Dim File, Files() As FileInfo
' Check all the text files in temporary directory.
Dim Dir As New DirectoryInfo("c: emp")
Files = Dir.GetFiles("*.txt")
' Display the name of all the files.
For Each File In Files
Console.Write("Name: " & File.Name & " ")
Console.WriteLine("Size: " & File.Length.ToString)
Next

If you want to search subdirectories, you will need to add your own recursion, as described in recipe 5.7.

Note

You can use a similar technique to retrieve directories that match a specified search pattern by using the overloaded DirectoryInfo.GetDirectories method.

Process Files Recursively

Problem

You need to perform a task with all the files in the current directory and any subdirectories.

Solution

Use the DirectoryInfo.GetFiles method to retrieve a list of files in a directory, and use recursion to walk through all subdirectories.

Discussion

Both the Directory and DirectoryInfo classes provide a GetFiles method, which retrieves files in the current directory. They also expose a GetDirectories method, which retrieves a list of subdirectories. To process a tree of directories, you can call the GetDirectories method recursively, working your way down the directory structure.

The FileSearcher class that follows shows how you can use this technique to perform a recursive search. The SearchDirectory routine adds all the files that match a specific pattern to an ArrayList and then calls SearchDirectory individually on each subdirectory.

Public Class FileSearcher
Private _Matches As New ArrayList
Private _FileFilter As String
Private Recursive As Boolean
Public ReadOnly Property Matches() As ArrayList
Get
Return _Matches
End Get
End Property
Public Property FileFilter() As String
Get
Return _FileFilter
End Get
Set(ByVal Value As String)
_FileFilter = Value
End Set
End Property
Public Sub New(ByVal fileFilter As String)
Me.FileFilter = fileFilter
End Sub
Public Sub Search(ByVal startingPath As String, _
ByVal recursive As Boolean)
Matches.Clear()
Recursive = recursive
SearchDirectory(New DirectoryInfo(startingPath))
End Sub
Private Sub SearchDirectory(ByVal dir As DirectoryInfo)
' Get the files in this directory.
Dim FileItem As FileInfo
For Each FileItem In dir.GetFiles(FileFilter)
' If the file matches, add it to the collection.
Matches.Add(FileItem)
Next
' Process the subdirectories.
If Recursive Then
Dim DirItem As DirectoryInfo
For Each DirItem In dir.GetDirectories()
Try
' This is the recursive call.
SearchDirectory(DirItem)
Catch Err As UnauthorizedAccessException
' Error thrown if you don't have security permissions
' to access directory - ignore it.
End Try
Next
End If
End Sub
End Class

Here's an example that demonstrates searching with the FileSearcher class:

It would be easy to enhance the FileSearcher class to support other types of search criteria, such as file size or attributes. In addition, the code would become more failsafe if ArrayList were replaced with a type-safe collection that could only accept FileInfo objects, as described in recipe 3.16.

Search for a File with Specific Text

Problem

You need to perform a search for a file that contains specific text.

Solution

Search through a file character-by-character using the FileStream.ReadByte method, and try to build up a matching string.

Discussion

Full-text searching is fairly easy to implement, although it can be time consuming, and it typically works best with text files. All you need to do is scan through a file, attempting to read each byte and convert it to a character. If you read a character that matches the requested text, you can then check to see if the next character matches, and so on.

The following FileTextSearcher class encapsulates the functionality required to perform a full-text search that works with any type of file.

Public Class FileTextSearcher
Private _Matches As New ArrayList
Private _FileFilter As String
Private _SearchText As String
Private _CaseSensitive As Boolean = True
Public ReadOnly Property Matches() As ArrayList
Get
Return _Matches
End Get
End Property
Public Property FileFilter() As String
Get
Return _FileFilter
End Get
Set(ByVal Value As String)
_FileFilter = Value
End Set
End Property
Public Property SearchText() As String
Get
Return _SearchText
End Get
Set(ByVal Value As String)
_SearchText = Value
End Set
End Property
Public Property CaseSensitive() As Boolean
Get
Return _CaseSensitive
End Get
Set(ByVal Value As Boolean)
_CaseSensitive = Value
End Set
End Property
Public Sub New(ByVal fileFilter As String, ByVal searchText As String)
Me.FileFilter = fileFilter
Me.SearchText = searchText
End Sub
Public Sub Search(ByVal startingPath As String)
Matches.Clear()
SearchDirectory(New DirectoryInfo(startingPath))
End Sub
Private Sub SearchDirectory(ByVal dir As DirectoryInfo)
' Get the files in this direcory.
Dim FileItem As FileInfo
For Each FileItem In dir.GetFiles(FileFilter)
' Test if file matches.
If TestFileForMatch(FileItem) Then
Matches.Add(FileItem)
End If
Next
' You could add recursive logic here by calling SearchDirectory
' on all subdirectories (see recipe 5.7).
End Sub
Private Function TestFileForMatch(ByVal file As FileInfo) As Boolean
' Open the file.
Dim fs As FileStream = file.OpenRead()
Dim Match As Boolean = False
' Search for the text.
Dim MatchCount, MatchPosition As Integer
Dim Character, MatchCharacter As String
' Read through the entire file.
Do Until fs.Position = fs.Length
' Get a character from the file.
Character = Convert.ToChar(fs.ReadByte())
' Retrieve the next character to be matched from the search text.
MatchCharacter = SearchText.Substring(MatchPosition, 1)
If String.Compare(Character, MatchCharacter, _
Not Me.CaseSensitive) = 0 Then
' They match. Now try to match the next character.
MatchPosition += 1
Else
' They don't match. Start again from the beginning.
MatchPosition = 0
End If
' Check if the entire string has been matched.
If MatchPosition = SearchText.Length - 1 Then
Return True
End If
Loop
fs.Close()
Return False
End Function
End Class

Here's how you can use this class to search a set of Visual Basic code files for a specific variable named MyVariable:

Dim Searcher As New FileTextSearcher("*.vb", "MyVariable")
Searcher.Search("c: emp")
' Display results.
Dim File As FileInfo
For Each File In Searcher.Matches
Console.WriteLine(File.FullName)
Next

Fill a TreeView with a Just In Time Directory Tree

Problem

You need to show a directory tree with the TreeView control, but filling the directory tree structure at startup is too time consuming.

Solution

React to the BeforeExpand event to fill in subdirectories just before they are displayed.

Discussion

You can use the recursion technique shown in recipe 5.7 to build an entire directory tree. However, scanning the file system in this way can be slow, particularly for large drives. For this reason, professional file management software (and Windows Explorer) use a different technique—they query the necessary directory information when the user requests it.

The TreeView control, shown in Figure 5-1, is particularly well suited to this approach because it provides a BeforeExpand event that fires before a new level of nodes is displayed. You can use a placeholder (such as an asterisk or empty TreeNode) in all the directory branches that are not filled in. This allows you to fill-in parts of the directory tree as they are displayed.

Figure 5-1: A directory tree with the TreeView

To support this technique, you should first create a procedure that adds a single directory node. The first level of subdirectories is entered using subnodes with an asterisk placeholder.

Private Sub Fill(ByVal dirNode As TreeNode)
Dim Dir As New DirectoryInfo(DirNode.FullPath)
Dim DirItem As DirectoryInfo
Try
For Each DirItem In Dir.GetDirectories
' Add node for the directory.
Dim NewNode As New TreeNode(DirItem.Name)
DirNode.Nodes.Add(NewNode)
NewNode.Nodes.Add("*")
Next
Catch Err As UnauthorizedAccessException
' Error thrown if you don't have security permissions
' to access directory - ignore it.
End Try
End Sub

When the form first loads, you can call this function to fill the root level of directories:

Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
' Set the first node.
Dim RootNode As New TreeNode("c:")
treeFiles.Nodes.Add(RootNode)
' Fill the first level and expand it.
Fill(RootNode)
treeFiles.Nodes(0).Expand()
End Sub

Finally, each time the user expands a node, you can react by using the Fill procedure to fill in the requested directory:

Test Two Files for Equality

Problem

You need to quickly compare the content of two files.

Solution

Calculate the hash code of each file using the HashAlgorithm class, and compare the hash codes.

Discussion

There are a number of ways you might want to compare more than one file. For example, you could examine a portion of the file for similar data, or you could read through each file byte-by-byte, comparing each byte as you go. Both of these approaches are valid, but in some cases it's more convenient to use a hash code algorithm.

A hash code algorithm generates a small (typically about 20 bytes) binary fingerprint for a file. While it's possible for different files to generate the same hash codes, it's statistically unlikely. In fact, even a minor change (for example, modifying a single bit in the source file) has a 50% chance of independently changing each bit in the hash code. For this reason, hash codes are often used in security code to detect data tampering.

To create a hash code, you must first create a HashAlgorithm object, typically by calling the shared HashAlgorithm.Create method. The HashAlgorithm class is defined in the System.Security.Cryptography namespace. You can then call HashAlgorithm.ComputeHash, which returns a byte array with the hash data.

The following code demonstrates a simple Console application that reads two file names that are supplied as arguments and tests them for equality.

Public Module FileCompare
Public Sub Main(ByVal args() As String)
If args.Length <> 2 Then
Console.WriteLine("Wrong number of arguments.")
Console.WriteLine("Specify two files.")
Else
Console.WriteLine("Comparing " & args(0) & " and " & args(1))
' Create the hashing object.
Dim Hash As System.Security.Cryptography.HashAlgorithm
Hash = System.Security.Cryptography.HashAlgorithm.Create()
' Calculate the hash for the first file.
Dim fsA As New FileStream(args(0), FileMode.Open)
Dim HashA() As Byte = Hash.ComputeHash(fsA)
fsA.Close()
' Calculate the hash for the second file.
Dim fsB As New FileStream(args(1), FileMode.Open)
Dim HashB() As Byte = Hash.ComputeHash(fsB)
fsB.Close()
' Compare the hashes.
If BitConverter.ToString(HashA) = _
BitConverter.ToString(HashB) Then
Console.WriteLine("Files match.")
Else
Console.WriteLine("No match.")
End If
End If
Console.ReadLine()
End Sub
End Module

The hashes are compared by converting them first into strings. Alternatively, you could compare them by iterating over the byte array and comparing each value.

Monitor the File System for Changes

Problem

You need to react when a file system change is detected in a specific path (such as a file modification or creation).

Solution

Use the FileSystemWatcher component, which monitors a path and raises events when files or directories are modified.

Discussion

When linking together multiple applications and business processes, it's often necessary to create a program that waits idly and only springs into action when a new file is received or changed. You can create this type of program by scanning a directory periodically, but you face a key tradeoff. The more often you scan, the more system resources you waste. The less often you scan, the longer it might take to detect the appropriate event. The solution is to use the FileSystemWatcher class to react directly to Windows file events.

To use FileSystemWatcher, you must create an instance and set the following properties:

Pathindicates the directory you want to monitor.

Filterindicates the types of files you are monitoring.

NotifyFilterindicates the type of changes you are monitoring.

The FileSystemWatcher raises four events: Created, Deleted, Renamed, and Changed. All of these events provide information through their FileSystemEventArgs parameter, including the name of the file (Name), the full path (FullPath), and the type of change (ChangeType). If you need, you can disable these events by setting the FileSystemWatcher.EnableRaisingEvents property to False.

Figure 5-2 shows an example Windows form that monitors a directory for new files (until the form is closed). The directory being monitored can be changed by typing in a new path and clicking the Start Monitoring button.

Figure 5-2: A file monitoring form

In this example, the FileSystemWatcher class has been created and connected manually. However, you can perform all of these steps at design time by adding FileSystemWatcher to the component tray, configuring it with the Properties window, and adding event handlers, in which case the code would be generated automatically as part of the form designer code.

Public Class MonitorForm
Inherits System.Windows.Forms.Form
' (Designer code omitted.)
' This is tracked as a form-level variable, because it must live as long
' as the form exists.
Private Watch As New FileSystemWatcher()
' Configure the FileSystemWatcher when the form is loaded.
Private Sub MonitorForm_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
' Attach the event handler.
AddHandler Watch.Created, AddressOf Watch_Created
End Sub
Private Sub cmdMonitor_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles cmdMonitor.Click
Try
Watch.Path = txtMonitorPath.Text
Watch.Filter = "*.*"
Watch.IncludeSubdirectories = True
Watch.EnableRaisingEvents = True
Catch Err As Exception
MessageBox.Show(Err.Message)
End Try
End Sub
' Fires when a new file is created in the directory being monitored.
Private Sub Watch_Created(sender As Object, _
e As System.IO.FileSystemEventArgs)
' Add the new file name to a list.
lstFilesCreated.Items.Add("'" & e.FullPath & _
"' was " & e.ChangeType.ToString())
End Sub
End Class

The Created, Deleted, and Renamed events are easy to handle. However, if you want to use the Changed event, you need to use the NotifyFilter property to indicate the types of changes you are looking for. Otherwise, your program might be swamped by an unceasing series of events as files are modified.

The NotifyFilter property can be set using any combination of the following values from the NotifyFilters enumeration:

Attributes

CreationTime

DirectoryName

FileName

LastAccess

LastWrite

Security

Size

You can combine any of these values using bitwise arithmetic through the Or keyword. In other words, to monitor for CreationTime and DirectoryName changes, you would use this code:

Create a Temporary File

Problem

You want to get a file name that you can use for a temporary file.

Solution

Use the shared Path.GetTempFileName method.

Discussion

There are a number of approaches to generating temporary files. In simple cases, you might just create a file in the application directory, possibly using a GUID filename or a timestamp in conjunction with a random value. However, the System.IO.Path class provides a helper method that can save you some work. It returns a unique filename (in the current user's temporary directory) that you can use to create a file for storing temporary information. This might be a path like c:documents and settingsusernamelocal settings emp mpac9.tmp.

If you call GetTempFileName multiple times, you will receive a different filename each time, even if you don't create a file with that name. This system is designed to avoid name collision between multiple applications.

Get the Executable Path

Problem

You want to retrieve the path where the current executable is stored.

Solution

Read the shared StartupPath property of the System.Windows.Forms.Application class.

Discussion

The System.Windows.Forms.Application class allows you to retrieve the directory where the executable is stored, even if it isn't a Windows application.

In order to use this technique, you must reference the System.Windows.Forms namespace. Alternatively, you can simply find the current working path (using recipe 5.14) or use reflection to find the codebase location of the currently executing assembly (as described in recipe 9.1).

Set the Current Working Path

Problem

You want to set the current working directory so you can use relative paths in your code.

Solution

Use the shared Directory.GetCurrentDirectory and Directory.SetCurrentDirectory methods.

Discussion

Relative paths are automatically interpreted in relation to the current working directory. You can retrieve the current working directory by calling Directory.GetCurrentDirectory, or change it using Directory.SetCurrentDirectory. In addition, you can use the shared Path.GetFullPath method to convert a relative path into an absolute path using the current working directory.

Using: D:TempConsoleApplication1in
The relative path myfile.txt will automatically become
D:TempConsoleApplication1inmyfile.txt
Changing current directory to c:
The relative path myfile.txt will automatically become c:myfile.txt

Note

If you use relative paths, it's recommended that you set the working path at the start of each file interaction. Otherwise, you could introduce unnoticed security vulnerabilities that could allow a malicious user to force your application into accessing or overwriting system files by tricking it into using a different working directory.

Use an Isolated Store

Problem

You need to store data in a file, but your application doesn't run with the required FileIOPermission.

Solution

Use a user-specific isolated store.

Discussion

The .NET Framework includes support for isolated storage, which allows you to read and write to a user-specific virtual file system that the common language runtime manages. When you create isolated storage files, the data is automatically serialized to a unique location in the user profile path (typically a path like c:document and settings[username]local settingsapplication dataisolated storage[guid_identifier]).

One reason you might use isolated storage is to give an untrusted application limited ability to store data. For example, the default common language runtime security policy gives local code FileIOPermission, which allows it to open or write to any file. Code that you run from a remote server on the local Intranet is automatically assigned less permission—it lacks the FileIOPermission, but has the IsolatedStoragePermission, giving it the ability to use isolated stores. (The security policy also limits the maximum amount of space that can be used in an isolated store.) Another reason you might use an isolated store is to better secure data. For example, data in one user's isolated store will be restricted from another nonadministrative user. Also, because isolated stores are sorted in directories using GUID identifiers, it might not be as easy for an attacker to find the data that corresponds to a specific application.

The following example shows how you can access isolated storage. It assumes you have imported the System.IO.IsolatedStorage namespace.

' Create the store for the current user.
Dim Store As IsolatedStorageFile
Store = IsolatedStorageFile.GetUserStoreForAssembly()
' Create a folder in the root of the isolated store.
Store.CreateDirectory("MyFolder")
' Create a file in the isolated store.
Dim Stream As New IsolatedStorageFileStream( _
"MyFolderMyFile.txt", FileMode.Create, Store)
Dim w As New StreamWriter(Stream)
' (You can now write to the file as normal.)
w.Close()

Note

You can also use methods such as IsolatedStorageFile.GetFileNames and IsolatedStorageFile.GetDirectoryNames to enumerate the contents of an isolated store.

By default, each isolated store is segregated by user and assembly. That means that when the same user runs the same application, the application will access the data in the same isolated store. However, you can choose to segregate it further by application domain, so that multiple instances of the same application receive different isolated stores.

' Access isolated storage for the current user and assembly
' (which is equivalent to the first example).
Store = New IsolatedStorageFile.GetStore(IsolatedStorageScope.User Or _
IsolatedStorageScope.Assembly, Nothing, Nothing)
' Access isolated storage for the current user, assembly,
' and application domain. In other words, this data is only
' accessible by the current application instance.
Store = New IsolatedStorageFile.GetStore(IsolatedStorageScope.User Or _
IsolatedStorageScope.Assembly Or IsolatedStorageScope.Domain, _
Nothing, Nothing)

The files are stored as part of a user's profile, so users can access their isolated storage files on any workstation they log on to if roaming profiles are configured on your LAN. By letting the .NET Framework and the common language runtime provide these levels of isolation, you can relinquish responsibility for maintaining separation between files, and you don't have to worry that programming oversights or misunderstandings will cause loss of critical data.

Read Application Configuration Settings

Problem

You need to store application-specific settings that can be modified easily without recompiling code.

Solution

Read settings from an application configuration file.

Discussion

Configuration files are ideal repositories for information such as directory paths and database connection strings. One useful feature about configuration files is the fact that they are tied to a particular directory, not a particular computer (as a registry setting would be). Thus, if several clients load the same application from the same directory, they will share the same custom settings. However, you might need to add additional security to prevent users from reading or modifying a configuration file that is shared in this way.

To create a configuration file for your application, give the file the same name as your application, plus the extension .config. For example, the application MyApp.exe would have a configuration file MyApp.exe.config. The only exception is Web applications including Web pages and Web services, which are loaded by Microsoft ASP.NET and Internet Information Services (IIS). In this case, ASP.NET always uses a file with the name web.config from the corresponding virtual directory.

Note

Visual Studio .NET provides a shortcut for creating configuration files. Simply right-click the project in the Solution Explorer, and select Add | New Item. Then choose Application Configuration File under the Local Project Items node.

The application configuration file is automatically assigned the name app.config. You should not change this name. When Visual Studio .NET compiles your project, it will create the configuration file in the appropriate directory, with the correct name. This allows you to rename your application's assembly name at design-time without needing to alter the name of the corresponding configuration file.

You can add an unlimited number of name-value pairs to a configuration file. You add these settings to the portion of the file using elements. Every custom setting has a string value and a unique string key that identifies it. Here is a configuration file with one custom setting (named CustomPath):

You can retrieve custom settings through the System.Configuration.ConfigurationSettings class using the key name. Settings are always retrieved as strings. The following code snippet assumes you have imported the System.Configuration namespace.

' Retrieve the custom path setting.
Dim MyPath As String
MyPath = ConfigurationSettings.AppSettings("CustomPath")
' MyPath is now set to "c:TempMyFiles"

If you want to store more than one related setting in a configuration file, you might want to create a custom configuration section, along with a custom section reader. This technique is described in recipe 5.17.

Note

If a class library uses the AppSettings class, it will access the configuration file that was loaded by the executable application. Thus, if the application MyApp.exe loads the assembly MyLib.dll, all configuration file access in MyLib.dll will be directed to the file MyApp.exe.config.

Create Custom Configuration Sections

Problem

You want to use a custom configuration setting to organize related custom settings.

Solution

Register your custom setting with the System.Configuration.NameValueSectionHandler class. You can then use the shared ConfigurationSettings.GetConfig method to retrieve a collection of settings from the section.

Discussion

.NET uses an extensible system of configuration file settings. You have multiple options for reading custom settings from a configuration file:

Place your custom settings in the group, and access them through the ConfigurationSettings.AppSettings collection. This approach was used in recipe 5.16.

Create your own custom section handler by implementing IConfigurationSectionHandler and registering it in the configuration file. This provides unlimited flexibility, but is rarely required.

Place your configuration settings in a custom group, and register a prebuilt configuration section reader such as NameValueSectionHandler or SingleTagSectionHandler. This is the approach we'll use in this section.

To use NameValueSectionHandler, you should first create the group with the custom settings and add it to your configuration file. The example that follows contains a custom section called in a group named . This section has a single setting, named key1.

Next you must register the section for processing with NameValueSectionHandler, which you identify using its strong name. Notice that the type information shown in the following code must all be entered on a single line. It's broken into multiple lines in this listing to fit the bounds of the page.

Depending on the version of .NET that you have installed, you might need to modify the version information in the type section of the tag. You can check the version information for the System.dll assembly using the Windows Explorer global assembly cache (GAC) extension.

Once you have made this change, retrieving the custom information is easy. First you need to import two namespaces into your application:

Imports System.Configuration
Imports System.Collections.Specialized

Then you simply need to use the ConfigurationSettings.GetConfig method, which retrieves the settings in a collection from a single section. You specify the section in the GetConfig method using a path-like syntax.

Read Header Information from MP3 Files

Problem

You need to read information about the song, artist, and album from an MP3 file.

Solution

Read the ID3v2 tag from the end of the MP3 file.

Discussion

Most MP3 files store information in a 128-byte ID3v2 tag at the end of the file. This tag starts with the word TAG and contains information about the artist, album, and song title in ASCII encoding. You can convert this data from bytes into a string using the Encoding object returned by the System.Text.Encoding.ASCII property.

The MP3TagData class shown here provides access to MP3 data, and it provides a ReadFromFile method that retrieves the information from a valid MP3 file.

Data in the MP3 tag is given a fixed width and is padded with nulls. You must trim these null characters from the string manually. Otherwise, they can cause problems depending on how you use the string in your application.

The following code shows how you can use the MP3TagData class to retrieve and display MP3 information:

Get Started with ZIP Files

Problem

You need to manipulate compressed ZIP archives, either to retrieve file information from a zip or to compress and uncompress individual files.

Solution

Use a dedicated .NET component, such as the freely reusable #ziplib.

Discussion

There are several commercial components that allow you to work with ZIP files. However, there's also at least one fully featured and freely redistributable ZIP component: #ziplib (also known as SharpZipLib), developed by Mike Krueger using a similar open-source Java component. You can download #ziplib with the code samples for this book, or from the Web site http://www.icsharpcode.net/opensource/ sharpziplib. This site includes samples in Visual Basic and C# and limited documentation.

To use #ziplib in a project, simply add a reference to the SharpZipLib.dll assembly and import the following namespace:

Imports ICSharpCode.SharpZipLib.Zip

To retrieve information about the files in a ZIP archive, you could use code similar to this:

You can also use #ziplib to compress and decompress files. Refer to the code samples included with the component for more information.

Get Started with PDF Files

Problem

You want to read data from a PDF file or programmatically generate a PDF file.

Solution

Evaluate a third-party component, or a free open-source component from http://www.sourceforge.net.

Discussion

The PDF file format using a complex multipart format that includes embedded data such as images and fonts. In order to successfully retrieve information from a PDF file, you will need to use a third-party component. Some retail components are available, along with two freely downloadable components on SourceForge. These include the Report.NET library (http://sourceforge.net/projects/report) and the PDF.NET library (http://sourceforge.net/projects/pdflibrary).

Exporting data to a PDF file is conceptually similar to printing it, and you need to explicitly control the coordinates of outputted text and images. The following code snippet shows an extremely simple sample showing how Report.NET can be used to create a basic PDF file using the current 0.06.01 release. In order to use this example, you must add a reference to the Reports.dll assembly and import the Root.Reports namespace.

' Create the PDF File.
Dim Doc As New Report(New PdfFormatter())
' Define the font information.
Dim FontDef As New FontDef(Doc, "Helvetica")
Dim FontProp As New FontPropMM(FontDef, 25)
' Create a new page.
Dim PDFPage As New Page(Doc)
' Add a line of text.
PDFPage.AddCenteredMM(80, New RepString(FontProp, "Hello World!"))
' Save the document.
Doc.Save("SimpleText.pdf")