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. |
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.
Create a FileInfo instance for the file, and use its properties and methods.
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.
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. |
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 and |
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, and |
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.
Public Module FileInformation Public Sub Main(ByVal args() As String) If args.Length = 0 Then Console.WriteLine("Please supply a filename.") Else Dim FileName As String = args(0) Dim CheckFile As New FileInfo(FileName) ' Display file information. Console.WriteLine("Checking file: " & CheckFile.Name) Console.WriteLine("In directory: " & CheckFile.DirectoryName) Console.WriteLine("File exists: " & CheckFile.Exists.ToString()) If CheckFile.Exists Then Console.Write("File created: ") Console.WriteLine(CheckFile.CreationTime.ToString()) Console.Write("File last updated: ") Console.WriteLine(CheckFile.LastWriteTime.ToString()) Console.Write("File last accessed: ") Console.WriteLine(CheckFile.LastAccessTime.ToString()) Console.Write("File size (bytes): ") Console.WriteLine(CheckFile.Length.ToString()) Console.Write("File attribute list: ") Console.WriteLine(CheckFile.Attributes.ToString()) ' Uncomment these lines to display the full file content. 'Dim r As StreamReader = CheckFile.OpenText() 'Console.WriteLine(r.ReadToEnd()) 'r.Close() Console.ReadLine() End If End If End Sub End Module
Here is the output you might expect:
Checking file: ConsoleApplication1.exe In directory: E:TempConsoleApplication1in File exists: True File created: 29/05/2002 1:53:28 PM File last updated: 25/11/2002 9:10:29 AM File last accessed: 25/11/2002 9:50:56 AM File size (bytes): 7680 File attribute list: Archive
Note |
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. |
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.
Create a DirectoryInfo instance for the directory, and use its properties and methods.
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.
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.
Public Module DirectoryInformation Public Sub Main(ByVal args() As String) If args.Length = 0 Then Console.WriteLine("Please supply a directory name.") Else Dim DirectoryName As String = args(0) ' Display directory information. Dim CheckDir As New DirectoryInfo(DirectoryName) Console.WriteLine("Checking Directory: " & CheckDir.Name) Console.WriteLine("In directory: " & CheckDir.Parent.Name) Console.Write("Directory exists: ") Console.WriteLine(CheckDir.Exists.ToString()) If CheckDir.Exists Then Console.Write("Directory created: ") Console.WriteLine(CheckDir.CreationTime.ToString()) Console.Write("Directory last updated: ") Console.WriteLine(CheckDir.LastWriteTime.ToString()) Console.Write("Directory last accessed: ") Console.WriteLine(CheckDir.LastAccessTime.ToString()) Console.Write("Directory attribute list: ") Console.WriteLine(CheckDir.Attributes.ToString()) Console.WriteLine("Directory contains: " & _ CheckDir.GetFiles.Length.ToString() & " files") End If End If Console.ReadLine() End Sub End Module
Here is the output you might expect:
Checking directory: bin In directory: ConsoleApplication1 Directory exists: True Directory created: 2002-05-29 1:53:14 PM Directory last updated: 2002-11-21 10:48:47 AM Directory last accessed: 2002-11-25 9:55:06 AM Directory attribute list: Directory Directory contains: 13 files
You want to retrieve file version information (such as the publisher of a file, its revision number, associated comments, and so on).
Use the GetVersionInfo method of the System.Diagnostics.FileVersionInfo class.
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:
Public Module FileVersionInformation Public Sub Main(ByVal args() As String) If args.Length = 0 Then Console.WriteLine("Please supply a filename.") Else Dim FileName As String = args(0) Dim Info As FileVersionInfo Info = FileVersionInfo.GetVersionInfo(FileName) ' Display version information. Console.WriteLine("Checking File: " & Info.FileName) Console.WriteLine("Product Name: " & Info.ProductName) Console.WriteLine("Product Version: " & Info.ProductVersion) Console.WriteLine("Company Name: " & Info.CompanyName) Console.WriteLine("File Version: " & Info.FileVersion) Console.WriteLine("File Description: " & Info.FileDescription) Console.WriteLine("Original Filename: " & Info.OriginalFilename) Console.WriteLine("Legal Copyright: " & Info.LegalCopyright) Console.WriteLine("InternalName: " & Info.InternalName) Console.WriteLine("IsDebug: " & Info.IsDebug) Console.WriteLine("IsPatched: " & Info.IsPatched) Console.WriteLine("IsPreRelease: " & Info.IsPreRelease) Console.WriteLine("IsPrivateBuild: " & Info.IsPrivateBuild) Console.WriteLine("IsSpecialBuild: " & Info.IsSpecialBuild) End If Console.ReadLine() End Sub End Module
Here's the output this code produces with the sample file c:windowsexplorer.exe (supplied as a command-line argument):
Checking File: c:windowsexplorer.exe Product Name: Microsoftr Windowsr Operating System Product Version: 6.00.2600.0000 Company Name: Microsoft Corporation File Version: 6.00.2600.0000 (xpclient.010817-1148) File Description: Windows Explorer Original Filename: EXPLORER.EXE Legal Copyright: c Microsoft Corporation. All rights reserved. InternalName: explorer IsDebug: False IsPatched: False IsPreRelease: False IsPrivateBuild: False IsSpecialBuild: False
You want to correct examine or modify file attribute information.
Use bitwise arithmetic with the And and Or keywords.
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
You want to read or write data from a binary file.
Use the BinaryReader or BinaryWriter to wrap the underlying FileStream.
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. |
You need to process multiple files based on a filter expression (such as *.txt or rec03??.bin).
Use the overloaded version of the DirectoryInfo.GetFiles method that accepts a filter expression.
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. |
You need to perform a task with all the files in the current directory and any subdirectories.
Use the DirectoryInfo.GetFiles method to retrieve a list of files in a directory, and use recursion to walk through all subdirectories.
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:
Dim Searcher As New FileSearcher("*.txt") ' Perform a single-directory search. Searcher.Search("c: emp", False) ' Display results. Console.WriteLine("Search results:") Dim File As FileInfo For Each File In Searcher.Matches Console.WriteLine(File.FullName) Next ' Perform a recursive directory search. Searcher.Search("c: emp", True) ' Display results. Console.WriteLine("Recursive search results:") For Each File In Searcher.Matches Console.WriteLine(File.FullName) Next
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.
You need to perform a search for a file that contains specific text.
Search through a file character-by-character using the FileStream.ReadByte method, and try to build up a matching string.
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
You need to show a directory tree with the TreeView control, but filling the directory tree structure at startup is too time consuming.
React to the BeforeExpand event to fill in subdirectories just before they are displayed.
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:
Private Sub treeFiles_BeforeExpand(ByVal sender As Object, _ ByVal e As TreeViewCancelEventArgs) Handles treeFiles.BeforeExpand ' Check for the dummy node. If e.Node.Nodes(0).Text = "*" Then ' Disable redraw. treeFiles.BeginUpdate() e.Node.Nodes.Clear() Fill(e.Node) ' Enable redraw. treeFiles.EndUpdate() End If End Sub
You need to quickly compare the content of two files.
Calculate the hash code of each file using the HashAlgorithm class, and compare the hash codes.
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.
You need to react when a file system change is detected in a specific path (such as a file modification or creation).
Use the FileSystemWatcher component, which monitors a path and raises events when files or directories are modified.
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:
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:
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:
Watch.NotifyFilter = NotifyFilters.CreationTime Or NotifyFilters.DirectoryName
You want to get a file name that you can use for a temporary file.
Use the shared Path.GetTempFileName method.
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.
Dim TempFile As String = Path.GetTempFileName() Console.WriteLine("Using " & TempFile) Dim fs As New FileStream(TempFile, FileMode.Create) ' (Write some data.) fs.Close() ' Now delete the file. File.Delete(TempFile)
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.
You want to retrieve the path where the current executable is stored.
Read the shared StartupPath property of the System.Windows.Forms.Application class.
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.
Console.Write("Executable is: ") Console.WriteLine(System.Windows.Forms.Application.ExecutablePath) Console.Write("Executable is executing in: ") Console.WriteLine(System.Windows.Forms.Application.StartupPath)
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).
You want to set the current working directory so you can use relative paths in your code.
Use the shared Directory.GetCurrentDirectory and Directory.SetCurrentDirectory methods.
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.
Here's a simple test that demonstrates these concepts:
Console.WriteLine("Using: " & Directory.GetCurrentDirectory()) Console.Write("The relative path myfile.txt will automatically become ") Console.WriteLine(Path.GetFullPath("myfile.txt")) Console.WriteLine("Changing current directory to c:") Directory.SetCurrentDirectory("c:") Console.Write("The relative path myfile.txt will automatically become ") Console.WriteLine(Path.GetFullPath("myfile.txt"))
The output for this example might be the following:
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. |
You need to store data in a file, but your application doesn't run with the required FileIOPermission.
Use a user-specific isolated store.
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.
You need to store application-specific settings that can be modified easily without recompiling code.
Read settings from an application configuration file.
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. |
You want to use a custom configuration setting to organize related custom settings.
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.
.NET uses an extensible system of configuration file settings. You have multiple options for reading custom settings from a configuration file:
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.
Dim Settings As NameValueCollection Settings = CType( _ ConfigurationSettings.GetConfig("mySectionGroup/mySection"), _ NameValueCollection) ' Displays "value1" Console.WriteLine(Settings("key1"))
You need to read information about the song, artist, and album from an MP3 file.
Read the ID3v2 tag from the end of the MP3 file.
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.
Public Class MP3TagData Private _Artist As String Private _SongTitle As String Private _Album As String Private _Year As String Public ReadOnly Property Artist() As String Get Return _Artist End Get End Property Public ReadOnly Property SongTitle() As String Get Return _SongTitle End Get End Property Public ReadOnly Property Album() As String Get Return _Album End Get End Property Public ReadOnly Property Year() As String Get Return _Year End Get End Property Public Sub ReadFromFile(ByVal filename As String) ' Clear existing values. _SongTitle = "" _Artist = "" _Album = "" _Year = "" Dim fs As New FileStream(filename, FileMode.Open) ' Read the MP3 tag. fs.Seek(0 - 128, SeekOrigin.End) Dim Tag(2) As Byte fs.Read(Tag, 0, 3) ' Verify that a tag exists. If System.Text.Encoding.ASCII.GetString(Tag).Trim() = "TAG" Then _SongTitle = GetTagData(fs, 30) _Artist = GetTagData(fs, 30) _Album = GetTagData(fs, 30) _Year = GetTagData(fs, 4) End If fs.Close() End Sub Private Function GetTagData(ByVal stream As Stream, _ ByVal length As Integer) As String ' Read the data. Dim Bytes(length - 1) As Byte stream.Read(Bytes, 0, length) Dim TagData As String = System.Text.Encoding.ASCII.GetString(Bytes) ' Trim nulls. Dim TrimChars() As Char = {Char.Parse(" "), Char.Parse(vbNullChar)} TagData = TagData.Trim(TrimChars) Return TagData End Function End Class
Note |
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:
Dim MP3Tag As New MP3TagData() MP3Tag.ReadFromFile("c:mp3mysong.mp3") Console.WriteLine("Album: " & MP3Tag.Album) Console.WriteLine("Artist: " & MP3Tag.Artist) Console.WriteLine("Song: " & MP3Tag.SongTitle) Console.WriteLine("Year: " & MP3Tag.Year)
You need to manipulate compressed ZIP archives, either to retrieve file information from a zip or to compress and uncompress individual files.
Use a dedicated .NET component, such as the freely reusable #ziplib.
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:
Dim ZipStream As New ZipInputStream(File.OpenRead("test.zip")) Dim Entry As ZipEntry = ZipStream.GetNextEntry() Do Until Entry Is Nothing Console.WriteLine("Name: " & Entry.Name) Console.WriteLine("Date: " & Entry.DateTime.ToString()) Console.WriteLine("Uncompressed Size: " & Entry.Size.ToString()) Console.WriteLine("Compressed Size: " + Entry.CompressedSize.ToString()) Console.WriteLine() Entry = ZipStream.GetNextEntry() Loop ZipStream.Close()
Here's an example of the output this code can generate for a ZIP archive containing three files:
Name: COPYING.txt Date: 12/07/2001 4:49:48 PM Uncompressed Size: 18349 Compressed Size: 6956 Name: Documentation.chm Date: 15/07/2002 10:21:12 AM Uncompressed Size: 321684 Compressed Size: 266795 Name: Readme.pdf Date: 25/07/2002 11:05:08 AM Uncompressed: 82763 Compressed Size: 79186
You can also use #ziplib to compress and decompress files. Refer to the code samples included with the component for more information.
You want to read data from a PDF file or programmatically generate a PDF file.
Evaluate a third-party component, or a free open-source component from http://www.sourceforge.net.
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")
Introduction