Visual Basic 2005: A Developer's Notebook/.NET 2.0 Platform Services

From WikiContent

Jump to: navigation, search
Visual Basic 2005: A Developer's Notebook

In earlier chapters, you learned about the most profound changes in .NET 2.0, including new features in Windows Forms, ASP.NET web applications, and ADO.NET data access. These changes are impressive, but they're only part of the story. In fact, Microsoft developers have been hard at work tweaking and fine-tuning the entire .NET class library. If you look around, you'll find new members, types, and namespaces cropping up everywhere.

Contents

Easily Log Events

When something goes wrong in your application, the user is rarely in a position to fix the problem. Instead of showing a detailed message box, it's much more important to make sure all the details are recorded somewhere permanent, so you can examine them later to try to diagnose the problem. In previous versions of .NET, logging was straightforward but tedious. In VB 2005, life becomes much easier thanks to the My.Application.Log object.

How do I do that?

You can use the new My.Application.Log object to quickly write to an XML file, an ordinary text file, or the Windows event log.

To write a log message with My.Application.Log, you simply need to use the WriteEntry( ) method. You supply a string message as the first parameter, and (optionally) two more parameters. The second parameter is the event type, which indicates whether the message represents information, a warning, an error, and so on. The third parameter is an exception object, the details of which will also be copied into the log entry.

Note

When something bad happens in your application, you want an easy way to log it to a file or event log. Look no further than the My.Application.Log object.

To try this out, create and run the console application in Example 6-1, which writes a short string of text to the log.

Example 6-1. Simple logging

Module LogTest
    
    Sub Main( )
      My.Application.Log.WriteEntry("This is a test!", _
        TraceEventType.Information)
    End Sub
    
End Module

Clearly, the logging code is extremely simple—but where are the log entries recorded? It all depends on the configuration of your application. .NET uses trace listeners, which are dedicated classes that listen to log messages and then copy them to another location (such as a file, event log, and so on). When you call the WriteEntry( ) method, the entry is written to the current set of trace listeners (which are exposed through the My.Application.TraceSource collection). By default, these listeners include the FileLogTraceListener, which writes to a user logfile. This file is stored under a user-specific directory (which is defined by the user's APPDATA environment variable) in a subdirectory of the form [CompanyName]\[ProductName]\[FileVersion], where CompanyName, ProductName, and FileVersion refer to the information defined in the application assembly. For example, if the Windows user JoeM runs the application LogTestApp, the logfile will be created in a directory such as c:\Documents and Settings\JoeM\Application Data\MyCompany\LogTestApp\1.0.0.0\LogTestApp.log.

Once you've found the right directory, you can open the logfile in Notepad to examine the text contents. You'll see the following information:

Note

To configure assembly information, double-click the My Project item in the Solution Explorer, select the Application tab, and then click the Assembly Information button.

Microsoft.VisualBasic.MyServices.Log.WindowsFormsSource    Information
    0    This is a test!

The number 0 represents the information log type. Subsequent entries append data to this logfile. Data is never removed (unless you delete the file by hand).

What about...

...logging to other locations? .NET includes a number of pre-built trace listeners that you can use. They include:

DefaultTraceListener
This listener writes information into the debug portion of the window in Visual Studio. It's primarily useful while testing.
FileLogTraceListener
This listener writes information to the application logfile named [AssemblyName].log. The default location of the logfile depends on the user's environment settings and the application information.
EventLogTraceListener
This listener writes information to the Windows event log.
XmlWriterTraceListener
This listener writes information to a file in XML format. You specify the location where the file should be stored. If needed, the directory will be created automatically.

By default, every new Visual Basic application you create starts its life with two trace listeners: a DefaultTraceListener and a FileLogTraceListener. To add new trace listeners, you need to modify the application configuration file. In Visual Studio, you can double-click the app.config item in the Solution Explorer. Trace-listener information is specified in two subsections of the <system.diagnostics> section.

Warning

The logging configuration settings have changed with newer builds. For a version of the code that's updated to work with the latest build, download the samples from this book's web site.

In the <sharedListeners> subsection, you define the trace listeners you want to have the option of using, specify any related configuration properties, and assign a descriptive name. Here's an example that defines a new listener for writing XML data to a logfile:

Note

Remember, after the application is built, the app.config file is renamed to have the name of the application, plus the extension .config.

<sharedListeners>
  <add name="MyXmlLog" type="System.Diagnostics.XmlWriterTraceListener"
    initializeData="c:\MyLog.xml" />
</sharedListeners>

In the <sources> subsection, you name the trace listeners you want to use, choosing from the <sharedListeners> list:

<source name="Microsoft.VisualBasic.MyServices.Log.WindowsFormsSource">
  <listeners>
    <add name="Xml"/>
  </listeners>
</source>

This separation between the <sharedListeners> section and the <sources> section allows you to quickly switch trace listeners on and off, without disturbing their configuration settings.

You can now re-run the application shown in Example 6-1. Now it will write the message to an XML file named MyLog.xml in the root C: directory. Here's what the contents look like (with the schema information removed for better readability):

<E2ETraceEvent>
  <System>
    <EventID>0</EventID>
    <Type>0</Type>
    <TimeCreated SystemTime="2004-07-26T16:14:04.7533392Z" />
    <Source Name="Microsoft.VisualBasic.MyServices.Log.WindowsFormsSource" />
    <Execution ProcessName="LogSample.vshost" ProcessID="3896" ThreadID="8" />
    <Computer>FARIAMAT</Computer>
  </System>
  <ApplicationData>
    <System.Diagnostics>
      <Message>This is a test!</Message>
               <Severity>Information</Severity>
    </System.Diagnostics>
  </ApplicationData>
</E2ETraceEvent>

Example 6-2 shows a complete configuration file example. It enables file tracing, event log tracing, and XML log tracing. Notice that the EventLogTraceListener is fine-tuned with a filter that ensures only error messages are logged.

Example 6-2. Logging data to three different trace listeners

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.diagnostics>
    
    <!-- Enable all three trace listeners
        (from the <sharedListeners> section). -->
    <sources>
      <source name="Microsoft.VisualBasic.MyServices.Log.WindowsFormsSource"
       switchName="DefaultSwitch">
        <listeners>
          <add name="FileLog"/>
          <add name="EventLog"/>
          <add name="Xml"/>
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="DefaultSwitch" value="Information" />
    </switches>
        
    <!-- Define three trace listeners that you might want to use. -->
    <sharedListeners>
      <add name="FileLog" type="System.Diagnostics.FileLogTraceListener"
       initializeData="FileLogWriter" delimiter=";" />
      <add name="EventLog" type="System.Diagnostics.EventLogTraceListener"
       initializeData="MyApplicationLog">
        <filter type="System.Diagnostics.SeverityFilter" initializeData="Error" />
      </add>
      <add name="Xml" type="System.Diagnostics.XmlWriterTraceListener"
       initializeData="c:\SampleLog.xml" delimiter=";"/> 
    </sharedListeners>
  </system.diagnostics>
</configuration>

You can now use the same simple application to simultaneously write the ordinary logfile, an XML logfile, and an entry in the Windows event log named Application.

Unfortunately, there isn't any high-level .NET API for retrieving information from a log. If the log information is stored in a file, you can use the FileStream and StreamReader classes from the System.IO namespace to read the file one line at a time. If you've entered information in the Windows event log, you'll need to rely on the EventLog class, which you can find in the System.Diagnostics namespace.

Note

The event log is a list of messages stored by the operating system for a specific period of time. To view the event log, choose Event Viewer from the Administrative Tools section of the Control Panel.

Where can I learn more?

For more information, look up the following classes in the MSDN help: DefaultTraceListener, FileLogTraceListener, EventLogTraceListener, and XmlWriterTraceListener.

Ping Another Computer

The Internet is a dynamic network where computers appear and drop out of sight without warning. One simple test an application can always perform to check if a computer is reachable is to send a ping message. Technically, a ping is the equivalent of asking another computer, "Are you there?" To get its answer, ping sends a special type of message over a low-level Internet protocol called ICMP (Internet Control Message Protocol).

Note

Need to find out if a computer is reachable over the Internet? With the new Ping class, you can make this simple request without a tangle of low-level socket code.

Sending a ping message using the classes found in the System.Net namespaces is challenging and requires dozens of low-level code statements that deal with raw sockets. In .NET 2.0, there's a much simpler solution with the new Ping class in the System.Net.NetworkInformation namespace.

How do I do that?

To ping a computer, you use the Ping( ) method of the My.Computer.Network object. This approach gives you convenient access to the bare minimum ping functionality. The Ping( ) method returns True or False depending on whether it received a response from the computer you're trying to contact.

Note

Windows includes a utility called ping.exe that you can use to ping other computers at the command line.

Example 6-3 uses this method in order to contact the web server at www.yahoo.com.

Example 6-3. Pinging a remote computer

Module PingTest
    
    Sub Main( )
        Dim Success As Boolean
    
        ' Try to contact www.yahoo.com (wait 1000 milliseconds at most,
        ' which is the default if you don't specify a timeout).
        Success = My.Computer.Network.Ping("www.yahoo.com", 1000)
        Console.WriteLine("Did the computer respond? " & Success)
    End Sub
    
End Module

When you call Ping( ), you specify two parameters: the URL or IP address for the computer you're trying to reach (e.g., www.microsoft.com or 123.4.123.4) and, optionally, a maximum wait time in milliseconds. Once this limit is reached, the request times out, and the Ping( ) method returns False to indicate the failure.

Warning

A ping message is a low-level test that doesn't necessarily correspond to the availability of services on a particular computer. For example, even if you can ping www.yahoo.com, that doesn't mean that its search engine web pages are available and working properly. Similarly, web servers or firewalls often reject ping messages to restrict the possibility of someone launching a denial of service attack by flooding the computer with millions of spurious requests. For that reason, if you ping www.microsoft.com, you won't receive a response, even though you can still surf to their web site using that address.

What about...

...getting more information from the remote computer? The My.Computer.Network object doesn't return any additional information about the results of the ping test. For example, you won't find out how long it took to receive a response, which is a key statistic used by some applications, such as peer-to-peer software, to rank the connection speed of different computers.

To get more information, you need to head directly to the Ping class in the System.Net.NetworkInformation namespace. It returns a PingResult object with several pieces of information, including the time taken for a response. The following code snippet puts this approach to the test. It assumes that you've imported the System.Net.NetworkInformation namespace:

Dim Pinger As New Ping
Dim Reply As PingReply = Pinger.Send("www.yahoo.com")
Console.WriteLine("Time (milliseconds): " & Reply.RoundTripTime)
Console.WriteLine("Exact status: " & Reply.Status.ToString( ))
Console.WriteLine("Adress contacted: " & Reply.Address.ToString( ))

Here's some sample output:

Time (milliseconds): 61
Exact status: Success
Adress contacted: 216.109.118.78

The Ping class also provides a SendAsync( ) method you can use to ping a computer without stalling your code (you can handle the response in another thread when a callback fires), and other overloaded versions of the Send( ) method that allow you to set low-level options (like the number of hops the ping message will travel before expiring).

Where can I learn more?

To use this added networking muscle, read up on the Ping class in the MSDN Help.

Get Information About a Network Connection

Some applications need to adjust how they work based on whether a network connection is present. For example, imagine a sales reporting tool that runs on the laptop of a traveling sales manager. When the laptop is plugged into the network, the application needs to run in a connected mode in order to retrieve the information it needs, such as a list of products, directly from a database or web service. When the laptop is disconnected from the network, the application needs to gracefully degrade to a disconnected mode that disables certain features or falls back on slightly older data that's stored in a local file. To make the decision about which mode to use, an application needs a quick way to determine the network status of the current computer. Thanks to the new My.Computer.Network object, this task is easy.

Note

Need to find out if your computer's currently online? With the My class, this test is just a simple property away.

How do I do that?

The My.Computer.Network object provides a single IsAvailable property that allows you to determine if the current computer has a network connection. The IsAvailable property returns True as long as at least one of the configured network interfaces is connected, and it serves as a quick-and-dirty test to see if the computer is online. To try it out, enter the following code in a console application:

If My.Computer.Network.IsAvailable Then
    Console.WriteLine("You have a network interface.")
End If

If you want more information, you need to turn to the System.Net and System.Net.NetworkInformation namespaces, which provide much more fine-grained detail. For example, to retrieve and display the IP address for the current computer, you can use the System.Net.Dns class by entering this code:

' Retrieve the computer name.
Dim HostName As String = System.Net.Dns.GetHostName( )
Console.WriteLine("Host name: " & HostName)
    
' Get the IP address for this computer.
' Note that this code actually retrieves the first
' IP address in the list, because it assumes the
' computer only has one assigned IP address
' (which is the norm).
Console.WriteLine("IP: " & _
  System.Net.Dns.GetHostByName(HostName).AddressList(0).ToString( ))

Here's the output you might see:

Host name: FARIAMAT
IP: 192.168.0.197

In addition, you can now retrieve even more detailed information about your network connection that wasn't available in previous versions of .NET. To do so, you need to use the new System.Net.NetworkInformation.IPGlobalProperties class, which represents network activity on a standard IP network.

The IPGlobalProperties class provides several methods that allow you to retrieve different objects, each of which provides statistics for a specific type of network activity. For example, if you're interested in all the traffic that flows over your network connection using TCP, you can call IPGlobalProperties.GetTcpIPv4Statistics(). For most people, this is the most useful measurement of the network. On the other hand, if you're using a next-generation IPv6 network, you need to use IPGlobalProperties.GetTcpIPv6Statistics() . Other methods exist for monitoring traffic that uses the UPD or ICMP protocols. Obviously, you'll need to know a little bit about networking to get the best out of these methods.

Tip

IP (Internet Protocol) is the core building block of most networks and the Internet. It uniquely identifies computers with a four-part IP address, and allows you to send a basic packet from one machine to another (without any frills like error correction, flow control, or connection management). Many other networking protocols, such as TCP (Transmission Connection Protocol) are built on top of the IP infrastructure, and still other protocols are built on top of TCP (e.g., HTTP, the language of the Web). For more information about networking, refer to a solid introduction such as Internet Core Protocols (O'Reilly).

The following code retrieves detailed statistics about the network traffic. It assumes that you've imported the System.Net.NetworkInformation namespace:

Dim Properties As IPGlobalProperties = IPGlobalProperties.GetIPGlobalProperties( )
Dim TcpStat As TcpStatistics
TcpStat = Properties.GetTcpIPv4Statistics( )
    
Console.WriteLine("TCP/IPv4 Statistics:")
Console.WriteLine("Minimum Transmission Timeout... : " & _
  TcpStat.MinimumTransmissionTimeOut)
Console.WriteLine("Maximum Transmission Timeout... : " & _
  TcpStat.MaximumTransmissionTimeOut)
    
Console.WriteLine("Connection Data:")
Console.WriteLine("  Current  .................... : " & _
  TcpStat.CurrentConnections)
Console.WriteLine("  Cumulative .................. : " & _
  TcpStat.CumulativeConnections)
Console.WriteLine("  Initiated ................... : " & _
  TcpStat.ConnectionsInitiated)
Console.WriteLine("  Accepted .................... : " & _
  TcpStat.ConnectionsAccepted)
Console.WriteLine("  Failed Attempts ............. : " & _
  TcpStat.FailedConnectionAttempts)
Console.WriteLine("  Reset ....................... : " & _
  TcpStat.ResetConnections)
    
Console.WriteLine( )
Console.WriteLine("Segment Data:")
Console.WriteLine("  Received  ................... : " & _
  TcpStat.SegmentsReceived)
Console.WriteLine("  Sent ........................ : " & _
  TcpStat.SegmentsSent)
Console.WriteLine("  Retransmitted ............... : " & _
  TcpStat.SegmentsResent)

Here's the output you might see:

TCP/IPv4 Statistics:
Minimum Transmission Timeout... : 300
Maximum Transmission Timeout... : 120000
Connection Data:
  Current  .................... : 6
  Cumulative .................. : 29
  Initiated ................... : 10822

Note

Statistics are kept from the time the connection is established. That means every time you disconnect or reboot your computer, you reset the networking statistics.

  Accepted .................... : 41
  Failed Attempts ............. : 187
  Reset ....................... : 2271
    
Segment Data:
  Received  ................... : 334791
  Sent ........................ : 263171
  Retransmitted ............... : 617

What about...

...other connection problems, like a disconnected router, erratic network, or a firewall that's blocking access to the location you need? The network connection statistics won't give you any information about the rest of the network (although you can try to ping a machine elsewhere on the network, as described in the previous lab, "Ping Another Computer"). In other words, even when a network connection is available there's no way to make sure it's working. For that reason, whenever you need to access a resource over the network—whether it's a web service, database, or application running on another computer—you need to wrap your call in proper exception-handling code.

Where can I learn more?

For more information on advanced network statistics, look up the "IPGlobalProperties" index entry in the MSDN help, or look for the "network information sample" for a more sophisticated Windows Forms application that monitors network activity.

Upload and Download Files with FTP

Earlier versions of .NET didn't include any tools for FTP (File Transfer Protocol), a common protocol used to transfer files to and from a web server. As a result, you either had to purchase a third-party component or write your own (which was easy in principle but difficult to get right in practice).

Note

Need to upload files to an FTP site or download existing content? New support is available in VB 2005.

In .NET 2.0, a new FtpWebRequest class neatly fills the gap. However, the FtpWebRequest class has its own complexities, so Microsoft programmers simplified life for VB developers even further by extending the My.Computer.Network object to provide two quick access methods for completing basic FTP operations. These are UploadFile(), which sends a file to a remote server, and DownloadFile( ), which retrieves a file and stores it locally.

How do I do that?

Whether you use the FtpWebRequest class or the My.Computer.Network object, all FTP interaction in .NET is stateless. That means that you connect to the FTP site, perform a single operation (like transferring a file or retrieving a directory listing), and then disconnect. If you need to perform another operation, you need to reconnect. Fortunately, this process of connecting and logging in is handled automatically by the .NET Framework.

The easiest way to use FTP in a VB application is to do so through the My.Computer.Network object. If you use its FTP methods, you never need to worry about the tedious details of opening, closing, and reading streams. To download a file, the bare minimum information you need is the URL that points to the FTP site and the path that points to the local file. Here's an example:

My.Computer.Network.DownloadFile( _
  "ftp://ftp.funet.fi/pub/gnu/prep/gtk.README", "c:\readme.txt")

This command retrieves the file that is on the FTP site ftp.funet.fi in the path /pub/gnu/prep/gtk.README and copies it to the local file c:\readme.txt.

Uploading uses similar parameters, but in reverse:

My.Computer.Network.UploadFile("c:\newfile.txt", _
  "ftp://ftp.funet.fi/pub/newfile.txt")

This command copies the local file newfile.txt from the directory c:\ to the FTP site ftp.funet.fi, in the remote directory /pub.

Both DownloadFile( ) and UploadFile( ) support several overloads that take additional parameters, including credentials (the username and password information you might need to log on to a server) and a timeout parameter to set the maximum amount of time you'll wait for a response before giving up (the default is 1,000 milliseconds).

Unfortunately, the DownloadFile( ) and UploadFile( ) methods haven't been too robust in beta builds of Visual Basic 2005, and the methods may fail to work. An option that works better is the more sophisticated FtpWebRequest class. Not only does it perform more reliably, but it also fills a few glaring gaps in the FTP support provided by the My.Network.Computer. Because FtpWebRequest allows you to execute any FTP command, you can use it to retrieve directory listings, get file information, and more.

Note

Internet Explorer has its own built-in FTP browser. Just type a URL that points to an FTP site (like ftp://ftp.microsoft.com) into the IE address bar to browse what's there. You can use this tool to verify that your code is working correctly.

To use the FtpWebRequest class, you need to follow several steps. First, pass the URL that points to the FTP site to the shared WebRequest.Create( ) method:

Dim Request As FtpWebRequest 
Request = CType(WebRequest.Create("ftp://ftp.microsoft.com/MISC"), _
  FtpWebRequest)

The WebRequest.Create() method examines the URL and returns the appropriate type of WebRequest object. Because FTP URLs always start with the scheme ftp://, the Create( ) method will return a new FtpWebRequest object.

Once you have the FtpWebRequest, you need to choose what FTP operation you want to perform by setting the FtpWebRequest.Method property with the text of the FTP command. Here's an example for retrieving directory information with the LIST command:

Request.Method = "LIST"

Once you've chosen the FTP operation you want to perform, the last step is to execute the command and read the response. The tricky part is the fact that the response is returned to you as a stream of text. It's up to you to move through this block of text line by line with a StreamReader and parse the information.

For example, the following code reads through a returned directory listing and displays each line in a Console window:

Dim Response As FtpWebResponse = CType(Request.GetResponse( ), FtpWebResponse)
Dim ResponseStream As Stream = Response.GetResponseStream( )
Dim Reader As New StreamReader(ResponseStream, System.Text.Encoding.UTF8)
    
Dim Line As String
Do
    Line = Reader.ReadLine( )
    Console.WriteLine(Line)
Loop Until Line = ""

The output looks like this:

dr-xr-xr-x   1 owner    group               0 Jul  3  2002 beckyk
-r-xr-xr-x   1 owner    group           15749 Apr  8  1994 CBCP.TXT
dr-xr-xr-x   1 owner    group               0 Jul  3  2002 csformat
dr-xr-xr-x   1 owner    group               0 Aug  1  2002 DAILYKB
-r-xr-xr-x   1 owner    group             710 Apr 12  1993 DISCLAIM.TXT
dr-xr-xr-x   1 owner    group               0 Jul  3  2002 FDC
dr-xr-xr-x   1 owner    group               0 Jul  3  2002 friKB
dr-xr-xr-x   1 owner    group               0 Jul  3  2002 FULLKB
dr-xr-xr-x   1 owner    group               0 Jul  3  2002 Homenet
-r-xr-xr-x   1 owner    group              97 Sep 28  1993 INDEX.TXT
...

Clearly, if you want to manipulate individual pieces of information (like the file size) or distinguish files from directories, you'll need to do extra work to parse the text returned by the StreamReader.

Finally, when you're finished with the FTP request and response, you need to close the streams:

Reader.Close( )
Response.Close( )

To put it all in context, it helps to consider a simple FTP browsing application. Figure 6-1 shows a sample application that's included with the downloadable samples for this chapter.

Figure 6-1. A simple FTP Browser application

A simple FTP Browser application

This Windows application includes the following controls:

  • A TextBox where you can enter a URL that points to a file or directory in an FTP site.
  • A Button named Query Directory that retrieves the folders and files at a given URL. This task requires the FtpWebRequest class.
  • A Button named Download File that downloads the file at a given URL. This task uses the My.Computer.Network.DownloadFile( ) method.
  • A FolderBrowserDialog that allows you to choose a folder where the downloaded file will be saved.
  • A ListView that shows the directory and file listing for the URL. This list is refreshed every time you click the Query Directory button. In addition, every time you click to select an item in the ListView, that information is automatically added to the URL in the text box. This allows you to quickly browse through an FTP site, drilling down several layers into the directory structure and selecting the file that interests you.

Example 6-4 shows code for the FTP browser form

Example 6-4. The FTP browser form

Public Class FtpForm
    Inherits System.Windows.Forms.Form

    ' Stores the path currently shown in the ListView.
    Private CurrentPath As String

    Private Sub cmdQuery_Click(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles cmdQuery.Click
        ' Check the URI is valid.
        Dim RequestUri As Uri = ValidateUri(txtFtpSite.Text)
        If RequestUri Is Nothing Then Return

        ' Clear the ListView.
        listDir.Items.Clear( )

        ' Create a new FTP request using the URI.
        Dim Request As FtpWebRequest
        Request = CType(WebRequest.Create(RequestUri), FtpWebRequest)

        ' Use this request for getting full directory details.
        Request.Method = "LIST" 
        Request.UsePassive = False

        Dim Response As FtpWebResponse
        Dim ResponseStream As Stream
        Dim Reader As StreamReader
        Try
            ' Execute the command and get the response.
            Response = CType(Request.GetResponse( ), FtpWebResponse)
            Debug.WriteLine("Status: " & Response.StatusDescription)

            ' Read the response one line at a time.
            ResponseStream = Response.GetResponseStream( )
            Reader = New StreamReader(ResponseStream, System.Text.Encoding.UTF8)
            Dim Line As String
            Do
                Line = Reader.ReadLine( )
                If Line <> "" Then
                    Debug.WriteLine(Line)

                    ' Extract just the file or directory name from the line.
                    Dim ListItem As New ListViewItem(Line.Substring(59).Trim( ))
                    If Line.Substring(0, 1) = "d" Then
                        ListItem.ImageKey = "Folder"
                    Else
                        ListItem.ImageKey = "File"
                    End If
                    listDir.Items.Add(ListItem)
                End If
            Loop Until Line = ""

            ' Operation completed successfully. Store the current FTP path.
            CurrentPath = RequestUri.ToString( )

        Catch Ex As Exception
            MessageBox.Show(Ex.Message)

        Finally
            ' Clean up.
            Reader.Close( )
            Response.Close( )

        End Try
    End Sub

    Private Sub cmdDownload_Click(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles cmdDownload.Click

        ' Check the URI is valid.
        Dim RequestUri As Uri = ValidateUri(txtFtpSite.Text)
        If RequestUri Is Nothing Then Return

        ' Prompt the user to choose a destination folder.
        ' Default the file name to the same file name used on the FTP server.
        dlgSave.FileName = Path.GetFileName(txtFtpSite.Text)
        If dlgSave.ShowDialog( ) <> Windows.Forms.DialogResult.OK Then
            Return
        End If

        ' Create a new FTP request using the URI.
        Dim Request As FtpWebRequest
        Request = CType(WebRequest.Create(RequestUri), FtpWebRequest)

        ' Use this request for downloading the file.
        Request.UsePassive = False
        Request.Method = "RETR"
 
        Dim Response As FtpWebResponse
        Dim ResponseStream, TargetStream As Stream
        Dim Reader As StreamReader
        Dim Writer As StreamWriter
        Try
            ' Execute the command and get the response.
            Response = CType(Request.GetResponse( ), FtpWebResponse)
            Debug.WriteLine("Status: " & Response.StatusDescription)
            Debug.WriteLine("File Size: " & Response.ContentLength)

            ' Create the destination file.
            TargetStream = New FileStream(dlgSave.FileName, FileMode.Create)
            Writer = New StreamWriter(TargetStream)

            ' Write the response to the file.
            ResponseStream = Response.GetResponseStream( )
            Reader = New StreamReader(ResponseStream, System.Text.Encoding.UTF8)
            Writer.Write(Reader.ReadToEnd( ))

        Catch Err As Exception
            MessageBox.Show(Err.Message)

        Finally
            ' Clean up.
            Reader.Close( )
            Response.Close( )
            Writer.Close( )
        End Try
    End If

    End Sub

    Private Function ValidateUri(ByVal uriText As String) As Uri
        Dim RequestUri As Uri
        Try
            ' Check that the string is interpretable as a URI.
            RequestUri = New Uri(uriText)

            ' Check that the URI starts with "ftp://"
            If RequestUri.Scheme <> Uri.UriSchemeFtp Then
                RequestUri = Nothing
            End If
        Catch
            RequestUri = Nothing
        End Try

        If RequestUri Is Nothing Then
            MessageBox.Show("Invalid Uri.")
        Else

        End If
        Return RequestUri
    End Function

    Private Sub listDir_SelectedIndexChanged(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles listDir.SelectedIndexChanged
        ' When a new item is selected in the list, add this 
        ' to the URI in the text box.
        If listDir.SelectedItems.Count <> 0 Then
            CurrentPath = CurrentPath.TrimEnd("/")
            txtFtpSite.Text = CurrentPath & "/" & listDir.SelectedItems(0).Text
        End If
    End Sub

End Class

The most complex code found in this example occurs in the event handler for the cmdQuery button, which retrieves a directory listing, parses out the important information, and updates the ListView.

Where can I learn more?

In previous builds, the MSDN help included much more information on FTP access and different FTP operations under the index entry "FtpMethods," complete with useful demonstrations of the different methods. This entry has disappeared in recent builds (along with the FtpMethods class), but check for it to return. In the meantime, you can read up on the FTP protocol and supported commands at www.vbip.com/winsock/winsock_ftp_ref_01.asp.

Test Group Membership of the Current User

The .NET Framework has always provided security classes that let you retrieve basic information about the account of the current user. The new My.User object provided by Visual Basic 2005 makes it easier than ever to access this information.

Note

Find out who's using your application, and the groups a mystery user belongs to.

How do I do that?

Applications often need to test who is running the application. For example, you might want to restrict some features to certain groups, such as Windows administrators. You can accomplish this with the My.User object.

The My.User object provides two key properties that return information about the current user. These are:

IsAuthenticated
Returns True if the current user account information is available in the My.User object. The only reason this information wouldn't be present is if you've created a web application that allows anonymous access, or if the current Windows account isn't associated with the application domain.
Username
Returns the current username. Assuming you're using a Windows security policy, this is the Windows account name for the user, in the form ComputerName\UserName or DomainName\UserName.

The My.User object also provides a single method, IsInRole( ). This method accepts the name of a group (as a string) and then returns True if the user belongs to that group. For example, you could use this technique to verify that the current user is a Windows administrator before performing a certain task.

To try this out, use the following console application in Example 6-5, which displays some basic information about the current user and tests if the user is an Administrator.

Note

To check the user and group list for the current computer (or make changes), select Computer management from the Administrative Tools section of the Control Panel. Then, expand the System Tools → Local Users and Groups node.

Example 6-5. Testing the current user identity

Module SecurityTest
    
    Sub Main( )
        ' Use Windows security. As a result, the User object will
        ' provide the information for the currently logged in user
        ' who is running the application.
        My.User.InitializeWithWindowsUser( )
    
        Console.WriteLine("Authenticated: " & My.User.Identity.IsAuthenticated)
        Console.WriteLine("User: " & My.User.Identity.Username)
    
        Console.WriteLine("Administrator: " & My.User.IsInRole("Administrators"))
    End Sub
    
End Module

Here's the sort of output you'll see when you run this test:

Authenticated: True
User: FARIAMAT\Matthew
Administrator: True

Encrypt Secrets for the Current User

Applications often need a way to store private data in a file or in memory. The obvious solution is symmetric encryption, which scrambles your data using a random series of bytes called a secret key. The problem is that when you want to decrypt your scrambled data, you need to use the same secret key you used to encrypt. This introduces serious complications. Either you need to find a secure place to safeguard your secret key (which is tricky at best), or you need to derive the secret key from some other information, like a user-supplied password (which is much more insecure, and can break down entirely when users forget their passwords).

Note

Need a quick way to encrypt secret information, without needing to worry about key management? The long awaited solution appears in . NET 2.0 with the ProtectedData class.

The ideal solution is to have the Windows operating system encrypt the data for you. To accomplish this, you need the DPAPI (Data Protection API), which encrypts data using a symmetric key that's based on a piece of user-specific or machine-specific information. This way, you don't need to worry about key storage or authentication. Instead, the operating system authenticates the user when he logs in. Data stored by one user is automatically inaccessible to other users.

In previous versions of .NET, there were no managed classes for using the DPAPI. This oversight is corrected in .NET 2.0 with the new ProtectedData class in the System.Security.Cryptography namespace.

How do I do that?

The ProtectedData class provides two shared methods. ProtectData( ) takes a byte array with source data and returns a byte array with encrypted data. UnprotectData( ) performs the reverse operation, taking an encrypted byte array and returning a byte array with the decrypted data.

The only trick to using the ProtectData( ) and UnprotectData( ) methods is that you can only encrypt or decrypt data in a byte array. That means that if you want to encrypt strings, numbers, or something else, you need to write it to a byte array before you perform the encryption.

To see this in action, you can run the console application code in Example 6-6.

Example 6-6. Storing an encrypted string of text in a file

Imports System.Security.Cryptography
Imports System.IO
    
Module ProctedData
    
    Sub Main( )     
        ' Get the data.
        Console.WriteLine("Enter a secret message and press enter.")
        Console.Write(">")
        Dim Input As String = Console.ReadLine( )
    
        Dim DataStream As MemoryStream
        If Input <> "" Then
            Dim Data( ), EncodedData( ) As Byte
    
            ' Write the data to a new MemoryStream.
            DataStream = New MemoryStream( )
            Dim Writer As New StreamWriter(DataStream)
            Writer.Write(Input)
            Writer.Close( )
    
            ' Convert the MemoryStream into a byte array,
            ' which is what you need to use the ProtectData( ) method.
            Data = DataStream.ToArray( )
    
            ' Encrypt the byte array.
            EncodedData = ProtectedData.Protect(Data, Nothing, _
              DataProtectionScope.CurrentUser)
    
            ' Store the encrypted data in a file.
            My.Computer.FileSystem.WriteAllBytes("c:\secret.bin", 
             EncodedData, False)
        End If
    End Sub
    
End Module

When you run this application, you'll be prompted to type in some text, which will be encrypted using your current user account information and stored in the file secret.bin. The data won't be accessible to any other user.

To verify that the data is encrypted, you have two choices. You can open the file and take a look for yourself, or you can modify the code so that it reads the data directly from the encrypted memory stream. This code tries the latter, and displays a string of meaningless gibberish as a result:

' Verify the data is encrypted by reading and displaying it
' without performing any decryption.
DataStream = New MemoryStream(EncodedData)
Dim Reader As New StreamReader(DataStream)
Console.WriteLine("Encrypted data: " & Reader.ReadToEnd( ))
Reader.Close( )

To decrypt the data, you need to place it into a byte array and then use the UnprotectData( ) method. To extract your data out of the unencrypted byte array, you can use a StreamReader. To add decryption support to the previous example, insert the following code, which opens the file and displays the secret message that you entered earlier:

If My.Computer.FileSystem.FileExists("c:\secret.bin") Then
    Dim Data( ), EncodedData( ) As Byte
    
    EncodedData = My.Computer.FileSystem.ReadAllBytes("c:\secret.bin")
    Data = ProtectedData.Unprotect(EncodedData, Nothing, _
      DataProtectionScope.CurrentUser)
    
    Dim DataStream As New MemoryStream(Data)
    Dim Reader As New StreamReader(DataStream)
    
    Console.WriteLine("Decoded data from file: " & Reader.ReadToEnd( ))
    Reader.Close( )
End If

Remember, because the data is encrypted using the current user profile, you can decrypt the data at any time. The only restriction is that you need to be logged on under the same user account.

Note that when you protect data, you must choose one of the values from the DataProtectionScope enumeration. There are two choices:

Note

No matter which DataProtectionScope you choose, the encrypted information will be stored in a specially protected area of the Windows registry.

LocalMachine
Windows will encrypt data with a machine-specific key, guaranteeing that no one can read the data unless they log in to the same computer. This works well for server-side applications that run without user intervention, such as Windows services and web services.
CurrentUser
Windows will encrypt data with a user-specific key, so that it's inaccessible to any other user.

In the current example, user-specific data is stored. However, you could modify the DataProtectionScope to store data that's accessible to any user on the current computer.

What about...

...protecting data before you put it in a database? Once you use the ProtectedData class to encrypt your data, you can put it anywhere you want. The previous example wrote encrypted data to a file, but you can also write the binary data to a database record. To do so, you simply need a binary field in your table with enough room to accommodate the encrypted byte array. In SQL Server, you use the varbinary data type.

Unleash the Console

.NET 1.0 introduced the Console class to give programmers a convenient way to build simple command-line applications. The first version of the Console was fairly rudimentary, with little more than basic methods like Write( ), WriteLine( ), Read( ), and ReadLine( ). In .NET 2.0, new features have been added, allowing you to clear the window, change foreground and background colors, alter the size of the window, and handle special keys.

Note

At last, a Console class with keyboard-handling and screen-writing features.

How do I do that?

The best way to learn the new features is to see them in action. Example 6-7 shows a simple application, ConsoleTest, which lets the user move a happy face character around a console window, leaving a trail in its wake. The application intercepts each key press, checks if an arrow key was pressed, and ensures that the user doesn't move outside of the bounds of the window.

Warning

In order for the advanced console features to work, you must disable the Quick Console window. The Quick Console is a console window that appears in the design environment, and it's too lightweight to support features like reading keys, setting colors, and copying characters. To disable it, select Tools → Options, make sure the "Show all settings checkbox" is checked, and select the Debugging → General tab. Then, turn off the "Redirect all console output to the Quick Console window."

Example 6-7. Advanced keyboard handling with the console

Module ConsoleTest
    
    Private NewX, NewY, X, Y As Integer
    Private BadGuyX, BadGuyY As Integer
    
    Public Sub Main( )
        ' Create a 50 column x 20 line window.
        Console.SetWindowSize(50, 20)
        Console.SetBufferSize(50, 20)
    
        ' Set up the window.
        Console.Title = "Move The Happy Face"
        Console.CursorVisible = False
        Console.BackgroundColor = ConsoleColor.DarkBlue
        Console.Clear( )
    
        ' Display the happy face icon.
        Console.ForegroundColor = ConsoleColor.Yellow
        Console.SetCursorPosition(X, Y)
        Console.Write("˘")
    
        ' Read key presses.
        Dim KeyPress As ConsoleKey
        Do
            KeyPress = Console.ReadKey( ).Key
    
            ' If it's an arrow key, set the requested position.
            Select Case KeyPress
                Case ConsoleKey.LeftArrow
                    NewX -= 1
                Case ConsoleKey.RightArrow
                    NewX += 1
                Case ConsoleKey.UpArrow
                    NewY -= 1
                Case ConsoleKey.DownArrow
                    NewY += 1
            End Select
    
            MoveToPosition( )
        Loop While KeyPress <> ConsoleKey.Escape
    
        ' Return to normal.
        Console.ResetColor( )
        Console.Clear( )
    End Sub
    
    Private Sub MoveToPosition( )
        ' Check for an attempt to move off the screen.
        If NewX = Console.WindowWidth Or NewX < 0 Or _
          NewY = Console.WindowHeight Or NewY < 0 Then
            ' Reset the position.
            NewY = Y
            NewX = X
            Console.Beep( )
        Else
            ' Repaint the happy face in the new position.
            Console.MoveBufferArea(X, Y, 1, 1, NewX, NewY)
    
            ' Draw the trail.
            Console.SetCursorPosition(X, Y)
            Console.Write("*")
    
            ' Update the position.
            X = NewX
            Y = NewY
            Console.SetCursorPosition(0, 0)
        End If
    End Sub
    
End Module

To try this out, run the application and use the arrow keys to move about. Figure 6-2 shows the output of a typical ConsoleTest session.

Figure 6-2. A fancy console application

A fancy console application

Some of the new Console methods used in ConsoleTest include the following:

Clear( )
Erases everything in the console window and positions the cursor in the top-left corner.
SetCursorPosition( )
Moves the cursor to the designated x- and y-coordinates (measured from the top-left corner). Once you've moved to a new position, you can use Console.Write( ) to display some characters there.
SetWindowSize( ) and SetBufferSize( )
Allow you to change the size of the window (the visible area of the console) and the buffer (the scrollable area of the console, which is equal to or greater than the window size).
ResetColor( )
Resets the foreground and background colors to their defaults.
Beep( )
Plays a simple beep, which is often used to indicate invalid input.
ReadKey( )
Reads just a single key press and returns it as a ConsoleKeyInfo object. You can use this object to easily tell what key was pressed (including extended key presses like the arrow keys) and what other keys were held down at the time (like Alt, Ctrl, or Shift).
MoveBufferArea( )
Copies a portion of the console window to a new position, and erases the original data. This method offers a high-performance way to move content around the console.

The new Console properties include:

Title
Sets the window caption.
ForegroundColor
Sets the text color that will be used the next time you use Console.Write( ) or Console.WriteLine( ).
BackgroundColor
Sets the background color that will be used the next time you use Console.Write( ) or Console.WriteLine( ). To apply this background color to the whole window at once, call Console.Clear() after you set the background color.
CursorVisible
Hides the blinking cursor when set to False.
WindowHeight and WindowWidth
Returns or sets the dimensions of the console window.
CursorLeft and CursorTop
Returns or moves the current cursor position.

What about...

...reading a character from a specified position of the window? Sadly, the new Console class provides no way to do this. That means that if you wanted to extend the happy face example so that the user must navigate through a maze of other characters, you would need to store the position of every character in memory (which could get tedious) in order to check the requested position after each key press, and prevent the user from moving into a space occupied by another character.

Where can I learn more?

To learn more about the new Console class and its new properties and methods, look for the Console and ConsoleKeyInfo classes in the MSDN help library reference.

Time Your Code

Timing code isn't difficult. You can use the DateTime.Now property to capture the current date and time down to the millisecond. However, this approach isn't perfect. Constructing the DateTime object takes a short time, and that little bit of latency can skew the time you record for short operations. Serious profilers need a better approach, one that uses low-level systems calls and has no latency.

Note

The new Stopwatch class allows you to track how fast your code executes with unparalleled precision.

How do I do that?

In .NET 2.0, the best way to time your code is to use the new Stopwatch class in the System.Diagnostics namespace. The Stopwatch class is refreshingly simple to use. All you need to do is create an instance and call the Start( ) method. When you're finished, call Stop( ).

Example 6-8 shows a simple test that times how long a loop takes to finish. The elapsed time is then displayed in several different ways, with different degrees of precision.

Example 6-8. Timing a loop

Module TimeCode
    
    Sub Main( )
        Dim Watch As New Stopwatch( )
    
        Watch.Start( )
    
        ' Delay for a while.
        For i As Integer = 1 To 1000000000
        Next
    
        Watch.Stop( )
    
        ' Report the elasped time.
        Console.WriteLine("Milliseconds " & Watch.ElapsedMilliseconds)
        Console.WriteLine("Ticks: " & Watch.ElapsedTicks)
        Console.WriteLine("Frequency: " & Stopwatch.Frequency)
        Console.WriteLine("Whole Seconds: " & Watch.Elapsed.Seconds)
        Console.WriteLine("Seconds (from TimeSpan): " & Watch.Elapsed.TotalSeconds)
        Console.WriteLine("Seconds (most precise): " & _
          Watch.ElapsedTicks / Stopwatch.Frequency)
    End Sub
    
End Module

Here's the output you'll see:

Milliseconds 10078
Ticks: 36075265
Frequency: 3579545
Whole Seconds: 10
Seconds (from TimeSpan): 10.0781705
Seconds (most precise): 10.078170549609

You can retrieve the elapsed time in milliseconds from the Stopwatch.ElapsedMilliseconds property. (One second is 1,000 milliseconds.) The ElapsedMilliseconds property returns a 64-bit integer (a Long), making it extremely precise. If it's more useful to retrieve the time as a number of seconds or minutes, use the Stopwatch.Elapsed property instead, which returns a TimeSpan object.

On the other hand, if you want the greatest possible precision, retrieve the number of ticks that have elapsed from the Stopwatch.ElapsedTicks property. Stopwatch ticks have a special meaning. When you use the TimeSpan or DateTime object, a tick represents 0.0001 of a millisecond. In the case of a Stopwatch, however, ticks represent the smallest measurable increment of time, and depend on the speed of the CPU. To convert Stopwatch ticks to seconds, divide ElapsedTicks by Frequency.

What about...

...pausing a timer? If you want to record the total time taken to complete multiple operations, you can use Stop( ) to pause a timer and Start( ) to resume it later. You can then read the total time taken for all the operations you timed from the Elasped and ElaspedMilliseconds properties.

You can also run multiple timers at once. All you need to do is create one Stopwatch object for each distinct timer you want to use.

Deploy Your Application with ClickOnce

One of the driving forces behind the adoption of browser-based applications is the fact that organizations don't need to deploy their applications to the client. Most companies are willing to accept the limitations of HTML in order to avoid the considerable headaches of distributing application updates to hundreds or thousands of users.

Note

Want the functionality of a rich client application with the easy deployment of a web application? ClickOnce offers a new solution for deploying your software.

Deploying a .NET client application will never be as straightforward as updating a web site. However, .NET 2.0 includes a new technology called ClickOnce that simplifies deployment dramatically.

How do I do that?

ClickOnce includes a few remarkable features:

  • ClickOnce can automatically create a setup program that you can distribute on a CD or launch over a network or through a web page. This setup program can install prerequisites and create the appropriate Start menu icons.
  • ClickOnce can configure your application to check for updates automatically every time it starts (or periodically in the background). Depending on your preference, you can give the user the option of downloading and running the new updated version, or you can just install it by force.
  • ClickOnce can configure your application to use an online-only mode. In this case, the user always runs the latest version of your application from a web page URL. However, the application itself is cached locally to improve performance.

ClickOnce is tightly integrated with Visual Studio 2005, which allows you to deploy a ClickOnce application to a web site using the Project → Publish menu command.

The following steps take you through the process of preparing your project for publication:

  1. Using Visual Studio 2005, create a new project. A good choice is a Windows Forms application. Before continuing, save the project.
  2. Choose Build → Publish [ ProjectName ] (or right-click your project in the Solution Explorer and choose Publish). This launches the Publish wizard, which gives you a chance to specify or change various settings.
  3. The first dialog page of the Publish wizard (the "Where do you want to publish" dialog) prompts you to choose the location where you will publish the files to be deployed (see Figure 6-3). This location is the file path or the virtual directory on your web server where you want to deploy the application. For a simple test, use a URL that starts with http://localhost/ (which refers to the current computer). Click Next to continue.

    Tip

    When Visual Studio publishes the application, it will automatically create a subdirectory named publish in the current application directory, and it will map this to the virtual directory path you've selected.

    Figure 6-3. Choosing a deployment directory

    Choosing a deployment directory

  4. Next, choose the install mode (see Figure 6-4) by clicking one of the buttons on the "Will the application be available offline" dialog page. Select "Yes, this application is available online or offline." This way, the setup will add application icons to the Start menu. If you choose "No, this application is only available online," the user will only be able to run it by surfing to the virtual directory to which it's been published. Click Next to continue.

    Figure 6-4. Choosing the install mode

    Choosing the install mode

  5. The Publish wizard now displays a summary of your settings. Click Finish to publish it. (You can publish an updated version at any time by selecting Build → Publish [ ProjectName ] from the menu.)

Once the wizard completes, the automatically generated ClickOnce web page is launched, as shown in Figure 6-5. Using this page, a user can click to download and install your application. Try it out by clicking the Install [ AppName ] link.

Figure 6-5. The ClickOnce installation page

The ClickOnce installation page

The installation runs without any messages, unless it needs to ask for user consent. For example, before the installation can add an icon to the Start menu, it needs to prompt the user.

Best of all, now that the application is in place, you can make use of its automatic update ability. To test this out, return to the application in Visual Studio .NET and change the main form (perhaps by adding a new button). Then, increment the version number of the application. (To do this, double-click the My Project item in the Solution Explorer, select the Application tab, and click the AssemblyInfo button. A dialog box will appear that lets you set assembly metadata, including the version number.) Finally, republish the application.

When a new version is available on the server, client applications will update themselves automatically, based on their update settings. If you run the installed sample application, it checks for updates when it starts. In this case, it will detect the new version and prompt you to install the update.

Warning

The ClickOnce plumbing has been tweaked and refined continuously during the beta cycle. In some builds of Visual Studio, you may encounter an error when you try to publish a project using ClickOnce. Unfortunately, there aren't any workarounds.

What about...

...computers that don't have the .NET Framework 2.0 installed? These machines can't download and install a ClickOnce application automatically. However, when they surf to the ClickOnce installation page, they will see a link that will install the required prerequisites. There are a number of other approaches you can pursue to get .NET 2.0 installed on the client ahead of time. One easy choice is to use the Windows Update feature (surf to http://windowsupdate.microsoft.com from the client computer).

Where can I learn more?

There are a number of articles that discuss the ClickOnce technology in much greater detail. For more information, you may want to refer to the book Essential ClickOnce (Addison Wesley, forthcoming), or the introduction from MSDN magazine at http://msdn.microsoft.com/msdnmag/issues/04/05/ClickOnce. You can also find a great deal of information in the MSDN help library, and online at http://msdn.microsoft.com/clickonce.

Personal tools