导航

Improving .NET Compact Framework HTTP Communications using HttpWebRequest and Custom ASP.NET Providers

Jim Wilson
JW Hedgehog, Inc.

October 2003

Applies to
   Microsoft® .NET Compact Framework
   Microsoft® Visual Studio® .NET 2003

Summary: Get technical insight into improving .NET Compact Framework HTTP communications using HttpWebRequest and customized ASP.NET to build efficient, interactive HTTP components. There is a test at the end to verify your code. (21 printed pages)

Download HttpCommunicationsSampleSetup.exe.

Contents

Introduction
Building an HTTP Client
Building Interactive Server Components
Conclusion

Introduction

HTTP provides a simple and powerful communications mechanism for mobile applications. In cases where large amounts of data need to be transferred or data is more efficiently transferred in binary form (such as MP3s or bitmaps), it may be more efficient to take direct control of the HTTP communications process. This is especially true for mobile applications that are often faced with low bandwidth connectivity or communication billing rates based on the number of megabytes transferred. In these situations, operational performance and costs can be improved by transferring only the core application data.

This article examines how to use the HTTP capabilities of the .NET Compact Framework and the customizable behavior of ASP.NET to build efficient, interactive HTTP components.

Building an HTTP Client

Before we can dig into the details of communicating between an HTTP client and an ASP.NET custom handler, it is important to understand the details of managing HTTP communications within the .NET Compact Framework. The best way to do this is to build a simple .NET Compact Framework program to manage uploading and downloading files with HTTP.

The .NET Compact Framework provides two HTTP communications classes: HttpWebRequest and HttpWebResponse. Together, these classes provide the necessary functionality to both send an HTTP request (HttpWebRequest) to a web server and receive a response (HttpWebResponse) back from that server.

Note   HttpWebRequest and HttpWebResponse are derived from the abstract classes WebRequest and WebResponse, respectively. These abstract classes generically model the behavior of sending and receiving data. Similarly file based operations can be performed using the derived classes FileWebRequest and FileWebResponse.

HTTP File Upload

Creating the Upload Functions

First we will look at the process of performing an HTTP file upload. To get started, lets use the Microsoft® Visual Studio® .NET "New Project" Wizard to create a Smart Device Application. The application will be a Microsoft® Windows® application targeting the Pocket PC.

Let's add a method to the application form named UploadFile. UploadFile accepts two string arguments. The first is the name of the local file to read. The second is the HTTP URL identifying the destination server and the name of the file to create on the server.

public void UploadFile(string localFile , string uploadUrl)

The actual upload requires an instance of HttpWebRequest. As is the case with all WebRequest derived classes, HttpWebRequest cannot be created with the new operator but rather is created using the static (shared in VB.NET) WebRequest Create method. WebRequest.Create accepts a URL and returns the appropriate WebRequest derived class as specified by the URL. URLs of the form "http://..." create an instance of HttpWebRequest. In our case, we pass the uploadUrl that specifies the server, virtual directory, and name of the file to be created such as http://192.168.1.102/HttpFileXferVdir/Test.dat, for example.

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uploadUrl);

Once the HttpWebRequest object is created, the Method property identifies which HTTP operation to perform. Uploading a file requires that data be sent as part of the body of the request, so we use the PUT verb.

req.Method = "PUT";
Note   The HttpWebRequest class is capable of handling any of the HTTP 1.1 verbs: GET, PUT, POST, DELETE, TRACE, HEAD or OPTIONS.

Performing the actual upload is broken into two parts. First, the data to be sent must be placed into the request buffer of the HttpWebRequest object. Then, once all the data is completely buffered, the request can be submitted to the server. To enable buffering, the AllowWriteStreamBuffering property must be set to true.

req.AllowWriteStreamBuffering = true;
Note   The requirement to set AllowWriteStreamBuffering to true is specific to the .NET Compact Framework. On the desktop, HTTP requests containing data can be handled as either buffered or non-buffered at the developer's discretion. For details, see the MSDN discussion of HttpWebRequest.AllowWriteStreamBuffering.

Like most .NET I/O operations, the request buffer is exposed as a stream. The stream is accessed through the GetRequestStream method. All data placed in the stream will be sent as part of the request. The request stream can be used directly or wrapped in a StreamWriter. If it is used directly, data must be written as a byte array. Because managing data as byte arrays can be inconvenient, it is often useful to wrap the request stream in a StreamWriter and take advantage of the high-level Write methods that enable writing most .NET data types such as strings, integers, and floating-points directly. Of course, this convenience comes at a slight performance cost.

The following code accesses the request stream and wraps it in a StreamWriter:

Stream reqStream = req.GetRequestStream();
StreamWriter wrtr = new StreamWriter(reqStream);

Once all of the data is written into the request buffer, the request is submitted to the server using the GetResponse method.

Req.GetResponse();

GetResponse submits the request including the contained data. It also retrieves any returned information from the server. In the case of our upload application, we're not concerned about the returned information so we just call the method and ignore the return value. We'll take a closer look at the GetResponse method when we build the download portion of the program.

Using the StreamReader class we can read the local file line-by-line giving us the complete UploadFile method.

public void UploadFile(string localFile, string uploadUrl)
{
  HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uploadUrl);
  req.Method = "PUT";
  req.AllowWriteStreamBuffering = true;

  // Retrieve request stream and wrap in StreamWriter
  Stream reqStream = req.GetRequestStream();
  StreamWriter wrtr = new StreamWriter(reqStream);

  // Open the local file
  StreamReader rdr = new StreamReader(localFile);

  // loop through the local file reading each line 
  //  and writing to the request stream buffer
  string inLine = rdr.ReadLine();
  while (inLine != null)
  {
    wrtr.WriteLine(inLine);
    inLine = rdr.ReadLine();
  }

  rdr.Close();
  wrtr.Close();

  req.GetResponse();
}

The StreamReader and StreamWriter classes work well for uploading text files. Using the StreamReader ReadLine method is especially useful in cases where the smart device application needs to process the local data before sending it to the server. For non-text files such as MP3s, bitmaps, and even .NET assemblies, or cases where the contents of the local file are not important to the smart device, we can use the FileStream class to read the local file as a series of bytes that write the bytes directly to the request stream to give us an UploadFileBinary method.

public void UploadFileBinary(string localFile, string uploadUrl)
{
  HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uploadUrl);

  req.Method = "PUT";
  req.AllowWriteStreamBuffering = true;

  // Retrieve request stream 
  Stream reqStream = req.GetRequestStream();

  // Open the local file
  FileStream rdr = new FileStream(localFile, FileMode.Open);

  // Allocate byte buffer to hold file contents
  byte[] inData = new byte[4096];

  // loop through the local file reading each data block
  //  and writing to the request stream buffer
  int bytesRead = rdr.Read(inData, 0, inData.Length);
  while (bytesRead > 0)
  {
    reqStream.Write(inData, 0, bytesRead);
    bytesRead = rdr.Read(inData, 0, inData.Length);
  }

  rdr.Close();
  reqStream.Close();

  req.GetResponse();
}

The UploadFileBinary works with any file type and will actually perform better than the UploadFile method. The UploadFile method's call to ReadLine is somewhat expensive because it has to scan through the file data to locate the end-of-line marker on each call; however, the ReadLine method is useful for scenarios where applications need to work on each line from the file individually.

Completing the Upload Program

Now that the two upload functions are written, we can complete the application by adding two textboxes and two buttons to the application form. One textbox specifies an HTTP URL identifying a file name on the web server; the other specifies a local smart device file name. When each button is clicked, the UploadFile and UploadFileBinary methods will be called respectively. Figure 1 shows the form.

Figure 1: Upload Application Form

Now we can add a click event handler for each button. Each button click event handler calls the appropriate upload method.

private void uploadButton_Click(object sender, System.EventArgs e)
{
  UploadFile(localFileTextBox.Text, urlTextBox.Text);
}
private void uploadBinaryButton_Click(object sender, System.EventArgs e)
{
  UploadFileBinary(localFileTextBox.Text, urlTextBox.Text);
}

Creating the Virtual Directory

To test the Upload program we need a virtual directory on a server running IIS. The one thing that is a little unusual about this virtual directory is the file access privileges. Because the application uses HTTP to upload a file, the virtual directory needs to allow "Write" privilege.

To create a virtual directory with "Write" privilege:

  1. Create a new folder to act as the virtual directory or use an existing one. In the accompanying example, use the "HttpFileXferVdir" folder.
  2. Open the "Internet Information Services" (IIS) manager.
    • Open the "Control Panel".
    • Do one of the following:
    • For Microsoft® Windows® 2000 or Microsoft® Windows® XP "Classic View": Open the "Administrative Tools" group and double-click the Internet Information Services icon.
    • For Windows XP "Category View": Open the "Performance and Maintenance" category, then open the "Administrative Tools" group and double-click the Internet Information Services icon.
  3. From within IIS manager, expand the tree in the left-hand pane until you see the "Default Web Site".
  4. Right-click the "Default Web Site".
  5. Choose "New".
  6. Choose "Virtual Directory".
  7. Choose "Next" in the first wizard screen.
  8. Type the alias for your virtual directory into the next screen. For example, "HttpFileXferVdir".
  9. Click "Next".
  10. Type or browse to the directory created in the first step.
  11. Click "Next".
  12. In the "Access Permissions" wizard screen, ensure that both "Read" and "Write" are checked.
  13. For security reasons,

Windows 2000: set the "Execute Permissions" drop-down to "None".

Windows XP: uncheck "Execute" and "Run Scripts" if they are not already.

Note   If we allow scripts to execute in a virtual directory with write privileges enabled, someone can upload a hostile ASP or ASP.NET file to the server and then execute it. Disabling execute privileges, prevents this scenario.
  1. 14. Click "Next".
  2. 15. Click "Finish".

Virtual Directory Security

Once the virtual directory is created, the security must be appropriately set. For simplicity, the UploadFile and UploadFileBinary functions have been written to assume anonymous access.

To enable anonymous access on the virtual directory:

  1. If it is not already open, open the IIS manager.
  2. Expand the tree in the left-hand pane until you see the virtual directory you created in "Creating the Virtual Directory" above ("HttpFileXferVdir" if you used the name given in the example).
  3. Right-click the virtual directory and choose "Properties".
  4. Click on the "Directory Security" tab.
  5. Click the "Edit" button in the "Anonymous access and authentication control" section.
  6. Check the "Anonymous access" check box. If it is already checked, leave it checked.
    CAUTION   Anonymous access is useful for testing purposes but is extremely dangerous and should never be used on publicly available servers. For details on the IIS authentication options see IIS Authentication.
  7. Click "OK".
  8. Click "OK" again.

The directory now supports anonymous access.

The code in both the UploadFile and UploadFileBinary methods can be easily modified to work with servers that do not allow anonymous access. To do so, assign an instance of the NetworkCredential class to the HttpWebRequest Credentials property just after the HttpWebRequest instance is created.

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uploadUrl);
req.Credentials = new NetworkCredential("username", "password");

With the Credentials set, the HttpWebRequest object will automatically respond with the provided credentials when challenged by the server for a log on.

Note   For more information on the NetworkCredential class see the MSDN discussion of NetworkCredential.
CAUTION   The password is shown here as a string for convenience. In practice, passwords should never be stored as clear text within a program but rather be obtained from the user at runtime or read from a secure source.

Testing the Upload Functions

The program is now ready to test the upload functionality. Run the program. Enter the URL of the file to create on the server in the first textbox; the file does not need to exist on the server (for example, http://192.168.1.110/HttpFileXferVdir/Test.dat. In the second textbox enter the full path of the smart device file to upload; this file must already exist on the smart device (for example, \Program Files\HttpFileXfer\Test.dat).

Note   The accompanying "HttpFileXfer" project includes a simple "Test.dat" text file that can be used for testing the upload functions. The project is configured to automatically deploy this file to the application directory of the smart device or emulator.

When either the "Upload" or "Upload Binary" button is clicked, the file identified by the URL should appear on the server populated with the contents of the smart device file. Remember, the "Upload" button is valid only for text files whereas the "Upload Binary" can be any file type - even an application assembly.

If there is a problem getting either of the upload methods to work, the problem is most likely due to one of the following configuration issues.

  • The URL or file name is entered incorrectly.
  • The virtual directory does not have "Write" privilege enabled as specified in "Creating the Virtual Directory".
  • The virtual directory does not support anonymous authentication as specified in "Virtual Directory Security".

HTTP File Download

Once the upload portion of the application is built, adding download functionality is easy. We'll start by adding a DownloadFile method and accept the local file name and download URL as parameters.

public void DownloadFile(string localFile, string downloadUrl)

The HttpWebRequest object is created the same as before, but this time we'll set the Method to "GET" to retrieve a file from the server.

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(downloadUrl);
req.Method = "GET";

Unlike "PUT", "GET" specifies all necessary information in the URL. It therefore does not need to send any data with the request to enable write stream buffering – the request can simply be submitted by calling GetResponse. This time, we need to keep the GetResponse return value.

HttpWebResponse resp = (HttpWebResponse) req.GetResponse();

The returned HttpWebResponse represents all information returned from the server including the status code, headers, and any returned data. The returned data (which in the case of a "GET" is the contents of the server's file specified by the URL) is accessed through the HttpWebResponse response stream.

Stream respStream = resp.GetResponseStream();

If line-by-line access to the file is required, the stream can be read using the ReadLine method of StreamReader with each line being written to the local file using a StreamWriter.

public void DownloadFile(string localFile, string downloadUrl)
{
  HttpWebRequest req = (HttpWebRequest)WebRequest.Create(downloadUrl);
  req.Method = "GET";
 
  HttpWebResponse resp = (HttpWebResponse) req.GetResponse();

  // Retrieve response stream and wrap in StreamReader
  Stream respStream = resp.GetResponseStream();
  StreamReader rdr = new StreamReader(respStream);

  // Create the local file
  StreamWriter wrtr = new StreamWriter(localFile);

  // loop through response stream reading each line 
  //  and writing to the local file
  string inLine = rdr.ReadLine();
  while (inLine != null)
  {
    wrtr.WriteLine(inLine);
    inLine = rdr.ReadLine();
  }

  rdr.Close();
  wrtr.Close();
}

Just as in the case of the upload, reading the stream directly and using a FileStream to create the local file provides a more efficient download that also supports non-text files such as MP3s, bitmaps, and .NET assemblies.

public void DownloadFileBinary(string localFile, string downloadUrl)
{
  HttpWebRequest req = (HttpWebRequest)WebRequest.Create(downloadUrl);
  req.Method = "GET";

  HttpWebResponse resp = (HttpWebResponse) req.GetResponse();

  // Retrieve response stream
  Stream respStream = resp.GetResponseStream();

  // Create local file
  FileStream wrtr = new FileStream(localFile, FileMode.Create);

  // Allocate byte buffer to hold stream contents
  byte[] inData = new byte[4096];

  // loop through response stream reading each data block
  //  and writing to the local file
  int bytesRead = respStream.Read(inData, 0, inData.Length);
  while (bytesRead > 0)
  {
    wrtr.Write(inData, 0, bytesRead);
    bytesRead = respStream.Read(inData, 0, inData.Length);
  }

  respStream.Close();
  wrtr.Close();
}

Completing the Download Functionality

Now we can add two more buttons to the form, "Download" and "Download Binary" as shown in Figure 2.

Figure 2. Application form with download buttons added

Once click event handlers are added to each button, the buttons call DownloadFile and DownloadFileBinary, respectively.

private void downloadButton_Click(object sender, System.EventArgs e)
{
  DownloadFile(localFileTextBox.Text, urlTextBox.Text);
}

private void downloadBinaryButton_Click(object sender, System.EventArgs e)
{
  DownloadFileBinary(localFileTextBox.Text, urlTextBox.Text);
}

Testing the Download Functions

The program is now ready to test the download functions. Run the program. In the first textbox, enter the URL of the file to download from the server, this file must already exist on the server (for example, http://192.168.1.105/HttpFileXferVdir/Test2.dat). In the second textbox, enter the full path of the smart device file to create; this file does not need to exist on the smart device (for example, \Program Files\HttpFileXfer\Test2.dat). The accompanying example includes a "Test2.dat" text file in the HttpFileXferVdir virtual directory that can be used as a test. Click a download button.

When either download button is clicked, the file should appear on the smart device. If there is a problem getting the download functions to work, the problem is most likely due to one of the following reasons:

  • The URL or file name is entered incorrectly.
  • The virtual directory does not have "Read" privilege enabled as specified in the "Creating the Virtual Directory" section.
  • The virtual directory does not support anonymous authentication as specified in the "Virtual Directory Security" section.

Building Interactive Server Components

With the client side of a .NET Compact Framework HTTP conversation covered, we turn our attention to the server. In the Upload/Download application in the first half of this article, the client application triggered processing on the server by sending an HTTP "PUT" or "GET". In that case, IIS performed the default HTTP processing of uploading or downloading a file. Using ASP.NET custom handlers, it's possible to override the default IIS behavior and associate more sophisticated processing with HTTP operations.

ASP.NET Custom Handler Basics

A custom handler is a .NET component, usually contained in a DLL assembly, which has been registered with ASP.NET to handle an HTTP operation and URL suffix combination (for example, "PUT" operations for URLs ending in ".delivery") within a virtual directory. Familiar URL suffixes are ".aspx" for ASP.NET pages and ".asmx" for Web services. Using custom handlers, it is possible to associate server processing with arbitrary suffixes such as ".delivery" or ".payroll".

Although ASP.NET custom handlers are written using the full .NET Framework, they can communicate with any client communicating to the server through HTTP.

Building a custom handler is as simple as creating a .NET class that implements the IHttpHandler interface.

namespace HandlerLib
{
  public class DeliveryDataHandler : IHttpHandler
  {
    public void ProcessRequest(HttpContext context)
    {
      // Do custom handler work
    }
    public bool IsReusable
    {
      get {return false;}
    }
  }
}

The IHttpHandler interface requires that the class implement the ProcessRequest method and the IsResuable property. ProcessRequest is the method that actually performs the handlers processing. It receives a reference to the HttpContext object, which is arguably the most important object in ASP.NET as it provides access to the ASP.NET environment for this request (a.k.a. the request context).

Note   For a complete list of everything available from HttpContext visit the HttpContext Member List on MSDN.

The IsReusable property is used for housekeeping by the ASP.NET runtime. IsReusable identifies whether instances of the handler can be pooled. Returning false from IsReusable indicates that a new instance of the handler must be created for each request. Returning true indicates that once the handler has completed its processing, it can be placed in a handler pool and be reused for future requests. As long as the handler does not maintain state it can safely be pooled. If you have any doubts as to whether a handler can be safely pooled, it is best to return false.

Note   For a thorough discussion of HTTP Handlers and where they fit in the overall ASP.NET pipeline, check out the excellent MSDN article HTTP Pipelines.

Putting the ASP.NET Custom Handler to work

ASP.NET custom handlers enable IIS to act as a component server rather than simply serving up web pages. Properly configured, IIS will route a request to ASP.NET, allowing a custom handler to process that request rather then IIS perform the default processing.

In addition to processing the data sent by the client, the custom handler can respond back to the client. The ability of the custom handler to both receive data from and send data back to the client empowers the custom handler to act as a full function component.

Receiving Data from the Client

In the UploadFile method of the .NET Compact Framework client, data was sent to the web server by writing to the HttpWebRequest request stream. Using the InputStream property of HttpContext.Request, the custom handler can read that data. Just as on the client, a StreamReader can be used to provide line-by-line access to the data.

The custom handler can perform any type of processing on the data such as data validation, data transformation, perform a database lookup, or even insert the data into a database. For simplicity, our handler will count the number of lines read.

public void ProcessRequest(HttpContext context)
{
  // Access the request stream and wrap in a StreamReader
  Stream requestStream = context.Request.InputStream;
  StreamReader rdr = new StreamReader(requestStream);
  
  int linesRead = 0 ;  
  // Read data from the client line-by-line
  string inLine = rdr.ReadLine();
  while (inLine != null)
  {
    // perform line processing
    linesRead += 1;
    inLine = rdr.ReadLine();
  }
}

Responding to the Client

The custom handler is not limited to reading client data. The HttpContext provides a Response property, allowing the handler to send data back to the client. Data written to the OutputStream property of HttpContext.Reponse is available to the client through the HttpWebResponse class' response stream. In the case of our simple handler, the number of lines read and a "Processing Successful" message are sent to the client after all of the data is read. The response could just as easily be the results of a database lookup or a server calculation.

public void ProcessRequest(HttpContext context)
{
  // Access the request stream and wrap in a StreamReader
  Stream requestStream = context.Request.InputStream;
  StreamReader rdr = new StreamReader(requestStream);
  
  int linesRead = 0 ;  
  // Read data from the client line-by-line
  string inLine = rdr.ReadLine();
  while (inLine != null)
  {
    // perform line processing
    linesRead += 1;
    inLine = rdr.ReadLine();
  }

  // Attach to the response stream and wrap in a StreamWriter
  Stream responseStream = context.Response.OutputStream;
  StreamWriter wrtr = new StreamWriter(responseStream);

  // Write the response message
  wrtr.WriteLine("LinesRead=");
  wrtr.WriteLine(linesRead.ToString());
  wrtr.WriteLine("Processing Successful");

  wrtr.Flush();
}

ProcessRequest now makes a full round-trip: it reads a message from the client, processes the message, and responds back to the client with the processing results.

Hooking Up the Handler

Now that all of the code is written, we need to get the custom handler installed and registered with ASP.NET.

Deploying the Custom Handler Assembly

First, the assembly containing the handler needs to be made available to ASP.NET. To do this, the assembly must be copied to the "bin" subdirectory of a virtual directory. In this case the virtual directory requires no special "Access Permissions" or "Execute Permissions," so use the procedure detailed in the earlier "Creating the Virtual Directory" section except skip steps 12 and 13. In Windows 2000, the default is to allow scripts and executables so no permission changes are necessary. In Windows XP, the default is to be secure so you must check "Execute" and "Run Scripts" if they are not already (the opposite of step 13 above). Name this virtual directory "HandlerVdir".

In the accompanying example, the custom handler is contained in the "HandlerLib" assembly so the "HandlerLib.dll" needs to be placed in the "HandlerVdir\bin" directory. To work, the virtual directory must support anonymous access as detailed in "Virtual Directory Security".

Note   The assembly can also be installed into the Global Assembly Cache rather than the local "bin" which is useful if the assembly will be used by multiple virtual directories. In order to install the assembly into the Global Assembly Cache, the assembly must have a strong name. For more details on creating a strong name and installing assemblies into the Global Assembly Cache see this How To.

Registering the Custom Handler

Now that the handler is visible to ASP.NET, ASP.NET needs to know what to do with it. Handlers are registered with ASP.NET using the <add> statement in the <httpHandlers> section of the <system.web> sub section of the virtual directory's "web.config" file. Here's the "web.config" file for the accompanying example.

<configuration>
  <system.web>
    <httpHandlers>
      <add verb="PUT" path="*.delivery"
        type="HandlerLib.DeliveryDataHandler, HandlerLib"/>
    </httpHandlers>
  </system.web>
</configuration>

This "web.config" file tells ASP.NET that HTTP "PUT" operations for URLs in this virtual directory ending in ".delivery" are handled by the "Handlerlib.DeliveryDataHandler" class which is located in the "HandlerLib" assembly.

Note   For more information on configuring ASP.NET custom handlers see <httpHandlers> element.

Registering the URL Extension with IIS

We're almost done; there's just one more step. ASP.NET is dependent on IIS and although ASP.NET has been configured to send "PUT" requests for URLs ending in ".delivery" to the "DeliveryDataHandler" custom handler, IIS needs to be configured to direct those requests to ASP.NET.

To configure IIS to direct all "PUT" requests ending in ".delivery" to ASP.NET:

  1. Open the IIS manager if it is not already open.
  2. Expand the tree in the left-hand pane until you see the virtual directory you created in the "Creating the Virtual Directory" section above ("HandlerVdir" if you used the name given in the directions).
  3. Right-click the virtual directory and choose "Properties".
  4. Click the "Configuration" button of the "Properties" dialog.
Note   For step 6, an easy way to get the path of the aspnet_isapi.dll is to copy the path from an existing ASP.NET entry such as "aspx". To copy the path from the "aspx" entry, do the following.
Before performing step 5, scroll to and double-click on the "aspx" entry in the "Application Mappings" list box of the "Application Configuration" dialog. Highlight the entire contents of the "Executable" field. Right-click the contents of the "Executable" field and choose "Copy" then click "Cancel" on the dialog. The path of "aspnet_isapi.dll" is now in the clipboard.
Perform step 5 as normal. In step 6, right-click on the "Executable" field and choose "Paste" to paste the path into the "Executable" field.
  1. Click the "Add" button of the "Application Configuration" dialog.
  2. Type the path of the aspnet_isapi.dll into the "Executable" field. The exact path of the aspnet_isapi.dll depends on which operating system and which version of the .NET is installed. Do one of the following:
    • For .NET Framework 1.1 (VS.NET 2003) running on Windows 2000, the path is "C:\WINNT\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll".
    • For Windows XP or Microsoft® Windows® 2003, the path is "C:\Windows\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll".
    • For .NET Framework 1.0 (VS.NET 2002), the path is "C:\WINNT\Microsoft.NET\Framework\v.1.0.3705\aspnet_isapi.dll".
  3. Type ".delivery" into the "Extension" field.
  4. Click the "Limit To:" radio button.
  5. Type PUT into the "Limit To:" field.
  6. Ensure that the "Check that file exists" and "Script Engine" check boxes are unchecked.
  7. Click "OK" on the "Add/Edit Application Extension Mapping" dialog.
  8. Click "OK" on the "Application Configuration" dialog.
  9. Click "OK" on the "Properties" dialog.

Building the Client

Now that the server is performing a full round-trip (that is, both receiving and sending data) we need an updated smart device client that can handle the more sophisticated behavior.

We'll create a new Smart Device project as a Windows Application targeting the Pocket PC. The updated client basically needs to combine upload and download functionality into a single method, SendServerMessage. The only parameter to the method is a URL.

void SendServerMessage(string serverUrl)

Using the upload behavior from the UploadFile method in the earlier smart device application, data is sent line-by-line to the custom handler using a "PUT". This data can come from anywhere – a flat file, SQL Server CE database, or application memory. To keep things simple, the following example reads through an array of strings sending each member.

Once the data is sent to the server, the server response data can be read line-by-line using the stream reading behavior from the earlier DownloadFile method.

void SendServerMessage(string serverUrl)
{
  // list of data to send
  string[] deliveryData = 
  {
    "John Smith, Compact Flash Cards, 12",
    "Jane Johnson, WiFi Cards, 2",
    "Mary McGee, USB Extension Cable, 1",
    "Fred Foobar, Pocket PC, 3"
  };

  // Create a HttpWebRequest and do a "PUT"
  HttpWebRequest req = (HttpWebRequest)WebRequest.Create(serverUrl);
  req.Method = "PUT";
  req.AllowWriteStreamBuffering = true;

  // Retrieve request stream and wrap in StreamWriter
  Stream reqStream = req.GetRequestStream();
  StreamWriter wrtr = new StreamWriter(reqStream);

  // Write data line-by-line to the request buffer
  foreach (string message in deliveryData)
  {
    wrtr.WriteLine(message);
  }

  wrtr.Close();

  // Submit the request and get the response object
  HttpWebResponse resp = (HttpWebResponse) req.GetResponse();

  // Retrieve the response stream and wrap in a StreamReader
  Stream respStream = resp.GetResponseStream();
  StreamReader rdr = new StreamReader(respStream);

  // Read through the response line-by-line
  string inLine = rdr.ReadLine();
  while (inLine != null)
  {
    // Process the response data
    //  For simplicity write to form list box
    serverMessageListbox.Items.Add(inLine);
    inLine = rdr.ReadLine();
  }

  rdr.Close();
}

The Application User Interface

As shown in Figure 3, the application user interface form has a textbox for the server URL, a button to execute the request, and a list box to display the server results.

When clicked, the "Send" button calls the SendServerMessage method passing the Server URL.

private void sendButton_Click(object sender, System.EventArgs e)
{
  SendServerMessage(serverUrlTextbox.Text);

}

Figure 3. The custom handler smart device client.

Testing it all out

Once all of the code is written and the custom handler registered with ASP.NET, the only thing left to do is try them out.

Run the smart device client and enter the server URL. The URL can be any URL referencing the handler's virtual directory and ending with ".delivery". An example might be http://192.168.1.102/HandlerVdir/foo.delivery. Click the "Send" button and watch for "LinesRead=4" and "Processing Successful" messages to appear in the "Server Message" list box.

The custom handler is loaded by the server for any URL ending in ".delivery". In the case of the URL http://192.168.1.102/HandlerVdir/foo.delivery, the file "foo.delivery" does not need to exist as the handler never tries to use it. As far as ASP.NET is concerned, the URL is only used to activate the custom handler. The custom handler is responsible for verifying the existence of the file.

Note   For custom handlers that depend on the existence of a file to function properly, IIS can be configured to verify that the file referenced by the URL exists. This behavior is enabled by checking the "Check that file exists" check box referenced in step 10 of "Registering the URL extension with IIS". When the file does not exist, IIS returns a "Not Found" error to the client without executing the custom handler.

If there is a problem getting the handler to work, the problem is likely due to one of the following:

  • The URL is invalid or does not end in ".delivery".
  • The custom handler assembly is not in the virtual directory's "bin" subdirectory as detailed in "Deploying the custom handler assembly".
  • The "web.config" file is either not in the virtual directory or it is incorrect. The "Type" entry on the <add> directive must exactly match the handler class name (including namespace) and assembly name. See "Registering the Custom Handler" for the format of the "web.config" file.
  • IIS is not properly configured to forward ".delivery" URLs to ASP.NET. Verify that the ".delivery" URL suffix is configured to direct requests to the aspnet_isapi.dll and that the aspnet_isapi.dll path is correct as detailed in "Registering the URL Extension with IIS".

Conclusion

That's it; we've built custom and efficient HTTP communications components. These components give us all the benefits of HTTP including firewall support and the flexibility to seamlessly support WiFi networks, wired networks, and mobile networks with a compact messaging format. Using this technique, there is no limit to what can be done.

For scenarios where communication efficiency and/or direct control of the messaging behavior are important, the capabilities of the .NET Compact Framework HTTP classes and ASP.NET custom handlers can't be beat.