Generating HTML tables for displaying data is a pretty common task these
days. Easy to do, easy to tweak, no special tools needed (though they do come in
handy), all the end user needs is a browser. How you do it can make all the
difference in how easy it is to maintain. They key to separate your display code
from the data. XML and style sheets offer some interesting ways of doing this
which I'll discuss in a later article, today we're just looking at old fashioned
HTML. Combined with our friend the ADO recordset, we can do some wonderful
things. Well, useful anyway. This may be old news to many of you, but I still
see a lot of this table code being written. If nothing else when you see it
you'll have a resource to point the offender to!
Let's start with the some what contrived example of needing to generate a web
page that displays all the files in a specific folder. We know that the folder
is updated once a day. Here is some code that shows one way of doing it that
could easily be run from a SQL job:
Sub LoadFilesIntoTable(PathToLoad As String, OutputFileName As String) Dim oFSO As Scripting.FileSystemObject Dim oFolder As Scripting.Folder Dim oFile As Scripting.File Dim oStream As Scripting.TextStream Dim sTemp As String Set oFSO = New Scripting.FileSystemObject If oFSO.FolderExists(PathToLoad) = True And OutputFileName <> "" Then Set oFolder = oFSO.GetFolder(PathToLoad) 'added tab and cr/lfs to make it more readable when viewing source sTemp = "<TABLE WIDTH=100% BORDER=1>" & vbCrLf For Each oFile In oFolder.Files sTemp = sTemp & vbTab & "<TR><TD>" & oFile.Name & "</TD><TD>" & oFile.Size & "</TD></TR>" & vbCrLf Next sTemp = sTemp & "</TABLE>" Set oFolder = Nothing 'create the file, overwrite any previous version of the file Set oStream = oFSO.CreateTextFile(OutputFileName, True, False) oStream.Write sTemp Set oStream = Nothing End If Set oFSO = Nothing End Sub |
Nothing very fancy, just using the filesystem object to both get the list of
files and to write it back to disk. See how the table formatting tags are mixed
in? Now suppose the user asks for the date created to be added. We only need to
change one line, like this:
sTemp = sTemp & vbTab & "<TR><TD>" & oFile.Name & "</TD><TD>" & oFile.Size & "</TD><TD>" & oFile.DateCreated & "</TD></TR>" & vbCrLf |
Fairly trivial right? What if the report needed to be sorted by file size? Or
able to sort on all columns? Sorting on all columns requires more effort, we'll
cover that next time too - but sorting on one column we should be able to do.
But how? Just thinking while I write, I came up with these ideas:
1) Load the info into an array, write or find some sorting code, sort it,
then do the file generation.
2) Maybe some kind of hack, use the OS to do something like this "dir
*.* >temp.txt" which will create temp.txt containing all the files in
the folder, then load & parse it back into columns, then do the file
generation.
3) Load the data into a table, sort it using an order by, create the file,
drop the table.
All will work, but being a SQL site and all, I think we'll try #3! Here is
what I came up with:
Sub LoadFilesIntoTable2(PathToLoad As String, OutputFileName As String) Dim oFSO As Scripting.FileSystemObject Dim oFolder As Scripting.Folder Dim oFile As Scripting.File Dim oStream As Scripting.TextStream Dim sTemp As String Dim cn As ADODB.Connection Dim rs As ADODB.Recordset Set oFSO = New Scripting.FileSystemObject If oFSO.FolderExists(PathToLoad) = True And OutputFileName <> "" Then Set cn = New ADODB.Connection cn.Open "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=pubs;Data Source=EG\ONE" cn.Execute "Create Table #FileList(FileName varchar(100), FileSize int, FileCreated datetime)" Set oFolder = oFSO.GetFolder(PathToLoad) For Each oFile In oFolder.Files cn.Execute "Insert into #FileList (FileName, FileSize, FileCreated) values ('" & oFile.Name & "'," & oFile.Size & ",'" & oFile.DateCreated & "')" Next Set oFolder = Nothing Set rs = cn.Execute("Select * from #FileList order by Filesize") 'added tab and cr/lfs to make it more readable when viewing source sTemp = "<TABLE WIDTH=100% BORDER=1>" & vbCrLf Do Until rs.EOF sTemp = sTemp & vbTab & "<TR><TD>" & rs.Fields("FileName") & "</TD><TD>" & rs.Fields("FileSize") & "</TD><TD>" & rs.Fields("FileCreated") & "</TD></TR>" & vbCrLf rs.MoveNext Loop sTemp = sTemp & "</TABLE>" rs.Close Set rs = Nothing cn.Execute "drop table #FileList" cn.Close Set cn = Nothing 'create the file, overwrite any previous version of the file Set oStream = oFSO.CreateTextFile(OutputFileName, True, False) oStream.Write sTemp Set oStream = Nothing End If Set oFSO = Nothing End Sub |
Now we can sort on any column if we need to, we could even use a where clause
to restrict the output. We've still got our display code mixed with the data and
I'll bet at least one person is thinking that using SQL and a table for sorting
this list is overkill?
Let's do something about making this code more generic and separating the
UI/data. For that all we need is a function that creates a table from a
recordset. Here is a very simple version:
Public Function CreateTableFromRS(rs As ADODB.Recordset) As String Dim J As Integer Dim sTemp As String sTemp = "<TABLE WIDTH=100% BORDER=1>" Do Until rs.EOF sTemp = sTemp & "<TR>" For J = 0 To rs.Fields.Count - 1 sTemp = sTemp & "<TD>" & rs.Fields(J) & "</TD>" Next sTemp = sTemp & "</TR>" rs.MoveNext Loop sTemp = sTemp & "</TABLE>" CreateTableFromRS = sTemp End Function |
Once we have that, we can change our earlier code to look like this:
Set rs = cn.Execute("Select * from #FileList order by Filesize") sTemp = CreateTableFromRS(rs) |
Now if (when!) we change the columns included, the output portion is handled
automatically. Now let me show you another advantage of this technique. Suppose
you'd like to add a header row to your table. Going back to our earlier example,
we would do something like this:
sTemp = sTemp & "<TR><TH>FileName</TH><TH>File Size</TH><TH>Date Created<TH></TR>" |
This works, but now we are back to having to change our code in multiple
places each time we change the columns being displayed. If we use the table
technique and our new function, we can leverage some meta data to do this work
for us, like this:
Public Function CreateTableFromRS2(rs As ADODB.Recordset) As String Dim J As Integer Dim sTemp As String sTemp = "<TABLE WIDTH=100% BORDER=1>" sTemp = sTemp & "<TR>" For J = 0 To rs.Fields.Count - 1 sTemp = sTemp & "<TH>" & rs.Fields(J).Name & "</TH>" Next sTemp = sTemp & "</TR>" & vbCrLf Do Until rs.EOF sTemp = sTemp & "<TR>" For J = 0 To rs.Fields.Count - 1 sTemp = sTemp & vbTab & "<TD>" & rs.Fields(J) & "</TD>" & vbCrLf Next sTemp = sTemp & "</TR>" rs.MoveNext Loop sTemp = sTemp & "</TABLE>" CreateTableFromRS2 = sTemp End Function |
As you can see I'm looping through the fields collections to get the column
names. This code be easily extended to format the column sizes based on the size
of the column, do special formatting when the data type is a number or date,
etc. If you use this function in all your pages, you leverage every little
improvement you make across all of those pages! That is code reuse at it's best.
Now let's return to whether we really need SQL to do the sorting. Or even to
create a recordset. The answer is a conditional no. We can do the sorting other
ways, but having the recordset is what makes the separation between data and UI
work. We don't have to have SQL running to use a recordset though. In the next
example I'm creating a disconnected recordset, loading the data into it, then
using it's ability to filter and sort to customize the output.
Sub LoadFilesIntoTable3(PathToLoad As String, OutputFileName As String) Dim oFSO As Scripting.FileSystemObject Dim oFolder As Scripting.Folder Dim oFile As Scripting.File Dim oStream As Scripting.TextStream Dim sTemp As String Dim rs As ADODB.Recordset Set oFSO = New Scripting.FileSystemObject If oFSO.FolderExists(PathToLoad) = True And OutputFileName <> "" Then Set rs = New ADODB.Recordset rs.Fields.Append "FileName", adVarChar, 100 rs.Fields.Append "FileSize", adInteger rs.Fields.Append "FileCreated", adDBTimeStamp rs.Open Set oFolder = oFSO.GetFolder(PathToLoad) For Each oFile In oFolder.Files rs.AddNew rs.Fields("FileName") = oFile.Name rs.Fields("FileSize") = oFile.Size rs.Fields("FileCreated") = oFile.DateCreated rs.Update Next Set oFolder = Nothing 'sort it rs.Filter = "Filename like 's%'" rs.Sort = "FileSize desc" rs.MoveFirst 'probably should go in the createtable function sTemp = CreateTableFromRS2(rs) rs.Close Set rs = Nothing 'create the file, overwrite any previous version of the file Set oStream = oFSO.CreateTextFile(OutputFileName, True, False) oStream.Write sTemp Set oStream = Nothing End If Set oFSO = Nothing End Sub |
Whether you create a table, do a select from an existing table, or create a
disconnected recordset depends on the circumstances of course. The key is that
by always pushing our data into a recordset we can leverage our display code
heavily and that is worth something.
In a follow up article we'll look at how we can use XML to accomplish the
same thing and add the ability to do client side sorting. As always I look
forward to your comments. Whether you agree or disagree, take a min to add your
thoughts to the discussion forum attached to the article, other readers will
appreciate the additional insight only you can provide.