Integrating a web site with a structured database poses a particular challenge.

How can a developer provide a flexible, easy to use, yet robust search interface for users to query the information stored in the database? In this article Beth provides a complete solution for this problem using Visual FoxPro, ADO and XML, by providing a dynamic searching mechanism in the middle tier that users can control from the front end.

If you've ever developed a database application, you know how important queries are for locating information accurately and quickly. Users want to query data in a variety of combinations, which means that their search interfaces need to be robust and easy to use. When these applications move to the web, the need for a scalable, dynamic and flexible search mechanism becomes more crucial, and providing a flexible design becomes more of a challenge. If you're developing a web application that allows users to perform dynamic searches, this article is for you.

XML does well in heterogeneous environments or in situations where data must be marshaled across the internet

When building our components, we must consider what format will be used to pass data back and forth from our middle tier objects to our client. We call these containers of data “resources.” Resources in their purest sense can take the form of ADO recordsets, XML streams, VFP cursors, arrays, strings or anything else that can contain data or state information. In distributed applications, however, we need a resource that can be easily marshaled across our application tiers. This usually weeds out VFP cursors and arrays because they cannot natively marshal themselves across process boundaries.

ADO recordsets work well in homogeneous environments where the servers are all Windows servers on the same network. XML streams do well in heterogeneous environments or in situations where data must be marshaled across the Internet. Our “data source” components are the business objects responsible for executing the queries and creating the resources that are passed back. I have chosen in this example to pass back our data in disconnected ADO recordsets, because our middle-tier components and the database are on the same Windows network.

Next, we need to figure out how we are going to pass search criteria parameters from the client to the data source components. When a user is searching for something, we usually present her with a screen to enter filter criteria. These criteria are interpreted by our application and translated into a query statement against the database. We need to construct a search criteria resource that can package up the user's criteria and send it to the data source. I'm going to show you how to create two types of search criteria resources: XML and disconnected ADO recordsets.

In this example, I chose to use Visual FoxPro to build my components because of its elegance and object-oriented nature. However, I use ADO in my data source components as the method for querying and returning data from the database to the web server, so any COM language like Visual Basic would do just fine. All the source code I will be presenting can be downloaded from www.flashcreative.com or from my website at www.BethMassi.com.

The example application I have created consists of a Visual FoxPro Database, Visual FoxPro middle tier DLL, and few ASP pages (search and results pages) for ADO and XML search criteria resources. If you plan on a large volume of users on your site, you may want to place the component in Microsoft Transaction Server (or Components Services in Windows 2000. In our application, however, we are making queries against a VFP database that doesn't support MTS transactions. Additionally, even if we were using a database that was COM-TX or XA compatible, we are not writing to it, so transaction support is unnecessary and would just be additional overhead.

How Does It Work?

To perform complex queries in our web application, the search page collects the search criteria from the user and then calls the data source component's GetRSSearchCriteriaResource or GetXMLSearchCriteriaResource interfaces (see Figure 1). These are public methods defined in the data source's CAbstractDataSource class definition. These interfaces pass back the Search Criteria Resource (SCR) that can take the form of either an ADO recordset or an XML stream. The client can be the web server managing the resource, or we could even have the client browser manipulate an XML resource directly. Internet Explorer 5 and higher supports XML and comes with the MSXML parser to manipulate XML on the client. I will show how to do this later in the article.

Figure 1 - Executing Dynamic Queries
Figure 1 - Executing Dynamic Queries

Having the middle tier generate the SCR makes it easer on the presentation service, because it simply collects the criteria from the user and places it in the SCR. The presentation service also doesn't need to know the details of the underlying data. The downside is that we are making an extra call to the middle tier. If we wanted to avoid this, we could have the presentation service create the SCR itself. The search criteria resource contains fields from the data source that created it. By default it contains only the fields from the data source's primary table. However, I'll show you how we can easily specify additional elements in the SCR from other data sources.

The client fills the search criteria resource with the user's selection criteria and then passes it back into an interface called GetDataSourceNameBySearchCriteria, where DataSourceName is the name of the particular data source component we are calling. There are two data sources in the example: Customer and CustomerNote. The search interface on Customer is GetCustomerBySearchCriteria and the search interface on CustomerNote is GetCustomerNoteBySearchCriteria. The Search Criteria Manager (SCM) on the middle tier then takes the SCR and iterates it, calling on the Syntax Object to create the SQL-SELECT clauses, which are used to generate the SQL statement in the Data source (Figure 2). The class definitions for the SCM's and Syntax classes are located in the accompanying code in the file CSearchCriteria.prg.

Figure 2 - Object Instantiation Diagram
Figure 2 - Object Instantiation Diagram

The Search Criteria Resource (SCR)

Packaging the Search Criteria Data

The Search Criteria Resource is the mechanism by which the search criteria is passed between the component and the client. They can take the form of ADO Recordsets or XML streams. The Search Criteria Manager dictates the structure of these resources. By default, the SCR has the following character properties:

  • Table - The name of the base table
  • Name - Field Name
  • Type - ADO DataTypeEnum represented as a character
  • Value - Field Value
  • EndValue - Field End Value (for retrieving ranges of data)
  • Order - The order
  • Operator - Comparison operator: LIKE, BETWEEN, $, =
  • Options - Order options like DESCENDING

This list of properties is defined by the Search Criteria Managers and can be subclassed to allow additional properties. In the case of an ADO recordset, the properties above would represent the fields collection in the recordset and you would have X number of rows depending on how many searchable fields you wanted to provide. In the case of an XML stream, you would have <FIELD> nodes for each searchable field with the properties above being elements of the field node.

<?xml version='1.0'?>
<SEARCHCRITERIA>
  <FIELD>
    <TABLE>CUSTOMER</TABLE>
    <NAME>lastname</NAME>
    <TYPE>129</TYPE>
    <ORDER></ORDER>
    <OPTION></OPTION>
    <OPERATOR>LIKE</OPERATOR>
    <VALUE></VALUE>
    <ENDVALUE></ENDVALUE>
  </FIELD>
  .
  .
  .
</SEARCHCRITERIA>

The client receives this SCR, which it then fills in with the appropriate values, and passes it back via the GetDataSourceNameBySearchCriteria() method.

Data Source Functionality

Adding Additional Search Criteria

By default, the SCR contains only fields from the data source's primary table, as declared in the cTableName property of the data source. The SCM creates a default set of search criteria by calling the GetEmptyResource() method of the data source. The GetEmptyResource() method creates an empty ADO recordset that has the exact same structure as the data source's primary table. This method is called transparently when the GetXMLSearchCriteriaResource() or GetRSSearchCriteriaResource() methods are called from the client. In this example, we need to provide additional fields in the search criteria resource for our CustomerNote data source, to allow the client to search on note contents. Therefore, in the data source method AddSearchCriteriaInfo_Post, we have additional code to create extra nodes/rows on the resource before it is sent to the client. So, in the CustomerNote data source in aDatasources.prg, you will see:

Protected Function AddSearchCriteriaInfo_Post( roRS )
    *----------------------------------
    With This.oSearchReferences.oSearchCriteriaManager
        .AddSearchElements( @roRS, "Customer", "LastName", adChar )
        .AddSearchElements( @roRS, "Customer", "FirstName", adChar )
    EndWith
    
    Return FILE_OK
Endfunc

The fields passed to the Search Criteria Manager's AddSearchElements() method are the SCR, the table name, the field name and the data type. This causes a new node/row of search criteria data to be added to the SCR.

The Search Criteria Manager (SCM) Classes

Generating and Iterating Search Criteria Resources

The Search Criteria Managers are not only used when sending SCRs out of the data source, but also (and more importantly) they are used to iterate the SCRs being sent into the data source to create the clauses of the SELECT statements. To understand exactly how the clauses are constructed, we must look deeper into the Search Criteria Manager. The SCM “manages” the Search Criteria Resources. The SCMs are the objects that dictate the structure of these SCRs as well as the method for iterating them. The SCM is controlled by the data source and can be different, depending on the SCR. In the example, I have included two SCM's; CSearchCriteriaManagerXML and CSearchCriteriaManagerRS, which are subclasses of CAbstractSearchCriteriaManager. The Data source's CreateFilterFromResource() method instantiates the proper SCM based on the type of the SCR. If the SCR is an XML stream, the Data source instantiates the CSearchCriteriaManagerXML object; if the SCR is an ADO recordset, the CSearchCriteriaManagerRS is instantiated. If the type of SCR cannot be determined, a FILE_ERROR is returned from the CreateFilterFromResource() method. The SCM iterates through the SCR and, for each row or node in the SCR, it calls upon the Syntax Object to help construct a WHERE clause.

The Search Syntax Classes

Creating the Clauses

The Search Criteria Manager controls how the Search Criteria Resources are created and sent out to the client. However, in order to create the clauses of the SELECT statement, the SCM and the Syntax Object (SO) must work together when SCRs are sent back into the data source. The SCM is in charge of creating and iterating through the SCR, while the Syntax object is responsible for creating the clauses of the SELECT statement. Because of the differences in SQL languages in different database backends, the data sources can specify which Syntax object to use to produce their clauses. The Syntax object is a concrete class, which inherits from CAbstractSearchSyntax. I have provided three classes for you that inherit from this class. They are CVFPSearchSyntax, CSQLSearchSyntax and CoracleSearchSyntax, for use respectively with Visual FoxPro, Microsoft SQL-Server and Oracle databases. You can add additional search syntax classes for other databases or other criteria sets as needed.

We can have our business objects dictate what our search form will look like by sending XML directly to the client

The Syntax object is instantiated by the SCM and is called upon to generate the proper clauses for the database being used by the data source. The data source property cSyntaxClass holds the name of the class to use. In our example, we are using a VFP database, so the CVFPSearchSyntax object is instantiated. The Syntax Object then creates a Clauses object to hold all of the generated clauses. This makes it easier for us to access all the clauses of the SELECT statement via one object. In our example, the Syntax object creates WHERE and ORDER BY clauses and places them in the Clause Object's cWhere and cOrder properties. As the Syntax object creates the WHERE clause, it is also internally managing the ORDER BY clause by filling and sorting an aOrderClause[] array, which is converted to an ORDER BY clause after the WHERE clause is constructed. The example provided creates a WHERE clause with the search criteria being ANDed together. To create more complex queries using the OR operator, you can modify the SCR to hold the additional property (or create a hierarchical XML document) and then enhance the SCM to handle it properly.

The Syntax object is called by the SCM every time a search criteria value is encountered while parsing the SCR (XML stream or ADO recordset). It constructs just the piece of the where clause that the field is involved in.

Consider this clause in VFP:

WHERE "columbia" $ lower(custnote.note)

This is the same clause in SQL-Server:

WHERE custnote.note LIKE '%Columbia%'

The syntax is different because SQL-Server is not case sensitive by default like VFP. Because of differing syntax, the Syntax object's GetFilterSyntax() methods may have different or additional code for constructing and supporting database-specific WHERE clauses. After these strings are constructed, they are put into a Clauses Object and passed back to the GetDataSourceNameBySearchCriteria() method. Here the clauses are tacked onto the SQL SELECT statements before calling ExecuteSQLQuery() to perform the query on the database.

The CSearchClausesParameter Class

Storing the clauses in a Search Parameter Object

In order to more easily and flexibly handle all the generated clauses of the SQL-Select, a CSearchClausesParameter object (subclass of CAbstractParameter) is created to hold all of the generated clauses in properties. The protected oSearchClauses property of the Data source references the search clauses object after the call to CreateFilterFromResource(). This way, we can easily retrieve the clauses in the GetDataSourceNameBySearchCriteria() methods.

FUNCTION GetCustomerNoteBySearchCriteria(txSCR)

    LOCAL loADO, lcSelect
    
    loADO = THIS.GetADOAggregateParameter()
    loADO.oCommand.ActiveConnection = loADO.oConnection
    
    lcSelect = [ SELECT CustNote.*, ] + ;
               [ RTRIM(Customer.LastName)+', '] +;
               [ RTRIM(Customer.FirstName)] +;
               [ AS FullName ] + ;
               [ FROM CustNote ] + ;
               [ INNER JOIN Customer ON ] + ;
               [ CustNote.Customer_ID = ] + ;
               [ Customer.Customer_ID ]

    If This.CreateFilterFromResource(txSCR) = FILE_OK
    
        With this.oSearchClauses
        
            *-- Apply the filter to the select statement
            loADO.oCommand.CommandText = lcSelect + .cWhere + .cOrder
        
        EndWith
    
    Else
        loADO.oCommand.CommandText = lcSelect + [ WHERE 1 = 0 ]
    Endif
    
    This.oSearchClauses = Null
    This.ExecuteSQLQuery( loADO )
    RETURN loADO.oRecordSet

ENDFUNC

The Front-End ASP Client

Searching Using Disconnected ADO Recordsets

In the example, I have a search page called adosearch.htm that presents some of the search criteria data to the user. For simplicity, I have kept the search screen simple. The user enters filter criteria and the form is submitted back to an ASP page called List.asp on the server, which places the form data into an ADO recordset SCR and calls the middle tier to execute the query.

In the List.asp page we need to fill our search criteria resource with the user's search criteria by finding the appropriate rows in the recordset and setting the value column equal to the submitted form's value. This is done with the recordset's Find method. You will also notice that I am specifying an order by setting the Order column's value. As you can see, you could easily design a much more flexible search screen by letting the user specify beginning and ending values, as well as orders.

Dim obj, rs, oSCR

Set obj = Server.CreateObject("FlexSearch.CustomerNote")

Set oSCR = obj.getRSSearchCriteriaResource()

oSCR.Find "Name = 'lastname'",,1,1
oSCR("Value") = Request.Form("LastName")
oSCR("Order") = "1"

oSCR.Find "Name = 'subject'",,1,1
oSCR("Value") = Request.Form("subject")

oSCR.Find "Name = 'note'",,1,1
oSCR("Value") = Request.Form("note")

oSCR.Find "Name = 'time'",,1,1
oSCR("Order") = "2"
oSCR("Option") = "DESC"

Once we've filled the SCR, we pass it into the middle tier and get back a resource with our data in it.

Set rs = obj.getCustomerNoteBySearchCriteria(oSCR)

Set obj = Nothing
Set oSCR = Nothing

The variable rs now holds a reference to an ADO recordset containing our data, which we can iterate and display in an HTML table. I typed in “F” for the Customer Last Name, giving me all the notes that belong to a customers whose last name is like “F” (Figure 3). In an actual live web application, you probably would not want to bring down all the notes in the search. I would allow hyperlinks on the customer name, for example, and display the individual record and the note in a separate window. For this example, however, I wanted to show the notes so we could see that our search was working properly.

Figure 3 - Part of the ADO Search Results Page (List.asp)
Figure 3 - Part of the ADO Search Results Page (List.asp)

The Front-End XML Client

Searching Using XML in Internet Explorer 5

This next example is really fun. Instead of a simple static search form, we are going to send to the browser our XML search criteria resource for it to manipulate. That way, we can have the business objects in our middle tier dictate what our search form will look like. If we add a field to the data source, we do not have to do anything to the search form. An easy way to do this is by creating an HTML page for the IE browser with the XML SCR as a “data island.” In Internet Explorer 5, you can use the <XML> tag to create a data island inside your HTML. Data islands are XML data referenced or included in an HTML page. The XML data can be included within the HTML or it can be in an external file. In the example file xmlsearch.asp we generate an XML data island from the middle tier:

<XML ID="MyXMLDoc">
<%
    Dim obj, xmlSCR
    Set obj = Server.CreateObject("Flexsearch.CustomerNote")
    xmlSCR = obj.GetXMLSearchCriteriaResource()
    Set obj = Nothing
    Response.Write(xmlSCR)
%>
</XML>

This code places the XML SCR generated by our middle-tier data source between the <XML> tags in our generated HTML page. To bind the XML to a table and controls, we have to specify the DATASRC element of the <TABLE> tag and the DATAFLD elements of the HTML controls:

<TABLE DATASRC="#MyXMLDoc">
    <THEAD>
        <TH>Field</TH>
        <TH>Value</TH>
        <TH>Order</TH>
    </THEAD>
    <TR>
        <TD>
            <DIV DATAFLD="name"></DIV>
        </TD>
        <TD>
            <INPUT TYPE=text DATAFLD="value">
        </TD>
        <TD>
            <INPUT TYPE=text DATAFLD="order">
        </TD>
    </TR>
</TABLE>

This HTML block binds the XML tree in the data island called MyXMLDoc to a DIV tag and two HTML inputs. Now the user can manipulate the XML SCR directly in the browser. Neat!

Now comes the tricky part. When the user wants to submit the search we have a special method of posting XML back to the server. You'll notice that there are no <FORM> tags in the generated HTML page. We don't want to submit the form the normal way because we don't want the values of the HTML controls, but instead want the XML tree. Internet Explorer and the MSXML parser include an object called XMLHTTP that we will use to post XML back to an ASP page on the server. Using the XMLHTTP object, you can post the XML tree itself as an object. In the following script, I create an XMLHTTP object and use it to post the XML tree MyXMLDoc to an ASP page on the server called post.asp.

<SCRIPT language="VBScript">
    Sub submitInfo
        dim httpObj

        Set httpObj = CreateObject("Microsoft.XMLHTTP")

        httpObj.Open "POST", "post.asp", false
        httpObj.send MyXMLDoc.XMLDocument

        document.all("results").innerHTML = httpObj.responseText

        Set httpObj = Nothing

    End Sub
</SCRIPT>

Now that we have posted the XML Search Criteria Resource to the server, we need to instantiate our middle-tier object and pass it the XML SCR. You'll see that post.asp has less code than our previous ADO example, because we do not have to fill the SCR manually, but instead directly pass in the XML SCR. To get at the XML tree in the Request Object, I instantiate the XMLDOM object and use the load method to load it. I then can access the XML SCR by referring to oSCR.xml:

<%
    Dim rs, oSCR, obj
    Set oSCR = Server.CreateObject("Microsoft.XMLDOM")
    
    oSCR.async=false
    oSCR.load(Request)
    
    Set obj = Server.CreateObject("Flexsearch.CustomerNote")
    
    Set rs = obj.GetCustomerNoteBySearchCriteria(oSCR.xml)
    
    Set oSCR = Nothing
    Set obj = Nothing
%>

The variable rs now holds a reference to a disconnected ADO recordset containing our data, which we can iterate and display in an HTML table. To display the generated HTML in the browser, you will notice in the client script a line that sets the innerHTML of a span tag called results to the responseText of the XMLHTTP Object. This is how the data is transmitted back to the client. As you can see, you could use the XMLHTTP object to send updated XML as well as HTML back to the client. Figure 4 shows our XML search criteria screen and a part of the results underneath.

Figure 4 - The XML Search and Part of the Results Page (xmlsearch.asp)
Figure 4 - The XML Search and Part of the Results Page (xmlsearch.asp)

You'll notice that I searched for all notes whose subject field started with INT and also specified an order by customer last name and then by note time. The XML SCR contains more data that you can display and allow the user to manipulate, like end values and ordering options. You can even bind data to <SELECT> tags and other HTML controls. I just left that out for the sake of simplicity.

Conclusion

Providing flexible searching on your data is not always easy, and constructing a simple, yet flexible user interface can be even more difficult. If you take special care in designing a good object-oriented framework for your middle tier, you will have an easier time adapting to other technologies like XML or ADO+. Take a more detailed look at the source code for examples of common design patterns used to connect and retrieve the data, as well as in the searching mechanism I described. Any way you look at it, distributed applications are here to stay. It's up to developers like us to find new ways to provide features on the web that users took for granted on their desktop. I hope this article has shown you a few fun and efficient ways to create dynamic queries against your remote data without sacrificing user interface flexibility or ease of use.