HTML and XML have made the Internet what it is today, but both technologies are not necessarily tied to the Internet.
Quite the contrary! Using HTML in regular Windows applications has always been a great alternative. Paired with XML and XSL, this technique is more powerful than ever, since there are a growing number of XML sources, such as SQL Server, Web Services, and XML-enabled Business Objects.
Very often, HTML is criticized as a weak interface technology. Many claim that it isn't powerful enough for complex and feature-rich interfaces. While this may be true when HTML is used to imitate Windows interfaces, HTML has a number of advantages over conventional Windows forms. For example, let's consider complex lists. Have you ever tried listing a customer with all orders and order details in a grid? If so, you may have used a hierarchical grid, such as the Microsoft FlexGrid, or one of the powerful third party grid controls. However, even with those controls, creating rows with flexible row heights or different numbers of columns for each row is difficult. Now, add the need for flexible controls in each row and you face an almost unachievable goal. With HTML, however, this is trivial.
The basic idea is simple: Retrieve data from some kind of data source or business object, turn it into a string, and then display it in an HTML control on a regular Windows form. A more up-to-date variation on the theme retrieves the data in XML. A Business Object, a Web Service, or a database such as SQL Server provides the XML. But, there is no need to create the HTML manually, since a simple XSL transformation does the job. Voila!
Retrieving the XML
That's the theory, so let's see how we can implement it. First, we need the XML. Let's use customer information including customer addresses, names and contacts, as well as orders, line items and payment status. Here's a portion of the XML string:
<CUSTOMERS> <CUSTOMER> <CUSTOMERID>ALFKI</CUSTOMERID> <COMPANY>Alfreds Futterkiste</COMPANY> <CONTACT>Maria Anders</CONTACT> <ADDRESS>Obere Strasse 57</ADDRESS> <CITY>Berlin</CITY> <COUNTRY>Germany</COUNTRY> <ORDERS> <ORDER> <ORDERID>2000JAN325</ORDERID> <TOTAL>958.85</TOTAL> <PAID>Yes</PAID> <DATE>01-14-2000</DATE> <LINEITEMS> <ITEM> <ITEMID>SAL</ITEMID> <DESC>Alaska Salmon</DESC> <QTY>100</QTY> <UNIT>Lbs</UNIT> <PRICE>8.99</PRICE> </ITEM> <ITEM> <ITEMID>SCHNI</ITEMID> <DESC>Wiener Schnitzel</DESC> <QTY>15</QTY> <UNIT>Lbs</UNIT> <PRICE>3.99</PRICE> </ITEM> </LINEITEMS> </ORDER> <ORDER> <ORDERID>2000JAN678</ORDERID> <TOTAL>2889.75</TOTAL> <PAID>No</PAID> <DATE>01-23-2000</DATE> <LINEITEMS> <ITEM> <ITEMID>BEER</ITEMID> <DESC>Original Microbrew</DESC> <QTY>500</QTY> <UNIT>Bottles</UNIT> <PRICE>1</PRICE> </ITEM> <ITEM> <ITEMID>WHISKEY</ITEMID> <DESC>Finest Scotch</DESC> <QTY>10</QTY> <UNIT>Bottles</UNIT> <PRICE>199</PRICE> </ITEM> <ITEM> <ITEMID>EBEER</ITEMID> <DESC>Dark Ale</DESC> <QTY>5</QTY> <UNIT>Kegs</UNIT> <PRICE>39.95</PRICE> <ACTION>Recall</ACTION> </ITEM> <ITEM> <ITEMID>WATER</ITEMID> <DESC>Bottled Mineral Water</DESC> <QTY>1000</QTY> <UNIT>Bottles</UNIT> <PRICE>.20</PRICE> </ITEM> </LINEITEMS> </ORDER> </ORDERS> </CUSTOMER> </CUSTOMERS>
I'm sure you recognize some of the data, since I used Microsoft's standard customer data and added fictitious order information.
To convert this information into HTML using XSL stylesheets, we need to load it into a "transformer". In a COM-based system, we can use Microsoft.XMLDOM. As mentioned above, we could retrieve this information from a multitude of sources and our scenario would still work. As Travis Vandersypen demonstrated in the Fall 2000 and in this issue of CoDe Magazine, XML-formatted data can be easily obtained from SQL Server 2000. Assuming that we have a server named www.eps-software.com with a SQL Server template named CustOrd.XML, we can load the information as follows. First, the Visual Basic version:
DIM oXML as Object SET oXML = CreateObject("Microsoft.XMLDOM") oXML.Load("Http://www.eps-software.com/sqltemp/CustOrd.xml")
The Visual FoxPro version is very similar:
LOCAL oXML oXML = CreateObject("Microsoft.XMLDOM") oXML.Load("Http://www.eps-software.com/sqltemp/CustOrd.xml")
In this scenario, the XML is dynamically created by SQL Server 2000 and we load it across the Internet (for more information, see Travis Vandersypen's articles). Alternatively, we could retrieve the XML from a business object on the current computer or local network. Here is some code that assumes the business object is a COM Object named Customers.Orders. In Visual Basic:
DIM oBiz AS Object DIM oXML AS Object SET oBiz = CreateObject("Customers.Orders") SET oXML = CreateObject("Microsoft.XMLDOM") oXML.LoadXML(oBiz.GetOrders("ALFKI"))
Like the first example, the differences in the Visual FoxPro version are minimal:
LOCAL oBiz, oXML oBiz = CreateObject("Customers.Orders") oXML = CreateObject("Microsoft.XMLDOM") oXML.LoadXML(oBiz.GetOrders("ALFKI"))
In this example, the Internet is not involved at all. However, we could call business objects across the Internet, in which case we would call them "Web Services" (see Rick Strahl's article in the current issue for more information).
Either way, we end up with an XMLDOM object holding our XML string.
Rendering and Displaying the HTML
Let's now create a stylesheet that will transform the XML into HTML, so we can display it to the user. Here is an XSL Stylesheet:
<?xml version="1.0"?> <xsl:stylesheet xmlns:xsl="uri:xsl"> <xsl:template match="/"> <html><body link="black" vlink="black"> <table width="100%"> <xsl:for-each select="CUSTOMERS/CUSTOMER"> <tr height="15"><td> </td></tr> <tr><td bgcolor="#DDDDDD"> <a> <xsl:attribute name="href"> vssc://customer/ <xsl:value-of select="CUSTOMERID"/> </xsl:attribute> <b><xsl:value-of select="COMPANY"/></b></a> <i>(Contact: <xsl:value-of select="CONTACT"/>)</i><br/> <font color="blue"><i> <xsl:value-of select="ADDRESS"/>, <xsl:value-of select="CITY"/>, <xsl:value-of select="COUNTRY"/><br/> </i></font> </td></tr> <tr><td> <xsl:for-each select="ORDERS/ORDER"> <table width="100%"> <tr><td width="25"> </td> <td bgcolor="moccasin" colspan="3"> <a> <xsl:attribute name="href"> vssc://order/ <xsl:value-of select="ORDERID"/> </xsl:attribute> <b>Order from: <xsl:value-of select="DATE"/> </b></a><br/> <small><i> (Id: <xsl:value-of select="ORDERID"/>) </i></small> </td></tr> <xsl:for-each select="LINEITEMS/ITEM"> <tr><td width="25"> </td> <td width="80"> <xsl:value-of select="QTY"/> </td> <td width="80"> <xsl:value-of select="UNIT"/> </td> <td width="*"> <a><xsl:attribute name="href"> vssc://item/ <xsl:value-of select="ITEMID"/> </xsl:attribute> <xsl:value-of select="DESC"/> </a> </td></tr> </xsl:for-each> <tr> <td align="right" colspan="4"> <b>Order Total: <xsl:value-of select="TOTAL"/></b> </td> </tr> <xsl:if test=".[PAID='No']"> <tr> <td align="center" colspan="4"> <font color="red"><b> This order has not been paid!<br/> <a><xsl:attribute name="href"> vssc://reminder/ <xsl:value-of select="../../CUSTOMERID"/> </xsl:attribute> Click here</a> to send a reminder. </b></font> </td> </tr> </xsl:if> </table> </xsl:for-each> </td></tr> </xsl:for-each> </table> </body></html> </xsl:template> </xsl:stylesheet>
The stylesheet is relatively simple. When combined with our XML data, it will iterate over all the customers to display their names and addresses. For each customer, the stylesheet will iterate over all the matching orders, and for each order it will iterate over all the line items. There is also some additional logic to see if an order has been paid. If not, some additional text will be displayed at the bottom of the order to allow the user to send a reminder.
As you can see (Figure 1), the display defined by the stylesheet is relatively simple (in structure as well as in artistic value). More complex stylesheets are possible, but I chose this one to keep our example easily understandable.
To apply the stylesheet (perform the transformation to create the form shown in Figure 1), we load it into a second instance of the XMLDOM and use the TransformNode() method of the first object to render the HTML. Again, the differences between the Visual FoxPro version and the Visual
Basic version are few. Here is the Visual Basic code:
DIM oXSL AS Object DIM cHTML AS String SET oXSL = CreateObject("Microsoft.XMLDOM") oXSL.Load("C:\StyleSheets\CustOrd.xsl") cHTML = oXML.TransformNode(oXSL.DocumentElement)
And the Visual FoxPro version:
LOCAL oXSL, cHTML oXSL = CreateObject("Microsoft.XMLDOM") oXSL.Load("C:\StyleSheets\CustOrd.xsl") cHTML = oXML.TransformNode(oXSL.DocumentElement)
The transformation is straightforward. We load the stylesheet into its own instance of the XMLDOM parser. All XSL stylesheets are XML-based and can therefore be treated like any other stylesheet. In this example, I assume that the stylesheet is stored on the local hard drive. However, we could store the stylesheet on a server if we wanted to be able to update it more easily.
The TransformNode() method performs the actual transformation. There is nothing special for us to do. We use the oXML object created earlier, and call its TransformNode() method. To specify what the transformation is to be based upon, we pass in our oXSL object (or its DocumentElement, which is the root XML node). The method returns the result of the transformation, which will be HTML, since this is what we created in our stylesheet. (We could have produced non-HTML output if we wanted, but this is a different subject altogether).
Now, we need a way to display the HTML. We can accomplish this using Internet Explorer or Microsoft's Web Browser Control (which, in fact, is Internet Explorer). At this point, the Visual Basic and Visual FoxPro versions differ a little more. In Visual Basic, we create a form and drop the Web Browser control onto it. However, before we can do this, we have to add the "Microsoft Internet Controls" to our toolbox. Once we do this, the Web Browser control becomes available. For my example, I named the instance on the form "oHTML".
Before we can manipulate the content of the HTML control, we have to load a blank document to make sure the control is properly initialized. Since we don't need any real content, we simply navigate to "about:blank". We can do this in the Form_Load() event:
Private Sub Form_Load() oHTML.Navigate2 ("about:blank") End Sub
Once the blank document is loaded, we can render our HTML. The earliest we can do this is when the DocumentComplete() event fires, but depending on the business logic, this might need to happen in a different place. Reusing the code we developed above to create cHTML, here is the VB code to render it:
Private Sub oHTML_DocumentComplete _ (ByVal pDisp As Object, URL As Variant) ' Other code goes here to create cHTML oHTML.Document.Open oHTML.Document.Write (cHTML) oHTML.Document.Close End Sub
In Visual FoxPro, some of the steps are slightly different. First, we create a new form and drop an OLEControl from our toolbox. VFP now asks what control we want to add, and we select "Microsoft Web Browser" from the list. Now, we need to solve a problem that only occurs when we use the control in VFP: The Web Browser tries to refresh itself before the control is actually displayed. To fix this, we can simply decide not to inherit that behavior from the Web Browser. To do so, we add the following code to the browser control's Refresh() event:
As in the Visual Basic version, we navigate to "about:blank" to generate a blank document. We do this in the form's Init() event:
Now, we are ready to display the HTML in the DocumentComplete() event:
LPARAMETERS pdisp, url * Other code goes here to create cHTML THIS.Document.Open THIS.Document.Write(cHTML) THIS.Document.Close
Figure 1 shows the result of both (Visual Basic and Visual FoxPro) versions.
Reacting to Events
We have now produced a very flexible interface that would have been next to impossible with conventional grid controls. However, the best interface doesn't do us a lot of good unless we can interact with it. There are a number of ways to interact with an HTML interface. I like to use simple navigation mechanisms, but rather than navigating to a different page, we can use one of the events the control fires to intercept the navigation and do things within our own application. The sample stylesheet displays the customer name as a link, with a URL that is somewhat different from a regular URL. Here is one of them:
One of the first obvious differences from other URLs is the protocol. Rather than using http, I called mine "vssc", which stands for "Visual Studio Script". I could have chosen any other name as well. The rest of the URL is an instruction to our application, telling it that this is about the customer "ALFKI". The idea is that the application can decide what to do with that information, such as starting a data entry form.
The key to making this work is the BeforeNavigate2() event. Internet Explorer passes several parameters to that event, but the two we are interested in are the URL and a parameter named "Cancel". This parameter is passed by reference, so we can set it to TRUE (.T.) to cancel the navigation. This allows us to look for a URL that starts with "vssc://", cancel the navigation if we find one, analyze the URL further, and react to the navigation operation with our own code. Here's the Visual FoxPro version:
*** ActiveX Control Event *** LPARAMETERS pdisp, url, flags, targetframename, postdata, headers, cancel IF url = "vssc://" cancel = .T. LOCAL lcForm, lcID lcForm = SUBSTR(url,At("/",url,2)+1) lcForm = Left(lcForm,At("/",lcForm)-1) lcID = SUBSTR(url,At("/",url,3)+1) DO FORM (lcForm) WITH lcID ENDIF
This code simply looks for "vssc://" in the URL. If encountered, the Web Browser navigation operation is canceled, and the name of the form we want to launch is retrieved from the URL along with the ID desired (such as the customer ID). We then start the form and pass the ID as the parameter. The form is a simple data entry form that might be found in any Visual FoxPro application.
The Visual Basic version is slightly different:
Private Sub oHTML_BeforeNavigate2 _ (ByVal pDisp As Object, URL As Variant, Flags As Variant, _ TargetFrameName As Variant, PostData As Variant, _ Headers As Variant, Cancel As Boolean) If Left(URL, 7) = "vssc://" Then Cancel = True Dim cForm As String Dim cID As String cForm = Mid(URL, 8, Len(URL) - 7) cForm = Left(cForm, InStr(1, cForm, "/") - 1) cID = Mid(URL, 9+Len(cForm), Len(URL)-8-Len(cForm)) If cForm = "customer" Then Customer.Show Customer.Load (cID) End If End If End Sub
As you can see, both examples use forms that are not part of this article. I'm sure you can come up with those on your own.
Technologies described in this article, such as XML and HTML, are valuable even if you never intend to create an Internet application. It is commonly believed that HTML has the disadvantage in interface issues over conventional Windows interfaces. This is not entirely correct. Although it is often difficult to create HTML interfaces that are as feature-rich as Windows interfaces, it is equally difficult to create Windows interfaces that are as flexible as HTML interfaces.