Chapter 15. Files and Directories

Software development in the 21st century has really turned programmers into a bunch of softies (no pun intended). In the old days of computers, developers had to solder programs into the computer by hand. Complex calculations could take days to set up, and one misplaced wire meant lead poisoning or worse. The suffering was real, and older issues of Popular Electronics are riddled with articles by former programmers who went crazy in their attempt to craft one more ballistics calculation algorithm.

Life improved tremendously for programmers when John von Neumann and others suggested that a computer could store internally the logic for an algorithm, and process it directly from memory instead of through hard-wired configurations. Engineers were soon putting their programs onto punch cards and paper tapes. The danger of lead poisoning was quickly replaced by the larger evil of paper cuts.

Punch cards were great—until you dropped your stack that took you hours or days to assemble. Some programmer somewhere dropped one too many card stacks and proclaimed, “That’s it! I’m going to invent the hard disk and related technologies such as IDE and SCSI. Sure I’ll become fabulously wealthy, but at least I won’t have to deal with these stupid cards anymore.”

And thus was born the filesystem, the structured storage of programs and information on a disk surface. Filesystems have been a part of Microsoft technologies since Bill Gates first wooed IBM. It’s no coincidence that the “DOS” in “MS-DOS” stands for Disk Operating System. Bill knew how essential filesystems were, and so do you.

In this chapter, we’ll talk about interactions with files and directories, the main units of storage and organization in the Windows filesystem. We’ll also see some of the technologies and features .NET provides to manipulate files and their content. Just make sure you turn the pages carefully; I wouldn’t want you to get a paper cut.

Traditional Visual Basic File Management

Visual Basic has included significant file management features since its first release. In fact, more features in Visual Basic deal with file and directory manipulation than with pretty much anything else.

Most of the functions that allow you to read and modify file content use a file handle, a numeric identifier that refers to a specific open file. This file handle is generated with the FreeFile function, and must be obtained before calling any of the traditional Visual Basic file features.

Dim fileID As Integer
fileID = FreeFile(  )
FileOpen(fileID, "C:TestData.txt", OpenMode.Append)
PrintLine(fileID, "Important output to file.")
FileClose(fileID)

File handle-based file manipulation works just fine, but it is so early-’90s. It’s not really a .NET technology, and is not object-based at all (unless you consider that an Integer is an object). Therefore, I won’t be covering it in this book, or using it in the Library Project. Table 15-1 lists the major Visual Basic features that use file handles. If you need to know about the handle-based features in Visual Basic, or if your work involves migrating pre-.NET Visual Basic applications, use this table to help you locate full feature details in the technical documentation supplied with Visual Basic.

Table 15-1. Visual Basic features that use file handles

Feature

Description

EOF

Returns a Boolean indicating whether the current position in the file is at or past the end of the file. Use this function to determine when to stop reading existing data from a file.

FileAttr

Accesses the file attributes currently set on an open file handle.

FileClose

Closes a specific file opened using a file handle.

FileGet

Retrieves structured data from a file and stores it in a matching object.

FileGetObject

Same as FileGet, but with slightly different data typing support.

FileOpen

Opens a file for input or output.

FilePut

Writes an object to a file in a structured manner.

FilePutObject

Same as FilePut, but with slightly different data typing support.

FileWidth

Sets the default line width for formatted text output files.

FreeFile

Returns the next available file handle.

Input

Retrieves a value previously written to a file using Write or WriteLine.

InputString

Retrieves a specific number of characters from an input file.

LineInput

Returns a complete line of input from a file.

Loc

Returns the current byte or record location in the file.

Lock

Locks a file or specific records in a file so that others cannot make changes.

LOF

Returns the length of an open file, in bytes.

Print

Sends text output to a file.

PrintLine

Sends text output to a file, ending it with a line terminator.

Reset

Closes all files currently opened with file handles.

Seek

Gets or sets the current position in a file.

SPC

This function helps format text for output to columnar text files.

TAB

This function helps format text for output to columnar text files.

Unlock

Removes locks previously set with Lock.

Write

Writes data to a file using a consistent format that can be easily read later.

WriteLine

Same as Write, but ends the output with a line terminator.

Manipulating Files Through Streams

The .NET Framework includes a new object-oriented approach to reading and writing files: streams. The abstract Stream object, found at System.IO.Stream, defines a generic interface to a chunk of data. It doesn’t matter where that data is: in a file, in a block of memory, in a String variable—if you have a block of data that can be read or written one byte at a time, you can design a derived stream class to interact with it.

Stream Features

The basic features of a Stream object include the Read and Write methods that let you read or write bytes. As data is read from or written to a stream, the Stream object maintains a “current position” within the stream that you can adjust using the Seek method, or examine using the Position property. The Length property indicates the size of the readable data. The class also exposes variations of these basic features to allow as much flexibility as possible.

Not every stream supports all features. Some streams are read-only, forward-only constructs that don’t support writing or seeking. Other streams support all possible features. The features available to you depend on the type of stream you use. Since Stream itself is abstract, you must create an instance of one of its derived classes. .NET defines several useful streams ready for your use:

FileStream

The FileStream object lets you access the content of a file using the basic methods of the generic Stream class. FileStream objects support reading, writing, and seeking, although if you open a read-only file, you won’t be able to write to it.

MemoryStream

A stream based on a block of raw memory. You can create a memory stream of any size, and use it to temporarily store and retrieve any data.

NetworkStream

This class abstracts data coming over a network socket. Whereas most of the derived stream classes reside in System.IO, this class sits in System.Net.Sockets.

BufferedStream

Adds buffering support to a stream to improve performance on streams with latency issues. You wrap a BufferedStream object around another stream to use it.

CryptoStream

This stream allows you to attach a cryptographic service provider to it, resulting in encrypted output from plain input, or vice versa. Chapter 11 includes examples that use this type of stream.

DeflateStream and GZipStream

Let you use a stream to compress or decompress data as it is processed, all using standard compression algorithms.

Streams are useful on their own, but you can also combine streams so that an incoming network stream can be immediately encrypted, compressed, and stored in a block of stream memory.

Using a Stream

Using a stream is simple; first you create it, and then you start reading and writing bytes left and right. Here’s some sample code I wrote that moves data into and out of a memory stream. It’s loosely based on the code you’ll find in the MSDN documentation for the MemoryStream class.

' ----- The Stream, or There and Back Again.
Dim position As Integer
Dim memStream As IO.MemoryStream
Dim sourceChars(  ) As Byte
Dim destBytes(  ) As Byte
Dim destChars(  ) As Char
Dim asUnicode As New System.Text.UnicodeEncoding(  )

' ----- Create a memory stream with room for 100 bytes.
memStream = New IO.MemoryStream(100)

' ----- Convert the text data to a byte array.
sourceChars = asUnicode.GetBytes( _
   "This is a test of the emergency programming system.")

Try
   ' ----- Store the byte-converted data in the stream.
   memStream.Write(sourceChars, 0, sourceChars.Length)
   ' ----- The position is at the end of the written data.
   '       To read it back, we must move the pointer to
   '       the start again.
   memStream.Seek(0, IO.SeekOrigin.Begin)

   ' ----- Read a chunk of the text/bytes at once.
   destBytes = New Byte(CInt(memStream.Length)) {}
   position = memStream.Read(destBytes, 0, 25)

   ' ----- Get the remaining data one byte at a time,
   '       just for fun.
   While (position < memStream.Length)
      destBytes(position) = CByte(memStream.ReadByte(  ))
      position += 1
   End While

   ' ----- Convert the byte array back to a set of characters.
   destChars = New Char(asUnicode.GetCharCount( _
      destBytes, 0, position)) {}
   asUnicode.GetDecoder(  ).GetChars(destBytes, 0, _
      position, destChars, 0)

   ' ----- Prove that the text is back.
   MsgBox(destChars)
Finally
   memStream.Close(  )
End Try

The comments hopefully make the code clear. After creating a memory stream, I push a block of text into it, and then read it back out. (The text stays in the stream; reading it did not remove it.) Actually, the stream code is pretty simple. Most of the code deals with conversions between bytes and characters. If it looks overly involved, that’s because it is.

Beyond Stream Bytes

For me, all that converting between bytes and characters is for the birds. When I write business applications, I typically deal in dates, numbers, and strings: customer names, order dates, payment amounts, and so on. I rarely have a need to work at the byte level. I sure wish there was a way to send this byte stuff down a programming stream of its own so that I wouldn’t have to see it anymore.

Lucky me! .NET makes some wishes come true. Although you can manipulate streams directly if you really want to or need to, the System.IO namespace also includes several classes that provide a more programmer-friendly buffer between you and the stream. These classes—implemented as distinct readers and writers of stream data—provide simplified methods of storing specific data types, and retrieving them back again.

The readers and writers are designed for single-direction start-to-finish processing of data. After creating or accessing a stream, you wrap that stream with either a reader or a writer, and begin traversing the extent of the stream from the beginning. You always have access to the underlying stream if you need more fine-tuned control at any point.

There are three main pairs of readers and writers:

BinaryReader and BinaryWriter

These classes make it easy to write and later read the core Visual Basic data types to and from a (generally) nontext stream. The BinaryWriter.Write method includes overloads for writing Bytes, Chars, signed and unsigned integers of various sizes, Booleans, Decimals and Doubles, Strings, and arrays and blocks of Bytes and Chars. Curiously missing is an overload for Date values.

The BinaryReader counterpart includes separate Read methods for each of the writable data types. The ReadDouble method returns a Double value from the stream, and there are similar methods for the other data types.

StreamReader and StreamWriter

These classes are typically used to process line-based text files. The StreamReader class includes a ReadLine method that returns the next text line in the incoming stream as a standard String. The related StreamWriter.Write method includes all the overloads of BinaryWriter.Write, and also has a version that lets you format a string for output. The reader includes features that let you read data one character at a time, one block at a time, or one entire file at a time.

StringReader and StringWriter

This pair of classes provides the same features as the StreamReader and StreamWriter pair, but uses a standard String instance for data storage instead of a file.

One additional pair—TextReader and TextWriter—provides the base class for the other nonbinary readers and writers. You can’t create instances of them directly, but they do let you treat the stream and string versions of the readers and writers generically.

With these new tools, it’s easier to process non-Byte data through streams. Here’s a rewrite of the simple memory stream code I wrote earlier, adjusted to use a StreamReader and StreamWriter:

' ----- The Stream, or There and Back Again.
Dim memStream As IO.MemoryStream
Dim forWriting As IO.StreamWriter
Dim forReading As IO.StreamReader
Dim finalMessage As String
Dim asUnicode As New System.Text.UnicodeEncoding(  )

' ----- Create a memory stream with room for 100 bytes.
memStream = New IO.MemoryStream(100)
Try
   ' ----- Wrap the stream with a writer.
   forWriting = New IO.StreamWriter(memStream, asUnicode)

   ' ----- Store the original data in the stream.
   forWriting.WriteLine( _
      "This is a test of the emergency programming system.")
   forWriting.Flush(  )

   ' ----- The position is at the end of the written data.
   '       To read it back, we must move the pointer to
   '       the start again.
   memStream.Seek(0, IO.SeekOrigin.Begin)

   ' ----- Create a reader to get the data back again.
   forReading = New IO.StreamReader(memStream, asUnicode)

   ' ----- Get the original string.
   finalMessage = forReading.ReadToEnd(  )

   ' ----- Prove that the text is back.
   MsgBox(finalMessage)
Finally
   memStream.Close(  )
End Try

That code sure is a lot nicer without all of that conversion code cluttering up the works. (We could simplify it even more by leaving out all of the optional Unicode encoding stuff.) Of course, everything is still being converted to bytes under the surface; the memory stream only knows about bytes. But StreamWriter and StreamReader take that burden away from us, performing all of the messy conversions on our behalf.

Reading a File Via a Stream

Most Stream processing involves files, so let’s use a StreamReader to process a text file. Although we already decided in Chapter 14 that INI files are a thing of the past, it might be fun to write a routine that extracts a value from a legacy INI file. Consider a file containing this text:

[Section0]
Key1=abc
Key2=def

[Section1]
Key1=ghi
Key2=jkl

[Section2]
Key1=mno
Key2=pqr

Now there’s something you don’t see everyday, and with good reason! Still, if we wanted to get the value for Key2 in section Section1 (the “jkl” value), we would have to fall back on the GetPrivateProfileString API call from those bad old pre-.NET programming days. Or, we could implement a StreamReader in a custom function all our own.

Public Function GetINIValue(ByVal sectionName As String, _
      ByVal keyName As String, ByVal iniFile As String) _
      As String
   ' ----- Given a section and key name for an INI file,
   '       return the matching value entry.
   Dim readINI As IO.StreamReader
   Dim oneLine As String
   Dim compare As String
   Dim found As Boolean

   On Error GoTo ErrorHandler

   ' ----- Open the file.
   If (My.Computer.FileSystem.FileExists(iniFile) = False) _
      Then Return ""
   readINI = New IO.StreamReader(iniFile)

   ' ----- Look for the matching section.
   found = False
   compare = "[" & Trim(UCase(sectionName)) & "]"
   Do While (readINI.EndOfStream = False)
      oneLine = readINI.ReadLine(  )
      If (Trim(UCase(oneLine)) = compare) Then
         ' ----- Found the matching section.
         found = True
         Exit Do
      End If
   Loop

   ' ----- Exit early if the section name was not found.
   If (found = False) Then
      readINI.Close(  )
      Return ""
   End If

   ' ----- Look for the matching key.
   compare = Trim(UCase(keyName))
   Do While (readINI.EndOfStream = False)
      ' ----- If we reach another section, then the
      '       key wasn't there.
      oneLine = Trim(readINI.ReadLine(  ))
      If (Len(oneLine) = 0) Then Continue Do
      If (oneLine.Substring(0, 1) = "[") Then Exit Do

      ' ----- Ignore lines without an "=" sign.
      If (InStr(oneLine, "=") = 0) Then Continue Do
     ' ----- See if we found the key. By the way, I'm
      '       using Substring(  ) instead of Left(  ) so
      '       I don't have to worry about conflicts with
      '       Form.Left in case I drop this routine into
      '       a Form class.
      If (Trim(UCase(oneLine.Substring(0, _
            InStr(oneLine, "=") − 1))) = compare) Then
         ' ----- Found the matching key.
         readINI.Close(  )
         Return Trim(Mid(oneLine, InStr(oneLine, "=") + 1))
      End If
   Loop

   ' ----- If we got this far, then the key was missing.
   readINI.Close(  )
   Return ""

ErrorHandler:
   ' ----- Return an empty string on any error.
   On Error Resume Next
   If (readINI IsNot Nothing) Then readINI.Close(  )
   readINI = Nothing
   Return ""
End Function

This routine isn’t an exact replacement for GetPrivateProfileString; it doesn’t support a default return value, or perform file caching for speed. You could improve the routine with better error handling. But it does retrieve the value we seek, and it does it by reading the INI file one line at a time through a StreamReader.

MsgBox(GetINIValue("Section1", "Key2", iniFilePath))
   ' ----- Displays 'jkl'

File Management with the My Namespace

The My namespace includes several file management features in its My.Computer.FileSystem branch, including features that create streams for reading and writing.

My Namespace Versus Visual Basic Commands

Most of the My.Computer.FileSystem object’s members exist to replace or supplement file management features already present in Visual Basic. Table 15-2 lists some of the long-standing file and directory interaction features in Visual Basic, and their equivalents in My.Computer.FileSystem.

Table 15-2. Two ways to do the same thing

Visual Basic feature

Purpose

My.Computer.FileSystem equivalent

ChDir

Change the current “working” directory on a specified or default drive.

The FileSystem.CurrentDirectory property gets and sets the current “working” directory as understood by the application. You set the active directory through an absolute or relative path string.

ChDrive

Change the current “working” drive.

The FileSystem.CurrentDirectory property not only reports or changes the active directory, it also modifies the active drive.

CurDir

Identify the current “working” directory and drive as a full path string.

Once again, FileSystem.CurrentDirectory is the substitute for this Visual Basic directory feature. CurDir does have a little more flexibility: it allows you to determine the current directory on a drive other than the current drive. This can’t be done with FileSystem.CurrentDirectory.

Dir

Retrieve files and directories in a parent directory that match a specific name pattern.

The FileSystem.GetDirectories and FileSystem.GetFiles methods both support wildcard patterns when retrieving matching directory and file names. Dir requires that you call it once for each entry to return, and it doesn’t work well when processing nested directories. The FileSystem equivalents return collections of matching items, and can optionally descend the entire subdirectory tree of a base path.

FileCopy

Make a copy of a file.

The FileSystem.CopyFile provides a few additional user-friendly features beyond FileCopy. But what’s the deal with the reversal of “File” and “Copy”?

FileDateTime

Retrieve the creation or modification date and time of a file.

Use the FileSystem.GetFileInfo method to retrieve a FileInfo object replete with details about a file. You’ll probably focus on the FileInfo.LastWriteTime property, but you can also get the original creation time and the last access time, features not available through the lowly and now disgraced FileDateTime function.

FileLen

Retrieve the length, in bytes, of a file.

Obtain a FileInfo object through the FileSystem.GetFileInfo method, and access that object’s Length property to get the file size in bytes.

GetAttr

Retrieve the attributes of a file as a bit field.

Get details on a file through the FileSystem.GetFileInfo method, and use the returned FileInfo object’s Attributes property to examine your attribute of choice. This object also exposes an IsReadOnly Boolean value.

Kill

Delete a file or empty directory.

The FileSystem.DeleteFile and FileSystem.DeleteDirectory methods replace the Kill procedure, and provide additional options not available with Kill. Plus, you won’t have the police knocking at your door asking why you constantly type Kill, Kill, Kill.

MkDir

Create a new directory.

The FileSystem.CreateDirectory method is a gentle replacement for MkDir. Anyway, “mkdir” is an old Unix command, and you’re not programming on Unix, are you?

Rename

Change the name of a file or directory.

Rename is replaced by distinct FileSystem.RenameFile and FileSystem.RenameDirectory methods.

RmDir

Delete a directory, even if it contains files.

The FileSystem.DeleteDirectory deletes directories that still contain other files, an action that RmDir rejected. There’s also an option to send the files to the Recycle Bin.

SetAttr

Modify the attributes of a file using a bit field.

Same process listed for GetAttr earlier in this table. The FileInfo object’s Attributes and IsReadOnly properties are read/write values, assuming you have the necessary security rights to change attributes.

Why would Microsoft introduce so many new My features that duplicate existing Visual Basic features? Perhaps it’s a way to bring consistency to file-based programming practices through a more object-oriented approach. Or maybe it’s yet another move by Microsoft, the U.S. government, the Knights Templar, Burger King, and other groups set on world domination by controlling you, your family, and your community through the “hidden hand” of extra-long source code statements.

Reading and Writing Files Through My

The My.Computer.FileSystem.OpenTextFileReader and parallel OpenTextFileWriter methods provide shortcuts to the filename-based constructor for StreamReader and StreamWriter objects. The statement:

Dim inputStream As IO.StreamReader = _
   My.Computer.FileSystem.OpenTextFileReader( _
   fileNamePath)

is identical to:

Dim inputStream As New IO.StreamReader(fileNamePath)

For me, the second version is better due to its terse nature, but it’s between you and your source code review team as to which one you will use.

If you want to load the entire contents of a file into either a String or a Byte array, there’s no need to open up a stream now that My includes the My.Computer.FileSystem.ReadAllText and related ReadAllBytes methods. This statement dumps the entire contents of a file into a String:

Dim wholeFile As String = _
   My.Computer.FileSystem.ReadAllText( _
   fileNamePath)

The My.Computer.FileSystem.WriteAllText and WriteAllBytes methods do the same thing, but in the opposite direction. There’s an append Boolean argument that lets you either append or replace the new content relative to any existing content in the file.

My.Computer.FileSystem.WriteAllText( _
   fileNamePath, dataToWrite, True)  ' True=append

One feature that has always been missing from Visual Basic is the ability to conveniently scan a delimited file (such as tab-delimited or comma-delimited) or a fixed-width-field file, and extract the fields on each line without a lot of extra parsing code. Visual Basic now includes the Microsoft.VisualBasic.FileIO.TextFieldParser object that simplifies this process. This object lets you indicate either a field delimiter (such as the tab character) or an array of column sizes. Once you associate it with a file path, it reads each data line, breaking up the distinct fields for you into a string array. The My.Computer.FileSystem.OpenTextFieldParser method opens the file and defines the parsing method in one fell swoop.

Dim dataFields(  ) As String
Dim sourceFile As FileIO.TextFieldParser

' ----- Open the file with tab-delimited fields.
sourceFile = My.Computer.FileSystem.OpenTextFieldParser( _
   sourceFilePath, vbTab)

' ----- Process each line.
Do While Not sourceFile.EndOfData
   dataFields = sourceFile.ReadFields(  )
   ' ----- dataFields is a simple string array,
   '       so you can examine each field directly.
   If (dataFields(0) = "NEW") Then
   ' ----- and so on...
Loop
sourceFile.Close(  )

The TextFieldParser object can also detect comment lines and ignore them silently. I am sure that it’s using a StreamReader secretly hidden inside the object’s black box. Although the internals are hidden from view, the exposed features of this object make it a snap to process field-based text files.

Summary

Managing and manipulating files isn’t brain surgery. But with the filesystem as a major focus of any operating system, tools and methods for reading and updating files just seem to multiply like rabbits. The .NET Framework uses the Stream as its primary file interaction method, so this should help make things simpler. Of course, it piles dozens of wrapper classes on top of the basic stream, but that’s another issue.

As for the management of files and directories, .NET is going in the opposite direction, giving you more and more language and object features to perform the same basic tasks. Beyond the traditional Visual Basic and My namespace features I introduced in this chapter, there are additional duplicate features in the .NET class libraries. Use the methods that meet your needs, and “file” the others away for future reference.

Project

I have some good news and some bad news. The bad news is that the Library Project does not make direct reads or writes of standard files, and has no need for file streams. That means we won’t be adding any code to the project in this chapter at all. The good news is that we still have interesting things to talk about. Besides, I figured that since you had finished more than half of the book, you could use a break.

PROJECT ACCESS

Chapter 15 does not include any project templates, so don’t bother looking in Visual Studio for them.

Configuring Log Output

Whenever an error occurs in the Library application, the GeneralError routine first shows the error message to the user, and then logs it to any configured “log listeners.”

Public Sub GeneralError(ByVal routineName As String, _
      ByVal theError As System.Exception)
   ' ----- Report an error to the user.
   On Error Resume Next

   MsgBox("The following error occurred at location '" & _
      routineName & "':" & vbCrLf & vbCrLf & _
      theError.Message, MsgBoxStyle.OkOnly Or _
      MsgBoxStyle.Exclamation, ProgramTitle)
   My.Application.Log.WriteException(theError)
End Sub

So, who’s listening? If you are running the program within Visual Studio, Visual Basic always configures a log listener that displays the text in the Immediate Window panel. But that doesn’t do much good in a compiled and deployed application.

You can design your own log listeners, but .NET also includes several predefined listeners, all of which can be enabled and configured through the application’s app.config file. If you access the “After” version of Chapter 14’s project, you will find content in its app.config file that sets up one such listener. Here’s a portion of that file, showing just the relevant sections:

<system.diagnostics>
  <sources>
    <!-- This section defines the logging configuration
         for My.Application.Log -->
    <source name="DefaultSource" switchName="DefaultSwitch">
      <listeners>
        <add name="FileLog"/>
      </listeners>
    </source>
  </sources>

  <switches>
    <add name="DefaultSwitch" value="Information" />
  </switches>

  <sharedListeners>
    <add name="FileLog" type=
    "Microsoft.VisualBasic.Logging.FileLogTraceListener,
    Microsoft.VisualBasic, Version=8.0.0.0, Culture=neutral,
    PublicKeyToken=b03f5f7f11d50a3a,
    processorArchitecture=MSIL"
    initializeData="FileLogWriter"/>
  </sharedListeners>
</system.diagnostics>

The <sharedListeners> section defines the details for a particular log listener. In this case, it’s the FileLogTraceListener listener, a class in the Microsoft.VisualBasic.Logging namespace. It’s enabled in the <source>/<listeners> section, where it’s included through an <add> tag. There’s a lot of stuff here that seems bizarre or extremely picky (such as the public key token). Fortunately, it’s all documented in MSDN if you ever need the details.

The FileLogTraceListener listener sends relevant logging data to an application-specific logfile. By default in Windows Vista, the file resides in the following:

C:UsersusernameApplication Data
CompanyProductVersionAppName.log

The username part is replaced by the name of the currently logged-in user. The Company, Product, and Version parts represent the company name, product name, and version number of your assembly as defined in its assembly attributes. AppName is the name of your application with the .exe extension stripped off. On my Windows Vista system, the logfile for the Library Project appears here:

C:UsersusernameApplication Data
ACMELibrary1.0.0.0Library.log

If you don’t like that location, you can change the output to any location you choose. To do it, you’ll need to alter the <add> tag in the <sharedListeners> section, adding two additional attributes to that tag.

<sharedListeners>
  <add name="FileLog" type=
     "Microsoft.VisualBasic.Logging.FileLogTraceListener,
     Microsoft.VisualBasic, Version=8.0.0.0, Culture=neutral,
     PublicKeyToken=b03f5f7f11d50a3a,
     processorArchitecture=MSIL"
     initializeData="FileLogWriter"
     location="Custom"
customLocation="c:	emp" />
</sharedListeners>

The new location and customLocation attributes do the trick. Set the customLocation attribute to the directory where the logfile should go. These attributes link to properties of the same name in the FileLogTraceListener class. Visual Studio’s documentation describes these properties and attributes, plus others that are available for you to configure through app.config.

This app.config change is based on an MSDN article titled “How to: Write Event Information to a Text File” that you can search for in your online help. (Use the Search feature, not the Index feature.)

Other Log Output Options

Another MSDN article, “Walkthrough: Changing Where My.Application.Log Writes Information,” describes how to send log output to more than just a simple text file. It discusses ways to log application information to the system Event Log, to a delimited file, to an XML-formatted file, and to the console display.

Some of the changes you need to make to the app.config file are, again, mysterious, so I’ll just list them here for your examination. Add the following content to the app.config file to define the available listeners:

<add name="EventLog"
  type="System.Diagnostics.EventLogTraceListener,
     System, Version=2.0.0.0,
     Culture=neutral, PublicKeyToken=b77a5c561934e089"
     initializeData="sample application"/>

<add name="Delimited"
  type="System.Diagnostics.DelimitedListTraceListener,
  System, Version=2.0.0.0,
  Culture=neutral, PublicKeyToken=b77a5c561934e089"
  initializeData="c:	empSomeFile.txt"
  delimiter=";;;"
  traceOutputOptions="DateTime" />

<add name="XmlWriter"
  type="System.Diagnostics.XmlWriterTraceListener,
  System, Version=2.0.0.0,
  Culture=neutral, PublicKeyToken=b77a5c561934e089"
  initializeData="c:	empSomeFile.xml" />
<add name="Console"
  type="System.Diagnostics.ConsoleTraceListener,
  System, Version=2.0.0.0,
  Culture=neutral, PublicKeyToken=b77a5c561934e089"
  initializeData="true" />

The initializeData attribute in each entry contains the values sent to the arguments of the relevant class constructor. Other attributes (except for type) modify the properties of the same name in the class specified through the type attribute. For all the options available to you for each listener, look up its class entry in the Visual Studio documentation.

To enable any of these listeners, use an <add> tag in the <source>/<listeners> section. The following XML block enables all the listeners defined in this chapter’s project:

<sources>
  <!-- This section defines the logging configuration
       for My.Application.Log -->
  <source name="DefaultSource" switchName="DefaultSwitch">
    <listeners>
      <add name="FileLog"/>
      <add name="EventLog" />
      <add name="Delimited" />
      <add name="XmlWriter" />
      <add name="Console" />
    </listeners>
  </source>
</sources>

Obtaining a Bar Code Font

Since we have a little time left, let’s talk about obtaining a bar code font. The Library Project will include bar code printing support, but only if you have a bar code font installed on your system. It’s no emergency, but you should obtain one before you reach Chapter 18, where we develop the bar code configuration code.

When you downloaded the code for this book, it didn’t include a bar code font. It’s all due to licensing issues and the like, you understand. But bar code fonts are easy to get. You can purchase a professional bar code font if you want to, and if you plan to deploy this project into an actual library setting, you probably should. But if you’re only reading this book for the great humor, you can download one of the many free bar code fonts available on the Internet. I’ve included some links to bar code font providers on the web site where you obtained the source code for this book. Even if you don’t plan to use the bar code printing features, I recommend that you download a free bar code font just so that you can try out some of the Chapter 18 features.

Once you’ve installed the font, you will need to tell the Library program to use it. The settings form we designed in the previous chapter included a selection field for this font. It’s the Barcode Font Name field on the System-Wide tab of the Maintenance form. You can see it in the middle of Figure 14-6. I made it a system-wide setting because it seemed best to have all administrators in a single library using a common font.

If your font is a “Code 3 of 9” bar code font (also called “Code 39”), make sure you select the Barcode is “Code 39” or “Code 3 of 9” field on that same form. (The provider of the font will let you know whether it is a Code 3 of 9 font or not.) These fonts require an asterisk before and after the bar code number. Selecting this field will cause the Library program to add the asterisk characters automatically.

Well, I’m getting tired of talking about files, be they fonts or config files. In the next chapter, we’ll go back into the world of code and its fraternal twin, data.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.107.64