A Simple ISAPI Extension

The ISAPI1.cpp generated by the compiler contains just a stub DllMain function. This function can remain untouched for this example but would often be used to allocate and deallocate resources, as needed. To be used as an ISAPI extension, the DLL must export the two functions mentioned previously, GetExtensionVersion and HttpExtensionProc . The finished program is shown in Listing 10-1.

Listing 10-1

ISAPI1.cpp

 //ISAPI1.cpp:DefinestheentrypointfortheDLLapplication. // #include"stdafx.h" #include"time.h" #include"string.h" #include"stdio.h" BOOLAPIENTRYDllMain(HANDLEhModule, DWORDul_reason_for_call, LPVOIDlpReserved) { //Thisfunctionwouldnormallybeusedtoinitialize //andallocateresourcesrequiredfortheextensionto //function. returnTRUE; } BOOLWINAPIGetExtensionVersion(HSE_VERSION_INFO*pVer) { pVer->dwExtensionVersion=1; strcpy(pVer->lpszExtensionDesc, "ISAPI1TestforInsideWindowsServerDevelopment"); return(TRUE); } DWORDWINAPIHttpExtensionProc(EXTENSION_CONTROL_BLOCK*lpECB) { DWORDcBuf; charszMessage[1024]; HSE_SEND_HEADER_EX_INFOHeaderExInfo; //Prepareaheader... HeaderExInfo.pszStatus="200OK"; HeaderExInfo.cchStatus=strlen(HeaderExInfo.pszStatus); HeaderExInfo.pszHeader="Content-type:text/html\r\n\r\n"; HeaderExInfo.cchHeader=strlen(HeaderExInfo.pszHeader); HeaderExInfo.fKeepConn=FALSE; //SendheadersusingIIS-providedcallback. //ThesefunctionsareusedquiteabitinISAPIprogramming. lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &HeaderExInfo, NULL, NULL); if(!(strnicmp(lpECB->lpszQueryString,"TIME",4))) { charszTemp[255]; charszTime[255]; strcpy(szMessage,"<HTML><HEAD><TITLE>"); strcat(szMessage,"ISAPITest</TITLE></HEAD>\r\n"); strcat(szMessage,"<BODY><H1><CENTER><P>" "HelloISAPIWorld!<BR>"); _strtime(szTime); sprintf(szTemp,"Thetimeontheserveris%s<BR>",szTime); strcat(szMessage,szTemp); strcat(szMessage,"</P>\r\n</CENTER></H1>"); strcat(szMessage,"</BODY></HTML>\r\n\r\n"); } else//Defaultbehavior... { strcpy(szMessage,"<HTML><HEAD><TITLE>"); strcat(szMessage,"ISAPITest</TITLE></HEAD>\r\n"); strcat(szMessage,"<BODY><H1><CENTER><P>" "HelloISAPIWorld!"); strcat(szMessage,"</P>\r\n</CENTER></H1>"); strcat(szMessage,"</BODY></HTML>\r\n\r\n"); } //Setthelengthofthebufferbeingwritten... cBuf=strlen(szMessage); //UseanotherofthefunctionsprovidedbyIIStowritecontent //tothebrowser. lpECB->WriteClient(lpECB->ConnID,szMessage,&cBuf,HSE_IO_SYNC); returnHSE_STATUS_SUCCESS; } 

GetExtensionVersion is a simple function. It sets the extension version and description elements of the HSE_VERSION_INFO structure and then returns. This function is never called directly, and the values set are of little practical value except that some administrative tools might use the description for display purposes.

Many of the elements of the structure are self-explanatory and can be explored more fully using the MSDN documentation, but several merit further explanation. The c onnID parameter is a number, assigned by the server, that must not be modified. This number should not be used for any purpose other than when passed to a function that expects a connection ID. In addition to data members , the structure contains four function pointers. This is how the extension interacts with the server, allowing data to be read from the client and sent to the client, as well as allowing the server to be modified.

Debugging ISAPI Extensions

Although debugging traditional DLLs can be daunting, debugging ISAPI extensions can be truly maddening. The first step toward being able to even develop ISAPI extensions is to disable Internet Information Server's caching of ISAPI extensions. By default, when an ISAPI extension is first used it is loaded, and it stays loaded, usually until IIS stops. This is great for performance, but can make the normal edit-compile-test cycle difficult. Fortunately, there is a solution. For IIS 4 and later, go to the Home Directory tab on the Web Site properties dialog. On that tab, click on Configuration. On the App Manager tab of the resulting dialog, uncheck Cache ISAPI Applications. This is a good first step toward at least being able to replace the DLL after a change.

To actually debug the application, MSDN has complete instructions for Visual C++ and other debugging tools. The Microsoft Knowlege Base article Q152054 gives details on debugging ISAPI DLLs. One caveat regarding that article: It mentions a registry entry to disable ISAPI Application Caching. For IIS 4 and later, it is far better to use the method I've described here, since the registry entry seems to be absent for IIS 5, included with Windows 2000.

Finally, after all the testing with caching of ISAPI Applications disabled, the application must be tested with caching enabled. With caching disabled, you can build an application that is dependent upon an initial state to operate . Enabling caching will help flush out such problems.

HttpExtensionProc is much more important than GetExtensionVersion ”it is the heart of any ISAPI extension. This function is called with a pointer to an EXTENSION_CONTROL_BLOCK structure. The structure declaration is shown in Listing 10-2. Many of the elements of this structure expose variables that are also available to ASP scripts. For instance, the lpszQueryString contains all of the string of the URL after the question mark. For example, in the URL http://dual/isapi1.dll?time , lpszQueryString points to the string time .

Listing 10-2

 typedef struct _EXTENSION_CONTROL_BLOCK  {     DWORD  cbSize;      // Size of this struct     DWORD  dwVersion;   // Version info of this spec     HCONN  ConnID;      // Context number not to be modified!     DWORD  dwHttpStatusCode;                // HTTP Status code     CHAR   lpszLogData[HSE_LOG_BUFFER_LEN]; // NULL-terminated                                             //  log info     LPSTR  lpszMethod;         // REQUEST_METHOD     LPSTR  lpszQueryString;    // QUERY_STRING     LPSTR  lpszPathInfo;       // PATH_INFO     LPSTR  lpszPathTranslated; // PATH_TRANSLATED     DWORD  cbTotalBytes;       // Total bytes indicated from client     DWORD  cbAvailable;        // Available number of bytes     LPBYTE lpbData;            // Pointer to cbAvailable bytes     LPSTR  lpszContentType;    // Content type of client data     BOOL  (WINAPI * GetServerVariable);     BOOL  (WINAPI * WriteClient);       BOOL  (WINAPI * ReadClient);       BOOL  (WINAPI * ServerSupportFunction); }   EXTENSION_CONTROL_BLOCK; 

GetServerVariable is a function that allows ISAPI extensions to access the same set of server variables that are available as part of the ASP ServerVariables collection of the Request object. As with all of the functions pointed to by the EXTENSION_CONTROL_BLOCK structure, this function accepts connID as a parameter. The function also accepts parameters specifying the name of the variable to retrieve, a pointer to a buffer to accept the value, and the length of the buffer.

ReadClient and WriteClient are used to receive data from and send data to the client. These functions contain a dwSync parameter that specifies whether the call should be synchronous or asynchronous. If the operation is to be done asynchronously, the extension should have already set up an I/O completion function using the ServerSupportFunction , which I'll describe shortly. If it has, the read or write function will return immediately and the I/O completion function will be called when the operation is complete. The I/O completion function is passed some context information provided by the extension when the callback is registered, as well as being passed an EXTENSION_CONTROL_BLOCK and the number of bytes read or written.

NOTE
Asynchronous I/O is often used to allow applications to continue functioning while relatively slow I/O operations take place. This is often appropriate, especially if the number of clients is expected to be high and the duration of the I/O operations is expected to be long. Well, then, why not always use asynchronous I/O? Generally , using asynchronous I/O results in a more complex program than would result from using synchronous I/O using multiple threads. I'll discuss asynchronous I/O in more detail in Chapter 13.

ServerSupportFunction is a jack-of-all-trades function that we call with one of 17 possible operation codes. The possible operations run the gamut from getting certificate or impersonation information to retrieving path names from URLs or redirecting the client to a new URL. The meanings of parameters passed into ServerSupportFunction depend upon the operation code being used. ServerSupportFunction 's general form is shown in the following function prototype:

 BOOLServerSupportFunction(HCONNConnID,//ConnectionID DWORDdwHSERRequest,//OperationCode LPVOIDlpvBuffer,//Buffer LPDWORDlpdwSize,//Size,whereappropriate LPDWORDlpdwDataType//DataType,whereappropriate); 

The HttpExtensionProc in the ISAPI1 example program does nothing more than display a message on the browser ("Hello ISAPI World!") and, optionally , display the time on the server. Figure 10-6 shows the results of calling this sample ISAPI extension using the URL that points to the DLL with ?time appended. Calling the extension without ?time (or with any value except the string ?time ) results in the same screen without the time reported .

The HttpExtensionProc function for this simple application illustrates the tasks associated with virtually any ISAPI extension application. The first step is to prepare a header, as shown in the following code:

 //Prepareaheader. HeaderExInfo.pszStatus="200OK"; HeaderExInfo.cchStatus=strlen(HeaderExInfo.pszStatus); HeaderExInfo.pszHeader="Content-type:text/html\r\n\r\n"; HeaderExInfo.cchHeader=strlen(HeaderExInfo.pszHeader); HeaderExInfo.fKeepConn=FALSE; 

All HTTP messages have a status associated with them, indicated with a number followed by a text explanation. For instance, in this example "200 OK" is the message that tells the browser that the request has resulted in a good response. Table 10-1 shows the general classes of status messages that can be returned. Notice that when you use IIS 4 or greater, the URL or file to be displayed in the event of an error can be custom-configured for each error message.

click to view at full size.

Figure 10-6 A simple ISAPI application as seen by the client browser.

Table 10-1 Classes of HTTP Status Messages

HTTP Status Category Description
1xx Informational Not used, but reserved for future use.
2xx Success The action was successfully received, understood , and accepted.
3xx Redirection Further action is required to complete the request.
4xx Client Error The request contains incorrect syntax and cannot be processed .
5xx Server Error The server failed to service the apparently valid request.

Once the header is created, you must send it. The header information here is appended to the header information already created by the IIS server. Rather than use the WriteClient function, we use the ServerSupportFunction to send the header. The call to ServerSupportFunction sends the connection ID, the operation code for sending a header, and a pointer to the header information structure, as in the code fragment shown below.

 //SendheadersusingIIS-providedcallback. //ThesefunctionsareusedquiteabitinISAPIprogramming. lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &HeaderExInfo, NULL, NULL); 

Notice that we are using the HSE_REQ_SEND_RESPONSE_HEADER_EX operation code rather than the HSE_REQ_SEND_RESPONSE_HEADER operation code; the latter has been deprecated, and its use is not recommended. The HSE_REQ_SEND_RESPONSE_HEADER_EX operation code also allows the program to specify whether the connection should be kept open .

While all of the possible operation codes are detailed in the MSDN documentation, several deserve special mention.

  • HSE_REQ_GET_IMPERSONATION_TOKEN is used to retrieve a handle to the impersonation token used by the request. This operation code can be used when your ISAPI extension is attempting to use a resource like a file or registry key. The returned token can be used by the ImpersonateLoggedOnUser or SetThreadToken Win32 functions. Unlike most handles your program will use, this handle should not be closed when you are through with it; the handle will be closed when the request's EXTENSION_CONTROL_BLOCK is destroyed .
  • The HSE_REQ_SEND_URL and HSE_REQ_SEND_URL_REDIRECT_RESP operation codes perform the same operation. They redirect the current connection to the absolute URL passed in the lpvBuffer parameter. While some browsers support relative URLs (such as default.asp ), most require an absolute path (such as www.microsoft.com/default.asp ). The HTTP 1.1 standard specifically requires absolute paths.
  • The HSE_REQ_TRANSMIT_FILE operation code allows an ISAPI extension to send a file to the client browser using the Win32 API TransmitFile function. The file transmission is carried out asynchronously. What this means to your application is that you must set an asynchronous callback function, either through the HSE_REQ_IO_COMPLETION operation code or by setting the callback function in the HSE_TF_INFO structure passed in as the lpvBuffer parameter. Once the file transmission is started, the extension should return the HSE_STATUS_PENDING status code.

The code in Listing 10-1 next checks the value of the query string, available as the lpszQueryString element of the extension control block passed into HttpExtensionProc . Recall that the query string contains the text entered as the URL immediately past the first question mark that indicates the beginning of arguments being passed. In the ISAPI1 example, we simply check the query string to see whether it is the literal "time". We check using a case-insensitive string compare, shown in this code fragment:

 if(!(strnicmp(lpECB->lpszQueryString,"TIME",4))) 

In the example shown in Figure 10-6, the query string is http://dual/isapi1.dll?time . Thus, this URL will meet the test and the code within the if block will be executed. The block of code copies normal HTML code into the szMessage character array and inserts the time into the string because the user requested it. Returning the time is specific to this ISAPI extension and is done solely because we programmed it this way. The HTML code copied into the szMessage buffer is the same code you would type to create the same simple page manually.

If "time" is not passed as an argument in the URL, the else clause is executed. In a real-world example, there would likely be several different functions that would be supported, each with its own set of code in the HttpExtensionProc function. Finally the extension procedure uses the WriteClient callback passed in as part of the extension control block.

MFC ”A Better Way to Create an ISAPI Extension

One thing that should be clear from the ISAPI1 example: a lot of code is essentially boilerplate. There ought to be a better way, and there is. Unlike the solution to the boilerplate associated with ODBC in Chapter 8, the ISAPI boilerplate problem has a solution within the Microsoft Foundation Classes (MFC).

Recall that in Chapter 8 I said that the database classes of MFC would not be covered primarily because using MFC for a service is often not the best approach. Why the change of heart? Nothing really has changed. I do feel that MFC, although entirely appropriate for client applications, is too much for most server applications. ISAPI is the one great exception to this.

There are several reasons why this is true. First and foremost, the MFC ISAPI classes are a work of art. While many of MFC's details require entire chapters to explain fully (and even then the explanations might not be completely satisfying ), the ISAPI extension classes (and the ISAPI filter classes covered in the next chapter) are concise and easily covered, even if you have little or no experience with MFC.

The second and perhaps most important reason for using the MFC ISAPI classes is that you can do so and use only the part of MFC required for ISAPI development. This eliminates any concern about adding too much superfluous code to your application. For instance, I created an MFC version of the simple ISAPI1.dll created in the previous section. I used the ISAPI Extension Wizard (described shortly) with the MFC components statically linked, with some modifications to ensure that only the ISAPI extension portions of MFC were linked into the resulting DLL. While the resulting MFC-based ISAPI DLL was twice the size of the hand-crafted ISAPI extension, at approximately 72 KB it was still quite small. More important, the MFC ISAPI extension DLL offered some convenient features not found in the hand-crafted ISAPI extension DLL, and the future growth in size as functionality is added is likely to be equivalent for both versions. The 36 KB penalty for using MFC in this case is a penalty I am more than willing to pay for the convenience offered .

Step 1: Using the MFC ISAPI Extension Wizard

The first step required to use the MFC ISAPI extensions is to select New from the File menu, select the ISAPI Extension Wizard from the list of project types, and enter the base path and project name, as shown in Figure 10-7. Technically you could begin creating an ISAPI application by simply creating a basic DLL project and manually adding all the MFC ISAPI support, but using the wizard is much simpler.

click to view at full size.

Figure 10-7 Selecting the ISAPI Extension Wizard from the New dialog in Visual C++6.

The ISAPI Extension Wizard, shown in Figure 10-8, consists of a single dialog that allows you to select the type of server object you would like to create. For this exercise, most of the default values are fine. Leave Generate A Filter Object unchecked; I'll cover ISAPI filters in the next chapter. The default Extension Class Name and Description boxes are fine, but I generally use the MFC library as a statically linked library. In this case, the size of the MFC library that is being linked in is quite small. Click on Finish, and a confirmation dialog summarizing your selections will appear.

click to view at full size.

Figure 10-8 The ISAPI Extension Wizard dialog in Visual C++ 6.

The resulting application is a straightforward ISAPI extension that simply sends out a message when it is called, without regard to the query string that is passed in. One of the first things that we need to do to this application is remove the vast majority of MFC code that is not needed by an ISAPI extension. Let's walk through the steps required to remove support for MFC.

First take a look at Listing 10-3, which is the main file for the wizard-generated application. Notice especially the code that is commented out at the bottom of the listing. Just above the commented code is a comment that explains the need to include this code if the extension will not use MFC. If you simply follow the instructions and compile the extension, uncommenting the commented section of code, the code will link. You need to take two additional steps, which I explain in the next section of this chapter.

Listing 10-3

ISAPIPhone.cpp

 //ISAPIPHONE.CPP-ImplementationfileforyourInternetServer //ISAPIPhoneExtension #include"stdafx.h" #include"ISAPIPhone.h" //////////////////////////////////////////////////////////////////// //command-parsingmap BEGIN_PARSE_MAP(CISAPIPhoneExtension,CHttpServer) //TODO:insertyourON_PARSE_COMMAND()and //ON_PARSE_COMMAND_PARAMS()heretohookupyourcommands. //Forexample: ON_PARSE_COMMAND(Default,CISAPIPhoneExtension,ITS_EMPTY) DEFAULT_PARSE_COMMAND(Default,CISAPIPhoneExtension) END_PARSE_MAP(CISAPIPhoneExtension) //////////////////////////////////////////////////////////////////// //TheoneandonlyCISAPIPhoneExtensionobject CISAPIPhoneExtensiontheExtension; //////////////////////////////////////////////////////////////////// //CISAPIPhoneExtensionimplementation CISAPIPhoneExtension::CISAPIPhoneExtension() { } CISAPIPhoneExtension::~CISAPIPhoneExtension() { } BOOLCISAPIPhoneExtension::GetExtensionVersion(HSE_VERSION_INFO*pVer) { //Calldefaultimplementationforinitialization. CHttpServer::GetExtensionVersion(pVer); //Loaddescriptionstring. TCHARsz[HSE_MAX_EXT_DLL_NAME_LEN+1]; ISAPIVERIFY(::LoadString(AfxGetResourceHandle(), IDS_SERVER,sz,HSE_MAX_EXT_DLL_NAME_LEN)); _tcscpy(pVer->lpszExtensionDesc,sz); returnTRUE; } BOOLCISAPIPhoneExtension::TerminateExtension(DWORDdwFlags) { //Extensionisbeingterminated. //TODO:Cleanupanyper-instanceresources. returnTRUE; } //////////////////////////////////////////////////////////////////// //CISAPIPhoneExtensioncommandhandlers voidCISAPIPhoneExtension::Default(CHttpServerContext*pCtxt) { StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt<<_T("Thisdefaultmessagewasproducedbythe"); *pCtxt<<_T("InternetServerDLLWizard.Edityour" "CISAPIPhoneExtension::Default()"); *pCtxt<<_T("implementationtochangeit.\r\n"); EndContent(pCtxt); } //Donoteditthefollowinglines,whichareneededbyClassWizard. #if0 BEGIN_MESSAGE_MAP(CISAPIPhoneExtension,CHttpServer) //{{AFX_MSG_MAP(CISAPIPhoneExtension) //}}AFX_MSG_MAP END_MESSAGE_MAP() #endif//0 //////////////////////////////////////////////////////////////////// //IfyourextensionwillnotuseMFC,you'llneedthiscodeto //ensuretheextensionobjectscanfindtheresourcehandleforthe //module.IfyouwantyourextensiontobeindependentofMFC, //removethecommentsaroundthefollowingAfxGetResourceHandle() //andDllMain()functions,andremovetheg_hInstanceglobal //variable. /**** staticHINSTANCEg_hInstance; HINSTANCEAFXISAPIAfxGetResourceHandle() { returng_hInstance; } BOOLWINAPIDllMain(HINSTANCEhInst,ULONGulReason, LPVOIDlpReserved) { if(ulReason==DLL_PROCESS_ATTACH) { g_hInstance=hInst; } returnTRUE; } ****/ 

Now take a look at Listing 10-4, which contains the StdAfx.h file generated by the wizard. You'll notice a series of include statements. In order to remove reliance upon MFC, all but the line including afxisapi.h should be commented out or removed. Finally select the Project/Settings menu option, and on the General tab, change the Microsoft Foundation Classes setting to Not Using MFC, as shown in Figure 10-9. Once you've done this, the ISAPI extension can be compiled and used.

Listing 10-4

 #if!defined(AFX_STDAFX_H__BB9DC598_3253_11D3_864F_00105A1177A2__INCLUDED_) #defineAFX_STDAFX_H__BB9DC598_3253_11D3_864F_00105A1177A2__INCLUDED_ //stdafx.h:includefileforstandardsystem includefiles //orproject-specificincludefilesthatareusedfrequently //butarechangedinfrequently. // #include<afx.h> #include<afxwin.h> #include<afxmt.h>//forsynchronizationobjects #include<afxext.h> #include<afxisapi.h> //{{AFX_INSERT_LOCATION}} //MicrosoftVisualC++willinsertadditionaldeclarations //immediatelybeforethepreviousline. #endif  //!defined(AFX_STDAFX_H__BB9DC598_3253_11D3_864F_00105A1177A2__INCLUDED) 

click to view at full size.

Figure 10-9 The Project Settings dialog in Visual C++ 6.

Step 2: Modify the application to suit

The next step is to take the admittedly simple application created by the wizard and to modify it to respond as required. Before doing so, however, let's examine several aspects of the MFC ISAPI classes that will allow an application to be created with greater ease and flexibility. First take a look at the function that performs the default behavior when the wizard-created extension is run:

 voidCISAPIPhoneExtension::Default(CHttpServerContext*pCtxt) { StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt<<_T("Thisdefaultmessagewasproducedbythe"); *pCtxt<<_T("InternetServerDLLWizard.Edityour" "CISAPIPhoneExtension::Default()"); *pCtxt<<_T("implementationtochangeit.\r\n"); EndContent(pCtxt); } 

The first thing that you'll notice is that there is none of the actual HTML code that was in Listing 10-1. The next thing to notice is that rather than creating a string and then calling an API function to write the string, the text is streamed to the CHttpServerContext function pointed to by the single parameter passed into the Default function. The _T macro is used to properly handle text that can be standard ANSI or multibyte character text. Finally notice the methods that are called to begin content, write a title, and finally end content.

Since there is no obvious HTML code explicitly located in the Default function, what is sent to the browser? Running the default application results in the following HTML code, captured from the Microsoft Internet Explorer View/Source menu option:

 <html><head><title>DefaultMFCWebServerExtension</title></head> <body>ThisdefaultmessagewasproducedbytheInternetServerDLL Wizard.EdityourCIsapi1mfcExtension::Default()implementationto changeit. </body></html> 

In any good abstraction, the details that are not central to the task at hand are often just boilerplate. As such, the MFC ISAPI classes hide the details of how the HTML code is emitted . Obviously, the functions to start content, end content, and write the title are doing the job. What's also significant is that these methods can be overridden to allow customization that permits an entire application to be tied together.

The CHttpServer class is the base class for the major derived class in this application, CISAPIPhoneExtension . The CHttpServer class handles the bulk of the interaction with the IIS. This class takes the information passed in HTTP format and breaks it down so that it is readily available. This behavior can make the function handler code of an ISAPI application look more like a traditional function without requiring your own custom code to parse command lines, route commands, and so on.

Table 10-2 lists the significant methods of CHttpServer with explanations of their default functions and why they might be overridden. Several of these methods will be overridden in the example presented shortly.

While the ISAPI classes differ in many ways from the balance of the MFC classes ”primarily because ISAPI classes have no direct user interface ”they also share several features with many of the MFC classes. One significant feature these classes have in common is the use of some fairly exotic macros that allow class member functions to be mapped to the proper functionality. If you have done any serious MFC programming, the parse mapping macros should look familiar to you. The following code snippet is the parse map for the ISAPIPhone application:

 BEGIN_PARSE_MAP(CISAPIPhoneExtension,CHttpServer) //TODO:insertyourON_PARSE_COMMAND()and //ON_PARSE_COMMAND_PARAMS()heretohookupyourcommands. //Forexample: ON_PARSE_COMMAND(Default,CISAPIPhoneExtension,ITS_EMPTY) DEFAULT_PARSE_COMMAND(Default,CISAPIPhoneExtension) END_PARSE_MAP(CISAPIPhoneExtension) 

Table 10-2 The Methods of CHttpServer That Can Be Overridden

CHttpServer Method Description Why Override?
CallFunction Finds and executes the function associated with the command in the URL Generally CallFunction is not overridden since it is at the core of the class, but it might need to be overridden if your form requires multiselect list boxes. (See the Microsoft Knowledge Base article Q169109.)
OnParseError Constructs an error message to be presented to the client Override OnParseError to create custom messages when parse errors occur. (See Table 10-4 for a listing of possible parse errors.)
OnWriteBody Writes data back to the client Override OnWriteBody if you need to modify the body before it is written to the client.
HttpExtensionProc Called by the framework for each request to the ISAPI extension Overriding HttpExtensionProc is not recommended.
GetExtensionVersion Called by the framework when the ISAPI extension is loaded Call the default version, and then override to replace the default text string with your version.
ConstructStream Constructs a CHtmlStream object Override ConstructStream to create your own descendant of CHtmlStream . This might be useful if you want to optimize the performance of your CHtmlStream descendant for your application
TerminateExtension Provides a safe way to clean up threads and complete other shut-down tasks If the unload is of type HSE_TERM_ADVISORY_UNLOAD, the override function can return FALSE to indicate the extension is not ready to shut down.

Every MFC ISAPI application will have the BEGIN_PARSE_MAP and END_PARSE_MAP macros. BEGIN_PARSE_MAP takes the name of the derived class ”in this case CISAPIPhoneExtension ”and the class it is based upon ”in this case CHttpServer . At minimum, an application will have a single ON_PARSE_COMMAND macro. This macro accepts as arguments the name of the member function, the name of the class where the member function is declared, and an argument list. Default is the member function used here, and it is passed in along with the class name and the ITS_EMPTY, indicating that no arguments are expected with this command. Commonly, one other macro will be used, DEFAULT_PARSE_COMMAND. This macro specifies the name of a default function to be called and the class name to map the function to. The possible types that can be passed to the parse map macros are outlined in Table 10-3.

Table 10-3 Symbols and the Related Types for Use in Parse Mapping of Parameters

Symbol Meaning
ITS_EMPTY Used for no arguments. (Arguments cannot simply be blank.)
ITS_PSTR A pointer to a string.
ITS_RAW The exact raw data is sent to the ISAPI extension. Cannot be used in conjunction with other parameter types.
ITS_I2 A 2-byte integer (a short).
ITS_I4 A 4-byte integer (a long).
ITS_R4 A 4-byte floating-point number (a float).
ITS_R8 An 8-byte floating-point number (a double).

These powerful macros are in some ways the real magic behind the CHttpServer class. Imagine a native ISAPI extension like the one in Listing 10-1. Rather than supporting two functions, imagine that it supported 50 different functions. This would require pages of comparisons between the function name passed in and the functions that would be executed for each of those 50 functions. Each of those 50 functions would have to be declared and accessible somehow to the HttpExtensionProc function.

Now imagine a similarly complex ISAPI application using the framework provided by the CHttpServer class. Rather than pages of if and stricmp statements, the functions are mapped using the parse map macros. Additionally, the functions are not stand-alone functions but rather member functions of the CHttpServer -derived class. This is a much cleaner way to implement complex functionality.

Let's suppose that we wanted to add another function, such as a search function, to this CISAPIPhoneExtension class. The steps involved are as follows :

  • Add a member function to CISAPIPhoneExtension using the Class Wizard. The return type is void, and it needs to accept a CHttpServerContext pointer and a string pointer, as in Figure 10-10. Note that the CHttpServerContext pointer is mandatory for all handlers that will be mapped using the parse map, and the string pointer is used to contain the text that will be searched.
  • Figure 10-10 Adding a Search member function to the CISAPIPhoneExtension class in Visual C++ 6.

  • Add a line to the parse map: ON_PARSE_COMMAND(Search, CISAPIPhoneExtension, ITS_PSTR). This line directs all URL search requests to the Search function just created. ITS_PSTR indicates that the function takes a single string parameter.
NOTE
ON_PARSE_COMMAND looks like a traditional function call, but of course it is not. One of the significant differences is the way the macro handles multiple arguments. Suppose we had a function that allowed a more specialized search ” for example, one that allowed us to specify last name and first name separately. The function prototype for such a function might look like this:
 void CISAPIPhoneExtension::SearchName(CHttpServerContext* pCtxt,      LPSTR szLName, LPSTR szFName) 

Many programmers would enter a parse map entry for this function like this:

 ON_PARSE_COMMAND(SearchName, CISAPIPhoneExtension,      ITS_PSTR, ITS_PSTR) 

This is a plausible-looking entry, but it will not pass muster. The two ITS_PSTR entries are not two separate arguments ”they need to be a single argument, resulting in an entry that looks like this:

 ON_PARSE_COMMAND(SearchName, CISAPIPhoneExtension,      ITS_PSTR ITS_PSTR) 

Note the lack of a comma between the two ITS_PSTR argument type symbols.

The resulting code has no code between the opening and closing brackets of the Search method. For testing purposes, copy the default actions from the Default method into Search , changing references to the Default method as appropriate. Next add one line to the code, a line that simply echoes the value of the szSearchFor . The resulting method definition should look like the following code fragment:

 voidCISAPIPhoneExtension::Search(CHttpServerContext*pCtxt, LPSTRszSearchFor) { StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt<<_T("ThisSearchmessagewasproducedbythe"); *pCtxt<<_T("InternetServerDLLWizard."); *pCtxt<<_T("EdityourCISAPIPhoneExtension::Search()"); *pCtxt<<_T("implementationtochangeit.\r\n"); *pCtxt<<_T("Lookingfor")<<_T(szSearchFor); EndContent(pCtxt); } 

The next step is to actually get a query string to this method. While it is certainly possible to do so by simply typing a URL at the browser's address line, a more useful approach is to create a form that calls this function. This will enable us to see exactly how the information will come back from the browser. One way to do this would be to generate an ASP or HTML file similar to the file used to gather the search criteria for the ASP phone directory lookup example in Chapter 9. There is also another solution.

The Default method can be changed to do what the default.asp script did in the ASP phone directory lookup example. Not all of the code that was required for the ASP example ”notably the <HTML>, <BODY>, and <TITLE> sections ”needs to be present because the CHttpServer methods that start and end content and write the title will take care of that. In addition, the exact same objects and methods are not present in this ISAPI application, so it is not possible to use the same Response.Write syntax to emit the HTML to the client. The resulting code that writes out the HTML for a form when the default function of this ISAPI application is called is shown in the following code fragment:

 voidCISAPIPhoneExtension::Default(CHttpServerContext*pCtxt) { StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt<<_T("<CENTER>"); *pCtxt<<_T("<h1><aNAME=""top"">Test</a></h1>"); *pCtxt<<_T("<hr>"); *pCtxt<<_T("<FORMmethod=POSTaction=ISAPIPhone.dll?search>"); *pCtxt<<_T("<p>"); *pCtxt<<_T("Searchforphonenumber..."); *pCtxt<<_T("<p><p>"); *pCtxt<<_T("<INPUTTYPE=textNAME=lookFor>"); *pCtxt<<_T("<p>"); *pCtxt<<_T("<INPUTtype=submitVALUE=""OK"">"); *pCtxt<<_T("</CENTER>"); *pCtxt<<_T("<hr>"); *pCtxt<<_T("<h5>SimpleISAPIPhoneDirectoryTest<br>"); *pCtxt<<_T("</h5>"); EndContent(pCtxt); } 

This code uses the CHttpServerContext pointer that is passed in to stream out text to the client. In this case, most of the text is identical to the ASP example, with two exceptions. First the action of the form is no longer an ASP page, but rather a call to this same ISAPI extension's Search method. Second the description of the page has changed to reflect that this is an ISAPI application. The default screen generated by the modified version of Default is shown in Figure 10-11.

click to view at full size.

Figure 10-11 The ISAPIPhone.dll default screen.

When example criteria are entered on this screen, for instance "Reilly", the query string is parsed such that szSearchFor will contain "lookFor=Reilly". This is a good first step and would be completely acceptable if there were not more that the framework could do for you. In fact, there is one more step that can be taken that will result in the string entered into the criteria field being returned by the MFC framework directly to your application, with no further parsing required.

The addition of the parse map line for the Search method results in a parse map that contains the following lines:

 BEGIN_PARSE_MAP(CISAPIPhoneExtension,CHttpServer) ON_PARSE_COMMAND(Search,CISAPIPhoneExtension,ITS_PSTR) ON_PARSE_COMMAND(Default,CISAPIPhoneExtension,ITS_EMPTY) DEFAULT_PARSE_COMMAND(Default,CISAPIPhoneExtension) END_PARSE_MAP(CISAPIPhoneExtension) 

These lines allow both the Search and Default methods to be called, either directly or ”by not specifying another valid function ”implicitly. There is one additional line that can be part of a parse map that will move us one step closer to the optimal level of parsing done by the framework. That line uses the ON_PARSE_COMMAND_PARAMS macro. Adding the following line just after the first ON_PARSE_COMMAND macro line allows the framework to parse the command line and place the value of "lookFor" into the single string parameter to Search :

 ON_PARSE_COMMAND_PARAMS("lookFor") 

The ON_PARSE_COMMAND_PARAMS macro is even more powerful than this. If required, a default argument can be specified, and since the parameters are named, they can appear in different orders or can even be absent. For instance, if the default value for "lookFor" were to be "Dave Jones", the parse map would be modified as follows:

 ON_PARSE_COMMAND_PARAMS("lookFor='DaveJones'") 

Note that in this case the default value for this string argument contains a space. This space is enclosed in single quotes to signal that the entire string, including quotes, is the default. The completed parse map for the ISAPI Phone Directory Lookup example should be as follows:

 BEGIN_PARSE_MAP(CISAPIPhoneExtension,CHttpServer) ON_PARSE_COMMAND(Search,CISAPIPhoneExtension,ITS_PSTR) ON_PARSE_COMMAND_PARAMS("lookFor") ON_PARSE_COMMAND(Default,CISAPIPhoneExtension,ITS_EMPTY) DEFAULT_PARSE_COMMAND(Default,CISAPIPhoneExtension) END_PARSE_MAP(CISAPIPhoneExtension) 

Given the above parse map, a URL such as http://server/path/ISAPIPhone.dll? will send only the contents of the lookFor text control on the form as the szSearchFor argument to the Search method.

The parser can encounter a number of problems, which are indicated by the values returned by the CallFunction method. Table 10-4 lists the enum values for the parse errors that might be returned by CallFunction . These errors can be more fully described to the user by overriding the OnParseError method.

Table 10-4 Return Codes from the CallFunction Method

Enum Value Description
callOK The function call was successful.
callParamRequired A required parameter was missing.
callBadParamCount Too many or too few parameters.
callBadCommand The command name was not found.
callNoStackSpace No stack space was available.
callNoStream No CHtmlStream was available.
callMissingQuote A parameter was not properly formatted.
callMissingParams No parameters were available.
callBadParam A parameter was not correctly formed , for example, missing a quote.

The final step: Searching for a number

Once we've handled the details of communicating between the initial form and the function that will eventually display the results, the next step is to have the Search method actually get the results. Failing that, we report either the error or that no numbers were found matching the criteria. Listing 10-5 is the final version of this example program. Listing 10-6 is the final version of the StdAfx.h file required to compile the final program.

Listing 10-5

ISAPIPhone.cpp

 //ISAPIPHONE.CPP-ImplementationfileforyourInternetServer //ISAPIPhoneExtension #include"stdafx.h" #include"ISAPIPhone.h" //////////////////////////////////////////////////////////////////// //command-parsingmap BEGIN_PARSE_MAP(CISAPIPhoneExtension,CHttpServer) //TODO:insertyourON_PARSE_COMMAND()and //ON_PARSE_COMMAND_PARAMS()heretohookupyourcommands. //Forexample: ON_PARSE_COMMAND(Search,CISAPIPhoneExtension,ITS_PSTR) ON_PARSE_COMMAND_PARAMS("lookFor") ON_PARSE_COMMAND(Default,CISAPIPhoneExtension,ITS_EMPTY) DEFAULT_PARSE_COMMAND(Default,CISAPIPhoneExtension) END_PARSE_MAP(CISAPIPhoneExtension) //////////////////////////////////////////////////////////////////// //TheoneandonlyCISAPIPhoneExtensionobject CISAPIPhoneExtensiontheExtension; //////////////////////////////////////////////////////////////////// //CISAPIPhoneExtensionimplementation CISAPIPhoneExtension::CISAPIPhoneExtension() { } CISAPIPhoneExtension::~CISAPIPhoneExtension() { } BOOLCISAPIPhoneExtension::GetExtensionVersion(HSE_VERSION_INFO*pVer) { //Calldefaultimplementationforinitialization. CHttpServer::GetExtensionVersion(pVer); //Loaddescriptionstring. TCHARsz[HSE_MAX_EXT_DLL_NAME_LEN+1]; ISAPIVERIFY(::LoadString(AfxGetResourceHandle(), IDS_SERVER,sz,HSE_MAX_EXT_DLL_NAME_LEN)); _tcscpy(pVer->lpszExtensionDesc,sz); returnTRUE; } BOOLCISAPIPhoneExtension::TerminateExtension(DWORDdwFlags) { //Extensionisbeingterminated. //TODO:Cleanupanyper-instanceresources. returnTRUE; } //////////////////////////////////////////////////////////////////// //CISAPIPhoneExtensioncommandhandlers voidCISAPIPhoneExtension::Default(CHttpServerContext*pCtxt) { StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt<<_T("<CENTER>"); *pCtxt<<_T("<h1><aNAME=""top"">Test</a></h1>"); *pCtxt<<_T("<hr>"); *pCtxt<<_T("<FORMmethod=POSTaction=ISAPIPhone.dll?search>"); *pCtxt<<_T("<p>"); *pCtxt<<_T("Searchforphonenumber..."); *pCtxt<<_T("<p><p>"); *pCtxt<<_T("<INPUTTYPE=textNAME=lookFor>"); *pCtxt<<_T("<p>"); *pCtxt<<_T("<INPUTtype=submitVALUE=""OK"">"); *pCtxt<<_T("</CENTER>"); *pCtxt<<_T("<hr>"); *pCtxt<<_T("<h5>SimpleISAPIPhoneDirectoryTest<br>"); EndContent(pCtxt); } voidCISAPIPhoneExtension::Search(CHttpServerContext*pCtxt, LPSTRszSearchFor) { charstrSQL[255]; UWORDcol=1; UWORDrc; CODBCDatabase*db; CODBCCursor*curs; StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt<<_T("<CENTER><FORMmethod=POST" "action=ISAPIPhone.dll?>"); *pCtxt<<_T("<INPUTtype=submit" "VALUE=""Return""></FORM></CENTER>"); try { //SetuptheODBCstuff... db=newCODBCDatabase("Phones");//UserIDandPassword //notneeded. if(db->isConnected()) { curs=newCODBCCursor(db); //Pickthetable. curs->setTables("tblPhone"); //Bindthecolumns. curs->bindColumn(col,"LName",255); curs->bindColumn(col,"FName",255); curs->bindColumn(col,"Dept",255); curs->bindColumn(col,"Facility",255); curs->bindColumn(col,"Phone",255); curs->setOrderBy("LName,FName,Dept"); //BecausewehaveusedtheON_PARSE_COMMAND_PARAMSmacro //above,theargumenttothisfunctionwillbecompletely //parsedforus.Thus,ratherthanszSearchForbeing //"lookFor=enteredValue"itwilljustbeequalto //"enteredValue". strcpy(strSQL,"WHERESearchStringlike'%"); strcat(strSQL,szSearchFor); strcat(strSQL,"%'"); curs->setRestrict(strSQL); curs->doSelect(); if(rc=curs->fetch()==SQL_SUCCESS) { //Buildthetableheader.... *pCtxt<<_T("<CENTER>"); *pCtxt<<_T("<P><TABLEBORDER=1><TR>"); *pCtxt<<_T("<TDBGCOLOR=darkcyan><B>LastName" "</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=darkcyan><B>FirstName" "</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=darkcyan><B>Dept" "</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=darkcyan><B>Facility" "</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=darkcyan><B>Phone" "</B></TD>"); *pCtxt<<_T("</TR><H3>"); do{ *pCtxt<<_T("<TRBGCOLOR=WHITE>"); *pCtxt<<_T("<TDBGCOLOR=blue><B>"<< (char*)(*curs)["LName"]<<"</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=blue><B>"<< (char*)(*curs)["FName"]<<"</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=blue><B>"<< (char*)(*curs)["DEPT"]<<"</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=blue><B>"<< (char*)(*curs)["Facility"]<<"</B></TD>"); *pCtxt<<_T("<TDBGCOLOR=blue><B>"<< (char*)(*curs)["Phone"]<<"</B></TD>"); *pCtxt<<_T("</TR>"); }while((rc=curs->fetch())==SQL_SUCCESS); *pCtxt<<_T("</H3>"); *pCtxt<<_T("</CENTER)"); } else { *pCtxt<<_T("<CENTER><H2>Sorry!Nomatch" "found!</H2></CENTER><BR>"); } deletecurs; } else { *pCtxt<<_T("<CENTER><H2>Sorry!Can'tconnect" "todatabase!</H2></CENTER><BR>"); } deletedb; } catch(char*str) { *pCtxt<<_T("<CENTER><H2>Exceptionthrownby" "database!</H2></CENTER><BR>"); *pCtxt<<_T(str); } EndContent(pCtxt); } //Donoteditthefollowinglines,whichareneededbyClassWizard. #if0 BEGIN_MESSAGE_MAP(CISAPIPhoneExtension,CHttpServer) //{{AFX_MSG_MAP(CISAPIPhoneExtension) //}}AFX_MSG_MAP END_MESSAGE_MAP() #endif//0 //////////////////////////////////////////////////////////////////// //IfyourextensionwillnotuseMFC,you'llneedthiscodeto //ensuretheextensionobjectscanfindtheresourcehandleforthe //module.IfyouconvertyourextensiontobeindependentofMFC, //removethecommentsaroundthefollowingAfxGetResourceHandle() //andDllMain()functions,aswellastheg_hInstanceglobal //variable. /****/ staticHINSTANCEg_hInstance; HINSTANCEAFXISAPIAfxGetResourceHandle() { returng_hInstance; } BOOLWINAPIDllMain(HINSTANCEhInst,ULONGulReason, LPVOIDlpReserved) { if(ulReason==DLL_PROCESS_ATTACH) { CODBCDatabase::InitCriticalSection(); g_hInstance=hInst; } elseif(ulReason==DLL_PROCESS_DETACH) { CODBCDatabase::DelCriticalSection(); } returnTRUE; } /****/ voidCISAPIPhoneExtension::StartContent(CHttpServerContext*pCtxt)const { //TODO:Addyourspecializedcodehereand/orcall //thebaseclass. *pCtxt<<_T("<HTML><BODYbgColor=BlueText=yellow>"); //CHttpServer::StartContent(pCtxt); } LPCTSTRCISAPIPhoneExtension::GetTitle()const { //TODO:Addyourspecializedcodehereand/orcall //thebaseclass. return"SimpleISAPIPhoneDirectoryTest"; //returnCHttpServer::GetTitle(); } 

Listing 10-6

StdAfx.h

 #if!defined(AFX_STDAFX_H__4EDA42D8_31AE_11D3_864E_00105A1177A2__INCLUDED_) #defineAFX_STDAFX_H__4EDA42D8_31AE_11D3_864E_00105A1177A2__INCLUDED_ //stdafx.h:includefileforstandardsystemincludefiles //orproject-specificincludefilesthatareusedfrequentlybut //arechangedinfrequently // //Thewizardplacesthesehere.Commentoutso //therestoftheMFCstuffisnotlinkedin. //#include<afx.h> //#include<afxwin.h> //#include<afxmt.h>//forsynchronizationobjects //#include<afxext.h> #include<afxisapi.h> #include"SQL.H" #include"sqlext.h" #include"odbclass.h" //{{AFX_INSERT_LOCATION}} //MicrosoftVisualC++willinsertadditionaldeclarations //immediatelybeforethepreviousline. #endif//!defined(AFX_STDAFX_H__4EDA42D8_31AE_11D3_864E_ 00105A1177A2__INCLUDED) 

The most important parts of Listing 10-5 are the modifications to Search . In order to retrieve the records that meet the criteria entered by the user, the ODBC classes described in Chapter 8 will be used. Most of the modifications to the project required to do the data retrieval are straightforward, using methods as outlined in Chapter 8's explanation of the ODBC classes. Listing 10-6 shows the additional header files that need to be included for support of the ODBC classes: standard header files SQL.h and SQLExt.h and the ODBClass.h file from Chapter 8. ODBClass.cpp is added to the project as well.

One of the requirements of the CODBCDatabase class is that a static critical section be initialized and then deleted after the last instance of the class is destroyed. DLLs have a function that is roughly equivalent to the main function used by console mode programs and the WinMain function used by GUI Windows programs. This function is called DllMain , and the default version created by the ISAPI Extension Wizard does nothing but set a single variable, g_hInstance , when the DLL_PROCESS_ATTACH message is received. This function is modified in the final version to allow the CODBCDatabase 's critical section to be initialized as the process attaches to the ISAPI Extension and destroyed as the process detaches. This is done with simple if-then-else logic. If thread attach and detach messages had to be handled as well, a case statement might be in order.

The Search method first creates the required objects for the CODBCDatabase and CODBCCursor classes. If the connection to the database cannot take place, for instance, if the data source name is incorrect or if an invalid user name or password is used, the method sends a message indicating a failure to connect to the database. Once we've constructed all the objects, we set the table name, bind the columns, and set the restrictions and order by clauses. We then perform the select and fetch the data.

The Search method sends the actual HTML source to the client browser. Alternatives exist for this behavior. If there were lots of tables to create, we could create objects to handle the start and finish of rows and cells of tables. For this small example, we can easily hand-code these bits of HTML.

ISAPI Error Handling

One change was required to make the database classes from Chapter 8 safe for inclusion in an ISAPI extension. In the version of these classes we used in the console mode example programs in Chapter 8, the error handling was quite simple: just print the error to the screen using printf . This is obviously not adequate for an ISAPI extension.

The problem with error handling in any class that is likely to be used in many different contexts is how the error is to be presented to the user. For instance, a message box would have been a more Windows-like solution to presenting error information, but this too would fail for ISAPI applications in particular and server applications in general.

The solution used in this example is C++'s exception handling. Using three new keywords ” try , catch , and throw ”classes such as the ODBC classes used here can format a message that conveys the meaning of the error and throw that exception, with the message, to whatever level above that exists to catch the exception. Thus the presentation of the exception is kept separate from the generation of the exception, allowing the consumer application to make the user aware that an exception has occurred.

More complex exception handling is also possible. In this simple example, a character pointer is what is thrown by the ODBC classes error handling function, and the code within the Search function catches exceptions of type char *. In real-world examples, there will likely be more information available about the error, and simply placing that information into a string is not the best solution. For this sort of situation, a data structure can be defined that captures all of the exception information, and that structure can be thrown. It is important that the calling program be aware of the type of exception that might be thrown, because a program can best handle exceptions that it is prepared to properly handle and display.

If you look back at Figure 10-11, you'll notice that the background color and the text color are different from the colors used in the ASP version of this program. All screens in this ISAPI extension use the same color scheme, and overriding one of the overridable functions mentioned in Table 10-2 sets those colors. The StartContent virtual method of the CHttpServer class is overridden to set the properties used for the body of each HTML page generated by the extension. To add the override function, right-click CISAPIPhoneExtension in the ClassView of the project and select Add Virtual Function from the context menu. A list of new overridable functions as well as currently overridden functions will appear. Select StartContent from this menu, and a new member function will be added to the class. The default implementation does nothing more than call the base class:

 voidCISAPIPhoneExtension::StartContent(CHttpServerContext*pCtxt)const { //TODO:Addyourspecializedcodehereand/or //callthebaseclass. CHttpServer::StartContent(pCtxt); } 

In some cases, it is appropriate to call the base class and take some other action. Here it is sufficient to replace the base class's functionality by adding a line similar to the following:

 *pCtxt<<_T("<HTML><BODYbgColor=BlueText=yellow>"); 

That simple replacement makes all pages use the specified color background and text. Of course, you can get more adventurous by creating a stock scheme for all pages that includes background images and even page headings. This is a simple way to ensure uniformity within the application created by this ISAPI extension.

Another simple customization that can be done by overriding a virtual function is setting a custom title. This can be accomplished in one of two ways. In the example ISAPI extension, GetTitle is overridden, with a static title returned when requested. WriteTitle , the virtual function actually used to write out the HTML that generates the title, can also be overridden. The effect is the same in either case. A real-world example might create titles based upon the application's special knowledge of the current state of the application.



Inside Server-Based Applications
Inside Server-Based Applications (DV-MPS General)
ISBN: 1572318171
EAN: 2147483647
Year: 1999
Pages: 91

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net