Export (0) Print
Expand All

Revisiting the Virtual List

As of December 2011, this topic has been archived. As a result, it is no longer actively maintained. For more information, see Archived Content. For information, recommendations, and guidance regarding the current version of Internet Explorer, see Internet Explorer Developer Center.

Michael Wallent
Lead Program Manager, DHTML
Microsoft Corporation

September 6, 1999

Contents

Squishing the Bugs
A Truly Virtual Experience
Bonus Behavior
Building Applications and Real Code

Back in January, I wrote about a virtual list using a combination of new DOM support and data binding. Over the past month, I've been working on a new application that actually took advantage of it.

Well, lo and behold, the previous virtual list example had a couple of bugs. This month, I'll review the changes required to fix the bugs and, as penance, I'll also revisit the initial code and make some pretty big changes. First, let's take a look at the bugs.

Squishing the Bugs

The first bug was a classic boundary condition problem. If you had less than a single page of data, you would get an immediate script error as the page loaded. To fix this, I added an initial condition check—doLoad—in the initialization method.

function doLoad() {
  // test to see if the dataset is smaller than the chunk size
  if (LargeData.recordset.recordcount <= SourceTable.dataPageSize) {
    copyRows();
  } else {

    // figure out total size of virtual list, to get spacers right
    TotalRecordCount = LargeData.recordset.recordcount - 10;

    // insert spacers into the vtable
    insertScrollBlocks();

    // hook scrolling and data events
   ScrollContainer.onscroll = testForScroll;
   SourceTable.onreadystatechange = dataAvailable;

    // initialize the first set of data
    testForScroll();
  }
}

If the data fits in a single page, it copies all the data into the target table. From then on, the list acts just like a regular data bound list.

The second bug was a little peskier. It turns out that before the first chunk of data got copied over, the getMoreData() method would increment the cursor position in the data—which would cause the first chunk of data to be skipped.

To fix this, the getMoreData() method was modified as follows:

function getMoreData() {
  if (SourceTable.readyState != "complete")
    return;

  // if all the data in the SourceTable hasn't been copied,
  // then don't scroll
  r = SourceTable.rows;
  lastRow = r[r.length-1];

  if (lastRow.recordNumber > LastRecordNumber) {
    copyRows();
  }

  SourceTable.nextPage();
}

The LastRecordNumber global, as its name implies, holds the record number of the last row to be copied. If, while trying to get more data, there is actually data in the table that still needs to copy, the method short circuits, and calls copyRows() to move the data across.

Those were the only bugs I knew about; however, working on this code again really brought up a key deficiency in the initial design. The way the scroll bar was managed was just hideous. I had created a placeholder in the scrolling region for each row that could ever exist. At startup time, each of those placeholders had to get inserted. As rows were inserted, placeholders were removed. That was a hack, and it was slow. While reviewing the code, one of the developers on the Internet Explorer team suggested that instead of having many placeholders, why not have one big placeholder that would change size as more rows were added. (Steve, for that super suggestion, you get the first ever "Dude of the Month" award.) This improved the performance significantly.

See the new and improved virtual table.

A Truly Virtual Experience

Even though this version was better, it still wasn't really virtual. If the user scrolled to the very bottom of the list, all the elements in the table needed to load—and that was pretty lame, performance-wise. What I really wanted was a virtual list that demand-loaded only the elements that needed to be in view. In addition, it was not very easy to "virtualize" a data-bound table. I wanted to be able to make any data-bound table a virtual table, without adding any script code to the original page. It was clear that the right way to implement the virtual table was through a DHTML behavior.

First, I tried to make the table truly virtual. In the initial version, the demand loading was based on capturing the onscroll event for the container, and then testing to see whether the last row was close to in view. If it was, another chunk was loaded. This process would be repeated multiple times if the last row were so far out of the view port that multiple data chunks had to be loaded.

With the "sparse matrix" virtual list, I could no longer depend on testing for the position of a "last" row—as there was no last row; every other chunk could theoretically be filled in. To solve this problem, I created an array. When a row is loaded, its corresponding array location is marked. Now, when the onscroll event occurs, I look to the array to see whether the rows that should be in view are actually loaded yet.

Here's an example of the new testForScroll() method, which is the event handler for the onscroll event for the virtual table:

function testForScroll() {
  var startRow, rows, i;

  startRow = Math.floor(element.scrollTop / SpacerHeight);
  rows = Math.floor(element.offsetHeight / SpacerHeight);
  for (i=startRow; i<=startRow+rows; i++) {
    if (!FilledArray[i]) {
      FilledArray[i] = 2;
      getMoreData(i);
      return;
    }
  }
}

I depend on each table row having a fixed height. So, based on the scrollTop property, I can tell how far down from the top the table is scrolled and, by simple division, find out what row should be at that location.

Inserting the rows at the right vertical position in the table was a trick as well. I considered having a set of dummy rows in the table, all with the right size, but then hiding them with the visibility: hidden property. However, this would have caused the same performance problems we saw in the very first version with the spacers. The solution to this was simple—absolute positioning. With Internet Explorer 5, even table rows can be absolutely positioned. Now, as a row is copied into the virtual table, its vertical position is set as a factor of its record number in the data set, multiplied by the constant row height.

Here's the copy code:

function copyRows() {
  var i, copyRow, row;

  for (i=0; i<SourceTable.rows.length; i++) {
     row = SourceTable.rows[i];

     if ((!FilledArray[row.recordNumber]) ||
       (FilledArray[row.recordNumber] == 2)) {
       // this uses new DOM functionality
       // to deep copy the row from the table
       copyRow = row.cloneNode(true);
      copyRow.style.position = "absolute";
      copyRow.style.posTop = SpacerHeight *
                             (row.recordNumber - 1);
      copyRow.style.posLeft = 0;
      copyRow.style.display = "block";
      copyRow.style.zIndex = "100";

      TargetBody.insertBefore(copyRow, null);
       // this is used to keep track of how many
       // rows are copied, to insure no duplicates
       LastRecordNumber = row.recordNumber;
      FilledArray[row.recordNumber] = 1;
     }
  }
}

The only remaining problem was to make it a DHTML behavior. The code changes I had to make were minimal. The difference was that instead of some immediate script code, I created an initialize() method that's called immediately after the new tag is parsed. Also, instead of directly setting event properties, such as element.onreadystatechange, I used the more component-friendly element.attachEvent() method. Remember, multiple listeners can be attached via the attachEvent() member, where as only one listener can be set with the event property directly. It's much better to use attachEvent() in a DHTML behavior, so that users won't accidentally break your functionality (or vice versa) by overriding one of your event settings.

Here's the HTML in the main document that shows how the new <ie:vtable> DHTML behavior is attached to a data-bound table.

<ie:vtable ID=ScrollContainer TABLEID=MyTable
  STYLE="width: 100%; height: 400px; background-color: blue">
 <TABLE BORDER="1" ID=MyTable DATASRC=#LargeData
   WIDTH=100% STYLE="table-layout: fixed;">
  <TR STYLE="background-color: blue; color: yellow">
    <TD>
      <DIV DATAFLD=data></DIV>
    </TD>
  </TR>
</TABLE>
</ie:vtable>

See the new and improved componentized and fully virtual table.

Download the HTML Component .htc file.

Bonus Behavior

Another code sniglet I wrote this month was a moveable separator (the moveable thing between panes in a program that you can move to resize). HTML frames have had this functionality for a while, but I wanted to resize panes, without the overhead and complexity of frames. The feature that really makes this all possible is dynamic properties. The three <DIV> elements in this sample have their positions specified with dynamic properties. Check out the sample and download the HTML Component .htc file.

This separator can "live move"—so as you drag it, the content resizes with you. Or, you can have it move an outline (which is nicely alpha blended over the background content), and then resize the page only after you drop it.

Building Applications and Real Code

This doesn't seem much like HTML, does it? It's almost like a platform for building applications. Yes, that was a setup. As people become more and more familiar with the capabilities and functionality in Internet Explorer and DHTML, those features are used more and more as a general-purpose platform, and less as a way to deliver the singing monkey page (although I have nothing against monkeys, or even singing monkeys). I hope these application components are helpful, and that you find even more interesting ways to use them.

 

DHTML Dude

Michael Wallent is Microsoft's group program manager for Internet Explorer.


  
Show:
© 2014 Microsoft