ActiveX Document Server Example EX36B

Look at the pizza form example from Chapter 35 (EX35A). Note that the server (the ISAPI DLL) processes the order only when the customer clicks the Submit Order Now button. This is okay for ordering pizzas because you're probably happy to accept money from anyone, no matter what kind of browser is used.

For a form-based intranet application, however, you can be more selective. You can dictate what browser your clients have, and you can distribute your own client software on the net. In that environment, you can make data entry more sophisticated, allowing, for example, the client computer to validate each entry as the user types it. That's exactly what's happening in EX36B, which is another ActiveX document server, of course. EX36B is a form-based employee time-sheet entry program that works inside Internet Explorer (as shown in Figure 36-4) or works as a stand-alone application. Looks like a regular HTML form, doesn't it? It's actually an MFC form view, but the average user probably won't know the difference. The Name field is a drop-down combo box, however—which is different from the select field you would see in an HTML form—because the user can type in a value if necessary. The Job Number field has a spin button control that helps the user select the value. These aren't necessarily the ideal controls for time-sheet entry, but the point here is that you can use any Windows controls you want, including tree controls, list controls, trackbars, and ActiveX controls, and you can make them interact any way you want.

Figure 36-4. Employee time-sheet entry form.

Field Validation in an MFC Form View

Problem: MFC's standard validation scheme validates data only when CWnd::UpdateData(TRUE) is called, usually when the user exits the dialog. Applications often need to validate data the moment the user leaves a field (edit control, list box, and so on). The problem is complex because Windows permits the user to freely jump between fields in any sequence by using the mouse. Ideally, standard MFC DDX- /DDV (data exchange/validation) code should be used for field validation, and the standard DoDataExchange function should be called when the user finishes the transaction.

Solution: Derive your field validation form view classes from the class CValidForm, derived from CFormView, with this header:

// valform.h
#ifndef _VALIDFORM
#define _VALIDFORM

#define WM_VALIDATE WM_USER + 5

class CValidForm : public CFormView
{
DECLARE_DYNAMIC(CValidForm)
private:
    BOOL m_bValidationOn;
public:
    CValidForm(UINT ID);
    // override in derived dlg to perform validation
    virtual void ValidateDlgItem(CDataExchange* pDX, UINT ID);
    //{{AFX_VIRTUAL(CValidForm)
    protected:
    virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
    //}}AFX_VIRTUAL

    //{{AFX_MSG(CValidForm)
    afx_msg LONG OnValidate(UINT wParam, LONG lParam);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
};
#endif // _VALIDFORM

This class has one virtual function, ValidateDlgItem, which accepts the control ID as the second parameter. The derived form view class implements this function to call the DDX/DDV functions for the appropriate field. Here is a sample ValidateDlgItem implementation for a form view that has two numeric edit controls:

void CMyForm::ValidateDlgItem(CDataExchange* pDX, UINT uID)
{
    switch (uID) {
    case IDC_EDIT1:
        DDX_Text(pDX, IDC_EDIT1, m_nEdit1);
        DDV_MinMaxInt(pDX, m_nEdit1, 0, 10);
        break;
    case IDC_EDIT2:
        DDX_Text(pDX, IDC_EDIT2, m_nEdit2);
        DDV_MinMaxInt(pDX, m_nEdit2, 20, 30);
        break;
    default:
        break;
    }
}

Notice the similarity to the wizard-generated DoDataExchange function:

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
    //{{AFX_DATA_MAP(CMyForm)
    DDX_Text(pDX, IDC_EDIT1, m_nEdit1);
    DDV_MinMaxInt(pDX, m_nEdit1, 0, 10);
    DDX_Text(pDX, IDC_EDIT2, m_nEdit2);
    DDV_MinMaxInt(pDX, m_nEdit2, 20, 30);
    //}}AFX_DATA_MAP
}

How does it work? The CValidForm class traps the user's attempt to move away from a control. When the user presses the Tab key or clicks on another control, the original control sends a killfocus command message (a control notification message) to the parent window, the exact format depending on the kind of control. An edit control, for example, sends an EN_KILLFOCUS command. When the form window receives this killfocus message, it invokes the DDX/DDV code that is necessary for that field, and if there's an error, the focus is set back to that field.

There are some complications, however. First, we want to allow the user to freely switch the focus to another application—we're not interested in trapping the killfocus message in that case. Next, we must be careful how we set the focus back to the control that produced the error. We can't just call SetFocus in direct response to the killfocus message; instead we must allow the killfocus process to complete. We can achieve this by posting a user-defined WM_VALIDATE message back to the form window. The WM_VALIDATE handler calls our ValidateDlgItem virtual function after the focus has been moved to the next field. Also, we must ignore the killfocus message that results when we switch back from the control that the user tried to select, and we must allow the IDCANCEL button to abort the transaction without validation.

Most of the work here is done in the view's virtual OnCommand handler, which is called for all control notification messages. We could, of course, individually map each control's killfocus message in our derived form view class, but that would be too much work. Here is the OnCommand handler:

BOOL CValidForm::OnCommand(WPARAM wParam, LPARAM lParam) 
{
    // specific for WIN32 — wParam/lParam processing different for 
    //  WIN16
    TRACE("CValidForm::OnCommand, wParam = %x, lParam = %x\n", 
        wParam, lParam);
    TRACE("m_bValidationOn = %d\n", m_bValidationOn);
    if(m_bValidationOn) {   // might be a killfocus
        UINT notificationCode = (UINT) HIWORD( wParam );
        if((notificationCode == EN_KILLFOCUS)  ||
           (notificationCode == LBN_KILLFOCUS) ||
           (notificationCode == CBN_KILLFOCUS) ) {
            CWnd* pFocus = CWnd::GetFocus(); // static function call
            // if we're changing focus to another control in the 
            //  same form
            if( pFocus && (pFocus->GetParent() == this) ) {
                if(pFocus->GetDlgCtrlID() != IDCANCEL) {
                    // and focus not in Cancel button
                    // validate AFTER drawing finished
                    BOOL rtn = PostMessage( WM_VALIDATE, wParam );
                    TRACE("posted message, rtn = %d\n", rtn);
                }
            }
        }
    }
    return CFormView::OnCommand(wParam, lParam); // pass it on
}

Note that m_bValidationOn is a Boolean data member in CValidForm.

Finally, here is the OnValidate message handler, mapped to the user-defined WM_VALIDATE message:

LONG CValidForm::OnValidate(UINT wParam, LONG lParam)
{
    TRACE("Entering CValidForm::OnValidate\n");
    CDataExchange dx(this, TRUE);
    m_bValidationOn = FALSE;   // temporarily off
    UINT controlID = (UINT) LOWORD( wParam );
    try {
        ValidateDlgItem(&dx, controlID);
    }
    catch(CUserException* pUE) {
        pUE->Delete();
        TRACE("CValidForm caught the exception\n");
        // fall through — user already alerted via message box
    }
    m_bValidationOn = TRUE;
    return 0;     // goes no further
}

Instructions for use:

  1. Add valform.h and valform.cpp to your project.

  2. Insert the following statement in your view class header file:

    #include "valform.h"

  3. Change your view class base class from CFormView to CValidForm.

  4. Override ValidateDlgItem for your form's controls as shown above.

That's all.

For dialogs, follow the same steps, but use valid.h and valid.cpp. Derive your dialog class from CValidDialog instead of from CDialog.

Generating POST Requests Under Program Control

The heart of the EX36B program is a worker thread that generates a POST request and sends it to a remote server. The server doesn't care whether the POST request came from an HTML form or from your program. It could process the POST request with an ISAPI DLL, with a PERL script, or with a Common Gateway Interface (CGI) executable program.

Here's what the server receives when the user clicks the EX36B Submit button:

POST scripts/ex35a.dll?ProcessTimesheet HTTP/1.0
(request headers)
(blank line)
Period=12&Name=Dunkel&Hours=6.5&Job=5

And here's the thread code from PostThread.cpp:

// PostThread.cpp (uses MFC WinInet calls) #include <stdafx.h> #include "PostThread.h" #define MAXBUF 50000 CString g_strFile = "/scripts/ex35a.dll"; CString g_strServerName = "localhost"; CString g_strParameters; UINT PostThreadProc(LPVOID pParam) { CInternetSession session; CHttpConnection* pConnection = NULL; CHttpFile* pFile1 = NULL; char* buffer = new char[MAXBUF]; UINT nBytesRead = 0; DWORD dwStatusCode; BOOL bOkStatus = FALSE; try { pConnection = session.GetHttpConnection(g_strServerName, (INTERNET_PORT) 80); pFile1 = pConnection->OpenRequest(0, g_strFile + "?ProcessTimesheet", // POST request NULL, 1, NULL, NULL, INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_RELOAD); // no cache pFile1->SendRequest(NULL, 0, (LPVOID) (const char*) g_strParameters, g_strParameters.GetLength()); pFile1->QueryInfoStatusCode(dwStatusCode); if(dwStatusCode == 200) { // OK status // doesn't matter what came back from server — we're looking // for OK status bOkStatus = TRUE; nBytesRead = pFile1->Read(buffer, MAXBUF - 1); buffer[nBytesRead] = `\0'; // necessary for TRACE TRACE(buffer); TRACE("\n"); } } catch(CInternetException* pe) { char text[100]; pe->GetErrorMessage(text, 99); TRACE("WinInet exception %s\n", text); pe->Delete(); } if(pFile1) delete pFile1; // does the close — prints a warning if(pConnection) delete pConnection; // Why does it print a warning? delete [] buffer; ::PostMessage((HWND) pParam, WM_POSTCOMPLETE, (WPARAM) bOkStatus, 0); return 0; }

The main thread assembles the g_strParameters string based on what the user typed, and the worker thread sends the POST request using the CHttpFile::SendRequest call. The tQueryInfoStatusCode to find out if the server sent back an OK response. Before exiting, the thread posts a message to the main thread, using the bOkStatus value in wParam to indicate success or failure.

The EX36B View Class

The CEx36bView class is derived from CValidForm, as described in "Field Validation in an MFC Form View". CEx36bView collects user data and starts the post thread when the user clicks the Submit button after all fields have been successfully validated. Field validation is independent of the internet application. You could use CValidForm in any MFC form view application.

Here is the code for the overridden or the overridden ValidateDlgItem member function, which is called whenever the user moves from one control to another:

void CEx36bView::ValidateDlgItem(CDataExchange* pDX, UINT uID)
{
    ASSERT(this);
    TRACE("CEx36bView::ValidateDlgItem\n");
     switch (uID) {
    case IDC_EMPLOYEE:
        DDX_CBString(pDX, IDC_EMPLOYEE, m_strEmployee);
        // need custom DDV for empty string
        DDV_MaxChars(pDX, m_strEmployee, 10);
        if(m_strEmployee.IsEmpty()) {
            AfxMessageBox("Must supply an employee name");
            pDX->Fail();
        }
        break;
    case IDC_HOURS:
        DDX_Text(pDX, IDC_HOURS, m_dHours);
        DDV_MinMaxDouble(pDX, m_dHours, 0.1, 100.);
        break;
    case IDC_JOB:
        DDX_Text(pDX, IDC_JOB, m_nJob);
        DDV_MinMaxInt(pDX, m_nJob, 1, 20);
        break;
    default:
        break;
    }
    return;
}

The OnSubmit member function is called when the user clicks the Submit button. CWnd::UpdateData returns TRUE only when all the fields have been successfully validated. At that point, the function disables the Submit button, formats g_strParameters, and starts the post thread.

void CEx36bView::OnSubmit() 
{
    if(UpdateData(TRUE) == TRUE) {
        GetDlgItem(IDC_SUBMIT)->EnableWindow(FALSE);
        CString strHours, strJob, strPeriod;
        strPeriod.Format("%d", m_nPeriod);
        strHours.Format("%3.2f", m_dHours);
        strJob.Format("%d", m_nJob);
        g_strParameters = "Period=" + strPeriod + "&Employee=" + 
            m_strEmployee + "&Hours=" +strHours + "&Job=" + 
            strJob + "\r\n";
        TRACE("parameter string = %s", g_strParameters);
        AfxBeginThread(PostThreadProc, GetSafeHwnd(), 
            THREAD_PRIORITY_NORMAL);
    }
}

The OnCancel member function is called when the user clicks the Reset button. The CValidForm logic requires that the button's control ID be IDCANCEL.

void CEx36bView::OnCancel() 
{
    CEx36bDoc* pDoc = GetDocument();
    m_dHours = 0;
    m_strEmployee = "" ;
    m_nJob = 0;
    m_nPeriod = pDoc->m_nPeriod;
    UpdateData(FALSE);
}

The OnPostComplete handler is called in response to the user-defined WM_POSTCOMPLETE message sent by the post thread:

LONG CEx36bView::OnPostComplete(UINT wParam, LONG lParam)
{
    TRACE("CEx36bView::OnPostComplete - %d\n", wParam);
    if(wParam == 0) {
        AfxMessageBox("Server did not accept the transaction");
    }
    else 
        OnCancel();
    GetDlgItem(IDC_SUBMIT)->EnableWindow(TRUE);
    return 0;
}

This function displays a message box if the server didn't send an OK response. It then enables the Submit button, allowing the user to post another time-sheet entry.

Building and Testing EX36B

Build the /vcpp36/ex36b project, and then run it once in stand-alone mode to register it and to write a document file called test.36b in your WWW root directory. Make sure the EX35A DLL is available in the scripts directory (with execute permission) because that DLL contains an ISAPI function, ProcessTimesheet, which handles the server end of the POST request. Be sure that you have IIS or some other ISAPI-capable server running.

Now run Internet Explorer and load test.36b from your server. The EX36B program should be running in the Browse window, and you should be able to enter time-sheet transactions.

ActiveX Document Servers vs. VB Script

It's possible to insert VB Script (or JavaScript) code into an HTML file. We're not experts on VB Script, but we've seen some sample code. You could probably duplicate the EX36B time-sheet application with VB Script, but you would be limited to the standard HTML input elements. It would be interesting to see how a VB Script programmer would solve the problem. (In any case, you're a C++ programmer, not a Visual Basic programmer, so you might as well stick to what you know.)