Microsoft Corporation
October 1998
Summary: Discusses how the Microsoft® Visual J++™ version 6.0 Dynamic HTML class library can be used to design and deploy Web-based applications. (16 printed pages) Covers:
Wiring a Dynamic HTML Table to a Data Source
Displaying a Partial Recordset
Microsoft Visual J++ 6.0 supports the creation of the next generation of Web-based applications using Dynamic HTML (DHTML) classes. Dynamic HTML is the most reliable way to create cross-platform applications. Soon to be a W3C standard, DHTML is a uniform language for creating applications across browsers, operating systems, and hardware configurations. It provides for user interaction and data presentation in an easy-to-understand combination of HTML, script code, and a robust document object model (DOM).
Visual J++ 6.0 contains a rich set of classes that enable developers to generate DHTML code without having to learn a scripting language or the nuances of the DOM. The Dynamic HTML class library gives developers control over the Web page hosting the application, allowing for a richer Web client application. The Windows Foundation Classes (WFC) Dynamic HTML class library can be used to build enterprise client/server applications that adhere to Internet standards of HTTP and HTML. The same class library can be used to build richer client applications when used in conjunction with Microsoft Internet Explorer 4.0, Win32®, and ActiveX® controls.
Using the Dynamic HTML class library, developers can author DHTML pages using only the Java language. The resulting Java application can directly render DHTML on the fly. With the Visual J++ 6.0 Dynamic HTML library, developers have the ability to design and deploy truly integrated Web- and Windows-based applications that can be executed on multiple platforms.
The DhTable class can be used to display tabular data on a Web page. As with any DHTML application, there are two fundamental modes of operation: client-side and server-side. If your client machines run the Win32 platform, have Internet Explorer 4.01 SP1, have a version of the Microsoft Virtual Machine (VM) for Java that has the WFC run time, and have access to your database, you can run your DHTML data applications as client-side apps. Otherwise, if you can't make assumptions about your client machines or they don't have access to the database, you will need to run your application on the server and have the HTML that is generated sent to client machines. In the first part of this document, we will develop a client-side application that displays data from the "Pubs" database, an example database that ships with Microsoft SQL Server™ version 6.5. The section later in this article titled "Server-Side Data Tables" describes how to write the data table a server-side Web application.
To create a client-side data table Web application using DHTML, you first need to create a Code Behind HTML project. This project sets up a class, Class1 by default, which derives from DhDocument and an HTML page, Page1.htm. The HTML document represents a single Web page, and the Class1 object represents the Java object that "lives" behind the Web page and allows the Web page to be manipulated programmatically from Java code. The data table, as a DhTable object, will be added to the Class1 object, and this will cause it to be displayed on the Web page. To begin, we first need to set up a DataSource object that will represent the connection to a database. For example, you can create a DataSource object, set the query using the setCommandText method, and set the connection string using the setConnectionString method. The client machine will need to have access to this data source, perhaps as an Open Database Connectivity (ODBC) connection. The following code added to the initForm() method of your document class will set up a DataSource connected to a database:
DataSource ds = new DataSource();
ds.setConnectionString("dsn=myDSN;uid=myUID;pwd=myPWD");
ds.setCommandText("select * from authors");
The table is created and told what DataSource to use:
myTable = new DhTable();
myTable.setDataSource(ds);
When the table is added to the document, a call to DataSource.begin() is made to populate the table with records returned by the query:
this.add(myTable);
ds.begin();
You will also need to add these imports:
import com.ms.wfc.data.*;
import com.ms.wfc.data.ui.*;
Congratulations! You have created a DHTML data table. The table that is specified by the previous code is quite "bare bones"—it has no formatting and no header row indicating what is in each column, and all the data is displayed, even if the recordset is very large. The following class, simpleAddress.java, illustrates these concepts. Insert your own Data Source Name (DSN) information in the connection string.
import com.ms.wfc.html.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.data.*;
import com.ms.wfc.data.ui.*;
public class simpleAddress extends DhDocument
{
DhTable theTable;
DataSource dataSource1;
public simpleAddress()
{
theTable = new DhTable();
// set up the data source with connection string and query
String connection = "DSN=ISQL_w;DATABASE=pubs;UID=myUID;PWD=myPWD";
dataSource1 = new DataSource();
dataSource1.setConnectionString(connection);
dataSource1.setCommandText("Select * from authors");
// attach the datasource to the table
theTable.setDataSource(dataSource1);
// add the table to the document
this.add(theTable);
// activate the Data Source
try {
dataSource1.begin();
}
catch(Exception e)
{
add(new DhText("Exception in dataSource1.begin(): " +
e.toString()));
}
}
}
The table can be broken up into pages. To specify a page size, use the setPageSize function of the DhTable class. When using a table with pages, the functions showNextPage and showPreviousPage are used to move from one page to another. If you would rather specify a record range, use the setRecordRange function. The records are considered to be numbered from zero; setRecordRange(0, 10) will display ten records numbered 0 through 9. If you want to find out what records the table is displaying, use the functions getRangeStart() and getRangeEnd().
In the previous example, the table simply inherited the style of the document that contained it. Also, the table showed all the fields of the records in the order they were returned from the database. In most situations, you will want more control over what data is displayed and how it is displayed. If you want to specify the formatting for the cells of the table or specify what fields you want, you will need to create a repeater row. The repeater row is a DhRow object that is used as a template for all the rows of the table. Thus, one can set properties and styles on the repeater row and specify bindings of each cell of the repeater row with the desired field in the recordset. The following example illustrates this:
myTable = new DhTable();
myTable.setDataSource(ds);
DhRow repRow = new DhRow();
// set Properties for the entire body of the table
repRow.setBackColor(Color.GRAY);
for (int i = 0; i < numColumns; i++)
{
repRow.add( new DhCell() );
// set Properties for a specific column.
if (i == 0)
{
repRow.getCell(i).setBackColor(Color.PURPLE);
}
}
myTable.setRepeaterRow( repRow );
myTable.setDataBindings( new DataBinding[] {
new DataBinding(repRow.getCell(0),"Text", "Field1"),
new DataBinding(repRow.getCell(1),"Text", "Field2"),
new DataBinding(repRow.getCell(2),"Text", "Field3")});
In the previous example, "Field1," "Field2," and so on should be replaced by the names of your fields. If the field names are not found in the database, your table will be empty.
Because the repeater row is a template for all rows in the table, setting the properties of the row affects the entire body of the table, and setting the properties of a cell in the repeater row affects that entire column.
If a repeater row is not specified, a default repeater row is created with data bindings set to all the fields, in order of appearance in the recordset. Thus, if you use your own repeater row, you don't get the default data bindings, and you have to set them in your code, as just shown. If you don't do this, the data table will be empty. Specifying your own databindings allows you to pick and choose the columns you want in the order you want them shown.
You can also specify a style to be associated with the repeater row or any of its columns. Because the DhCell elements that determine the style of the columns are children of the DhRow elements, setting a style on a cell will override the setting for the row. The following example illustrates this:
myTable = new DhTable();
myTable.setDataSource(ds);
DhRow repRow = new DhRow();
// set style for the entire body of the table
DhStyle rowStyle = new DhStyle();
rowStyle.setBackColor(Color.GRAY);
repRow.setStyle(rowStyle);
for (int i = 0; i < numColumns; i++)
{
repRow.add( new DhCell() );
// set style for a specific column.
if (i == 0)
{
DhStyle column0_Style = new DhStyle();
column0_Style.setBackColor(Color.PURPLE);
repRow.getCell(i).setStyle(column0_Style);
}
}
myTable.setRepeaterRow( repRow );
myTable.setDataBindings( new DataBinding[] {
new DataBinding(repRow.getCell(0),"Text", "Field1"),
new DataBinding(repRow.getCell(1),"Text", "Field2"),
new DataBinding(repRow.getCell(2),"Text", "Field3")});
The cell and the table have borders, but the rows do not. To set the style of the border, use the setBorderStyle method on either the cell or the entire table. There are several border styles encoded by the enum class DhBorderStyle spanning the range of border types supported by HTML. The setBorderColor method is used to give the colors of the various edges of the border. Use the DhBorders enum to specify the top, bottom, left, or right sides.
DhStyle style1 = new DhStyle();
style1.setBorderWidth(3, DhUnits.PIXEL);
style1.setBorderStyle(DhBorderStyle.OUTSET);
style1.setBorderColor(DhBorders.LEFT | DhBorders.TOP,
Color.LIGHTGRAY);
style1.setBorderColor(DhBorders.RIGHT | DhBorders.BOTTOM,
Color.CONTROLDARK);
If you want a particular style to apply to some rows but not others, you will need to do this after the table is populated with data and added to the document. In this case, it will not do to set the style of the repeater row, because that applies to all rows. You will need to get the DhRow objects for the rows that were actually created in the live table and change the properties of that object. The following example shows the use of the getBodyRows() and getBodyRowCount() functions to modify the table after it has been added to the document. Unlike our other examples, this code will not function correctly when called from the constructor or from initForm(). It will only work after the HTML elements for the table have been generated; for example, in the onDocumentLoad event handler. In this example, every other row is highlighted in yellow:
this.add(myTable);
DhRow bodyRows[] = myTable.getBodyRows();
for (int i = 0; i < myTable.getBodyRowCount(); i++)
{
if (i % 2 == 0)
{
bodyRows[i].setBackColor(Color.YELLOW);
}
}
If you need to access the data in the tables, you can either look at the recordset directly or access the text in the table using the getText function. Due to a known problem in the Visual J++ 6.0 release, it may be necessary to use the following functions to get and set the text of a cell:
public String getCellText(int row, int col)
{
DhCell cell = myTable.getBodyCell(row, col);
cell.getElementCount();
DhElement[] elems = cell.getElements();
String text = cell.getText();
cell.removeAll();
((com.ms.wfc.html.om.IHTMLElement) cell.getPeer()).setInnerHTML("");
for( int i = 0; i < elems.length; i++ ){
cell.add( elems[i] );
}
cell.setText( text );
return text;
}
public void setCellText(int row, int col, String s)
{
DhCell cell = myTable.getBodyCell(row, col);
cell.getElementCount();
DhElement[] elems = cell.getElements();
String text = cell.getText();
cell.removeAll();
(( com.ms.wfc.html.om.IHTMLElement) cell.getPeer()).setInnerHTML("");
for( int i = 0; i < elems.length; i++ ){
cell.add( elems[i] );
}
cell.setText( s );
}
The previous code can also be modified to add elements to cells.
Note If your data contains HTML tags, they will be interpreted, so you can use this fact to display active links, embedded images, and other HTML elements in the cells of your table.
If you want to have a default header row created for you, you can set the AutoHeader property on the DhTable object, which will create a header row with the field names for each column:
myTable.setAutoHeader(true);
If you want to specify your own unique header or footer row, you can add a header or footer row to the table by creating a DhRow object, adding cells, setting the properties and text of the cells, and calling setHeaderRow or setFooterRow:
DhRow headerRow = new DhRow();
for (int i = 0; i < numColumns; i++)
{
headerRow.add(new DhCell());
}
headerRow.getCell(0).setText("Column1");
headerRow.getCell(1).setText("Column2");
// etc.
myTable.setHeaderRow( headerRow );
You can make your data table more interactive by adding events to it. The following code (added before the call to setRepeaterRow) adds event handlers to the repeater row:
repeaterRow.addOnMouseEnter(new MouseEventHandler(
this.Table_MouseEnter ));
repeaterRow.addOnMouseLeave(new MouseEventHandler(
this.Table_MouseLeave));
Event handlers added to the repeater row are automatically copied to all body rows of the table. The previous code, when the following functions are added to the class, supports hot tracking, so when the mouse hovers over a row, it is highlighted:
DhElement currElement = null;
public void Table_MouseEnter( Object sender, MouseEvent event)
{
currElement = (DhElement) sender;
currElement.setBackColor(Color.RED);
}
public void Table_MouseLeave( Object sender, MouseEvent event)
{
currElement = (DhElement) sender;
currElement.resetBackColor();
}
You can hook up event handlers to individual columns by setting the event handler of a cell in the repeater row, and you can hook up event handlers to individual cells after the table is added to the document. You can, of course, also hook event handlers up to header and footer rows, specific cells in these rows, or to elements (such as DhButton) that are added to cells.
If you cannot make assumptions about the client machines or the client machines don't have direct access to the data source, you will need to have your DHTML application run on the server. When this is done, the database is accessed only from the server, rather than from each client. On the server, the running Java code (actually, the constructor and initForm() method of your DhDocument derived class) generates an HTML page and sends this to the client, where it is displayed as a normal HTML page with the client's browser. Once the HTML code is sent to the client, the Java program has no more control over it or communication with it. You can still take advantage of most of the functionality of the DhTable class; however, you cannot use events, and you cannot look at the cells of the table because the elements do not actually exist on the server where the Java code is running. If you do require interaction with the user, you will need to set up a DhForm with a DhSubmitButton, as described later.
Instead of an HTML page, the page will be loaded as an Active Server Pages (ASP) page. The ASP page needs to set up the DhModule with the Java class and the HTML template to use, if any. The following is a sample ASP page that does this:
<html>
<head>
<title></title>
</head>
<body>
<h1>ClassName </h1>
<%
Set Module = Server.CreateObject("DhModule")
Module.setCodeClass("ClassName")
Module.setHTMLDocument("")
%>
</body>
</html>
On the server, one will need to set up a "System DSN" for the database using the ODBC Manager on Windows Control Panel. Also, the class file(s) need to be on the classpath—for example, in C:\WINNT\Java\classes. The output directory for class files can be set in the Project Properties dialog box under Compile.
Apart from events and user interaction, the same code for data tables can be used on the client as on the server. For server-side Web applications, communication with the user is achieved via the submission of HTML forms.
The ServerDataBinding example included in the Visual J++ samples can be used as a starting point. In this example, there is limited user interaction via a form on the HTML page in which an SQL query can be entered by the user. A table is displayed with the results of the query. To better understand the server-side programming model, it is worthwhile to examine the sequence of events as this application is executed.
There are three files in the project: the ASP file, the HTML file, and the Java class. The ASP file is the entry point—the user on the client machine navigates to this ASP file using their browser. On the server, the ASP code sets up a module (a DhModule object) and sets the HTML document that will be used as the template. The Java class is loaded (assuming it is correctly on the classpath) and begins execution at the constructor, binding Java objects to the elements in the HTML template. The first time the user accesses the link, the default query is used to generate the table. This is turned into HTML that is sent back to the client machine and displayed on the user's browser. The user may then enter another query and select Submit Query. This causes the query to be sent back to the server as part of the HTTP request in the usual way for a form with a Submit button. At this point, an entirely new execution of the Java class is begun, starting from the constructor. The only difference is that when the query is examined, it may have changed from the default query. A new table is constructed with a new query, turned into raw HTML, and sent back to the client's browser. Whenever the user changes the query and selects Submit Query, this whole process repeats with an entirely new Java object.
How does the Java code know what information was returned by the submitted form? The HTML code contains an INPUT tag with the name attribute queryString. Whatever is in the text box is therefore submitted as part of the HTTP request. The DhModule object provides a way to access this information. The queryString parameter was accessed from Java code as follows:
String query = DhModule.getCurrentModule().getQueryParameter(
"queryString" );
In a similar way, one can add buttons for moving to the next and previous pages of a multipage table. Each of these buttons must be a Submit button, because whenever they are pressed on the client side, a form will need to be submitted containing the information about what button was pressed, along with any other information required from the client, such as what page of a multipage table they were looking at. None of this information can be saved in the Java code because a newly constructed class is executed each time a button is pressed. The solution to this is to have a hidden INPUT tag in the HTML that contains the data you want to preserve. This can be added to the HTML form from your Java code by adding a DhRawHTML object. For example, the following code could be used to preserve and retrieve the pageSize of a table:
int pageSize = 0;
// Get the OLD pageSize as a String from the http request.
String pageSizeString = DhModule.getCurrentModule().
getQueryParameter( "pageSize" );
// attempt to convert to an integer
try {
pageSize = Integer.parseInt( pageSizeString );
}
catch(NumberFormatException except)
{
messageBox.show("The pageSize parameter could not be read.");
pageSize = 10;
}
// Code here may change the pageSize; for example, if a submit
// button for changing the pageSize was pressed.
// add the NEW pageSize to the HTML (to be sent to the client) so
// that it is submitted as a hidden parameter in the next http
// request.
DhForm form1 = new DhForm();
DhRawHTML pageSizeRawHTML = new DhRawHTML(
"<input type=hidden name=pageSize value=" + pageSize + ">");
form1.add( pageSizeRawHTML );
A complete example is included here of a server-side DhTable with "Next Page" and "Previous Page" functionality implemented using Submit buttons. The current record is saved using the method just described. Notice that because the table is created anew each time a button is pressed, showNextPage() and showPreviousPage() functions on DhTable will not be useful. The current record is determined from the HTTP request, and setRecordRange() is used to set the current page.
The file SampleServerSide.java:
import com.ms.wfc.html.*;
import com.ms.wfc.core.*;
import com.ms.wfc.ui.*;
import com.ms.wfc.data.*;
import com.ms.wfc.data.ui.*;
/**
* This class demonstrates simple server-side data binding
* using a DhTable class and the sample Northwind database
*
*/
public class SampleServerSide extends DhDocument {
DhForm tableArea;
DhEdit queryEdit;
boolean prevPageHit = false;
boolean nextPageHit = false;
int pageSize = 10;
int recordNum = 0;
/**
* The constructor, which just calls initForm
*/
public SampleServerSide(){
initForm();
}
/**
* initForm is where you should do all your setup
* of the elements in the HTML template that you wish to bind to.
*/
protected void initForm() {
tableArea = new DhForm();
// retrieve the queryString parameter
String query = DhModule.getCurrentModule().
getQueryParameter( "queryString" );
// default to "SELECT * FROM Products"
if ( query == null || query.equals( "" ) ){
query = "SELECT * FROM Products";
}
// retrieve the current record number
String recordNumString = DhModule.getCurrentModule().
getQueryParameter( "recordNum" );
if (recordNumString != null)
{
try {
recordNum = Integer.parseInt( recordNumString );
}
catch( NumberFormatException except)
{
recordNum = 0;
}
}
// determine whether Next Page or Prev Page was pressed.
String previous = DhModule.getCurrentModule().
getQueryParameter( "prevPage");
if (! previous.equals(""))
{
// Previous Page was hit.
prevPageHit = true;
recordNum -= pageSize;
if (recordNum < 0)
{
recordNum = 0;
}
}
String next = DhModule.getCurrentModule().
getQueryParameter( "nextPage");
if (! next.equals(""))
{
// Next Page was hit.
nextPageHit = true;
recordNum += pageSize;
}
tableArea.add( createDataTable( query ) );
// setup the DhEdit
queryEdit = new DhEdit();
queryEdit.setText( query );
queryEdit.setSize(400, 25);
queryEdit.setSubmitName("queryString");
// set up the form with 3 submit buttons
DhForm form1 = new DhForm();
DhSubmitButton queryButton = new DhSubmitButton("Submit Query");
queryButton.setName("submitQuery");
DhSubmitButton nextPageButton = new DhSubmitButton("Next Page");
nextPageButton.setName("nextPage");
DhSubmitButton prevPageButton = new DhSubmitButton("Prev Page");
prevPageButton.setName("prevPage");
form1.add(queryEdit);
form1.newLine();
form1.add(queryButton);
form1.add(prevPageButton);
form1.add(nextPageButton);
form1.add(new DhRawHTML(
"<input type=hidden name=recordNum value=" +
recordNum + ">"));
this.add(form1);
this.add(new DhHorizontalRule());
this.add(tableArea);
}
/**
* This function creates a table and initializes it to be populated
* with data from the specified SQL query
* @param query The SQL statement to retrieve data to populate table
* @return A DhTable initialized with the data from the <i>query</i>
*/
private DhTable createDataTable(String query){
// create and format the table
DhTable table = new DhTable();
table.setBorder( 1 );
table.setAutoHeader( true );
table.setBackColor( Color.CONTROL );
table.setForeColor( Color.BLACK );
table.setPageSize(pageSize);
Recordset rs = null;
String error = null;
try{
// create a DataSource object, and set it up
DataSource ds = new DataSource();
ds.setConnectionString("DSN=Northwind;");
ds.setCommandText( query );
// cause the DataSource to generate a recordset
rs = ds.getRecordset();
}catch( Exception ex ){
error = ex.getMessage();
}
// if there is an error, or we have an empty recordset, display the
// appropriate message
if ( rs == null || rs.getEOF() ){
if ( error != null ){
error =
"The query produced the following error message:<BR><b>" +
error + "</b>";
}else{
error =
"The query produced no records. Please try another.";
}
DhRow r = new DhRow();
DhCell c = new DhCell( error );
c.setForeColor( Color.RED );
r.add( c );
table.setAutoHeader( false );
table.setBorder( 0 );
table.resetBackColor();
table.setFont( Font.ANSI_FIXED );
table.add( r );
}else{
// set the Recordset as the data source for the table
table.setDataSource( rs );
// make sure we don't go beyond the end of the Recordset
int recordCount = rs.getRecordCount();
if (recordNum >= recordCount)
{
recordNum = recordCount - pageSize;
}
table.setRecordRange(recordNum, recordNum + pageSize );
}
return table;
}
}
The following ASP file is used to load the previous class:
<HTML>
<HEAD>
<META NAME="GENERATOR" Content="Microsoft Visual Studio 98">
<META HTTP-EQUIV="Content-Type" content="text/html">
<TITLE>Document Title</TITLE>
</HEAD>
<BODY bgcolor=tan>
<%
ClassName = "SampleServerSide"
Set Module = Server.CreateObject( "SampleServerSide.Module1" )
Module.setCodeClass( ClassName )
Module.setHTMLDocument( "" )
%>
</BODY>
</HTML>
This example also uses a Module1.java:
import com.ms.wfc.html.*;
/**
* @com.register ( clsid=8D99D0B9-40FC-11D2-828B-006097ABFC58,
typelib=8D99D0BA-40FC-11D2-828B-006097ABFC58 )
*/
public class Module1 extends DhModule
{
}