Digital Colony!

Fixed Column HTML Tables

A developer will often get a requirement to query a database and return the results in an HTML table. If the number of records returned is a lot then the developer is asked to page the results (click here for the next 20 records). Paging is no fun, but it understandable that the user would not want to scroll to the point where they can't view the column headers. An ideal sitution is to return as much of the data as possible in format that the user can easily view and understand the data. Microsoft Excel allows users to fix column headers, this article explains how to do it HTML.

A Simple Solution

In HTML we can used a fixed width DIV to hold the data inside a TABLE. And we can restrict the height of the DIV to a pre-defined number of pixels. For space reasons, the examples below restrict the TABLE to 70 pixels. Setting the overflow attribute to auto will force the scrolling to take place inside the DIV. Now we can just code the column headers above the DIV in a separate TABLE. If the column header TABLE and the DIV have the same width it will appear as though the column headers are fixed.
<table width="400">
<tr>
<th>Name</th><th>City</th><th>1999 Sales</th><th>2000 Sales</th><th>2001 Sales</th>
<tr>
</table>

<div style="width:400; height:100; overflow:auto;border:" id="dataList">
<table width="380">
<!-- Add records here -->
</table>
</div>

Example 1

NameCity1999 Sales2000 Sales2001 Sales
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000

Getting the Columns Straight

In the above example the columns are a little crooked. They don't exactly line up. I tried use javascript to calculate the width of the data cells in order to draw fixed width column headers, but I couldn't get it to work. If anyone has been able to to do this please email me the code. In order for the column headers to line up straight, I did it the old-fashioned way: I hard coded the width of the table cells to match the width of the column headers.

Example 2

This is the same table with fixed width cells.
NameCity1999 Sales2000 Sales2001 Sales
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000

More Tips

Sometimes sizing everything perfectly can be difficult, especially if the data varies in length. Here are a few more tips to help achieve an Excel-like table in HTML.

table-layout:fixed - Dianliang Zhu alerted me to this CSS tag. By adding STYLE="table-layout:fixed;" to our table we can contain the browser cell to the width we define. However, if we don't define our cell widths, it will use the width of the first cells to determine the table layout. This is an excellent tag to use, so be sure to define the cell widths.

Fixed-Width Fonts - Counting characters can be a pain, especially when the size of each character varies in width. To eliminate this possibility consider using a fixed-width (aka Monospace) font such as Courier for the table.

Server-Side Data Trimming - To prevent long pieces of data from pushing out or streching a cell, perform some server-siding trimming. Do you need the full company name or would the first 30 characters be enough? (EX: companyName = Left(companyname,30))

Example 3 - Using More Tips

NameCity1999 Sales2000 Sales2001 Sales
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000
JoeAtlanta$400,000$450,000$430,000
SueMemphis$500,000$550,000$455,000
MattNYC$300,000$350,000$330,000
LouAkron$200,000$150,000$130,000
AnnAustin$400,000$450,000$430,000

Last Words

As expected, not every browser supports the overflow feature. The ideal use of this would be for a corporate Intranet where users were restricted to a one standards-compliant browser.

This article was first written in 2002. For a different solution see Cross-browser scrolling tbody.

Labels: ,

 

DataFormatString Not Working? Here is the Fix

If you have a GridView control and are are trying to data format a BoundField, you may find your format instructions are being ignored. The fix is to add HtmlEncode="False" to the asp:BoundField. Then the DataFormatString should work.
<asp:BoundField DataField="LastModified" 
  HeaderText="Modified" 
  HtmlEncode="False" 
  DataFormatString="{0:d}" />

Labels:

 

Mask Email ASCII Control for ASP.NET

In the article Masking Your Email Address, we went over why you would want to hide your email address inside the source code of an HTML document, but still make it visible to the human readers of that page.

Encapsulating the code into a single .NET user control is ideal for protecting email addresses for ASP.NET sites.

Step 1 - Create a Web User Control

Add an asp:Literal control to that page.
<asp:Literal ID="ltEmail" runat="server" />

Step 2 - Jump to the Code Behind

The EmailMask code follows. Note the name of the class lab_maskemail_EmailMask was created by Visual Studio for me. Use whatever name you like or Visual Studio recommends here. The rest of the code should be the same. This code is for ASP.NET 2.0.
using System;
using System.Text;
using System.Web;

public partial class lab_maskemail_EmailMask : System.Web.UI.UserControl
{
    private string emailAddress;
    public string EmailAddress
    {
        get { return emailAddress; }
        set { emailAddress = value; } 
    }

    private string visibleAddress;
    public string VisibleAddress
    {
        get 
        {
            // if unassigned return emailAddress
            if (visibleAddress==null || visibleAddress.Length == 0)
            {
                return emailAddress;
            }
            else
            {
                return visibleAddress; 
            }
            
       }
        set { visibleAddress = value; }
    }

    private string mouseoverTag;
    public string MouseoverTag
    {
        get { return mouseoverTag; }
        set { mouseoverTag = value; }
    }

    private string subject;
    public string Subject
    {
        get { return subject; }
        set { subject = value; }
    }

    private string cssClass;
    public string CssClass
    {
        get { return cssClass;}
        set { cssClass = value; }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        // The MIN required for control is an email address, confirm it exists
        if (emailAddress == null || emailAddress.Length == 0)
        {
            ltEmail.Text = "Assign EmailAddress to Control";
            return;
        }

        // Build ASCII encoded Link
        StringBuilder asciiLink = new StringBuilder();
        asciiLink.Append("<a href=\"m&#97;ilto:");
        asciiLink.Append(ASCIIEncode(EmailAddress));
        if(subject != null && subject.Length > 0 )
        {
            asciiLink.Append("?subject=" + subject);
        }
        asciiLink.Append("\"");
        if (mouseoverTag != null && mouseoverTag.Length > 0)
        {
            asciiLink.Append(" title=\"" + mouseoverTag + "\"");
        }
        if (cssClass !=null && cssClass.Length > 0)
        {
            asciiLink.Append(" class=\"" + cssClass + "\"");
        }
        asciiLink.Append(">");
        asciiLink.Append(ASCIIEncode(VisibleAddress));
        asciiLink.Append("</a>");

        ltEmail.Text = asciiLink.ToString();      
    }

    protected string ASCIIEncode(string regularText)
    {
        regularText = regularText.Trim();

        StringBuilder encodeSB = new StringBuilder();
        char regularLetter;

        for (int j = 0; j < regularText.Length; j++)
        {
            // peel off 1 character at a time
            regularLetter = regularText[j];
            encodeSB.Append("&#" + Convert.ToInt32(regularLetter).ToString() + ";");
        }

        return encodeSB.ToString();
    }

Step 3 - Create a Test Page

Register the control at the top using whatever path and naming convention you've chosen.
<%@ Register Src="~/lab/maskemail/EmailMask.ascx" TagName="EmailMask" TagPrefix="dc" %>
Inside the ASP.NET page and control is used like below.
<dc:EmailMask ID="eMask" 
 CssClass="Summer" 
 EmailAddress="larryKing@cnn.com" 
 VisibleAddress="Email Larry King"   
 MouseoverTag="Send feedback" 
 Subject="Tonight's Show" 
 runat="server" />

Labels: , ,

 

Delete Files using C#

When I first wrote the original version of the Mask Email Image Generator (Email Obfuscator), I didn't bother to add any code to periodically remove the images from the server. Yesterday I discovered it had almost 50,000 images in that folder. Not to repeat the same mistake, I wrote a function to delete image files that are older than 3 minutes. Sample code follows.
protected void CleanImageFolder()
{
    string imgFolder = Server.MapPath("~/lab/maskemail/img/");
    string[] imgList = Directory.GetFiles(imgFolder, "*.jpg");
    foreach (string img in imgList)
    {
        FileInfo imgInfo = new FileInfo(img);
        if (imgInfo.LastWriteTime < DateTime.Now.AddMinutes(-3))
        {
            imgInfo.Delete();
        }
    }
}

Labels: , , ,

 

Convert string to Color

You will get the following error when trying to assigning a string as a Color in .NET.
Cannot implicitly convert type 'string' to 'System.Drawing.Color'

Import System.Drawing and use Color.FromName to convert a string to a color.
using System.Drawing;
Color.FromName("Blue");

Labels:

 

Masking Your Email Address

Some of you are probably aware of spiders. They are these little programs that surf the internet looking for data. Some spiders assist search engines in helping you find the web page you are looking for. Those are the good spiders. There also exists evil spiders. They jump from web page to web page looking for email addresses. Once they find one, they send it to a database so someone can send you junk email. Not cool.

Hiding In Plain Sight

What we need is a way to display an email address so the reader of a web page can communicate with the web site, yet we also need to hide the address from the spider. The reader and the spider are looking at the same web page but at differently levels. The reader is looking at the browser's rendering of HTML. The spider is looking at raw HTML. Three ideas come to mind: ASCII codes, server-side mail forms and images. ASCII codes and images will look like email addresses on the screen, but nothing like an email address in the source code of the HTML document.

Method 1: ASCII

In HTML when you place "&#" in front of the ASCII code of a character the browser will write the character not the ASCII code to the screen. And because this article is being viewed by a browser, the code shots are images. The download will have the source in a text format.

The function below accepts an email address as a parameter and returns a masked email address that is made up of ASCII codes. When the browser writes the codes to the screen it will get converted back to text. Although it's possible for a spider to read and convert ASCII codes inside the HTML source, it's probably not that prevalent. The function goes character by character converting the email address. The last step is to merge the masked email address with the HTML mailto: tag. In order to minimize the chances a clever spider might look for the mailto:, this example maskes that word as well.
Function maskEmail(email) 
   For j= 1 to Len(email) 
      maskEmail = maskEmail & "&#" & asc(Mid(email,j,1)) & ";" 
   Next 
   maskEmail = "<a href=""m&#" & asc("a") & ";&#" & asc("i") & ";&#" &_ 
     asc("l") & ";&#" & asc("t") & ";o:" & maskEmail & """>" & maskEmail & "</a>" 
End Function
Then you can call that VBScript function from inside an ASP page.
<p>For more information email me at <%= maskEmail("someEmail@someDomain.net") %>.</p>

Method 2: Server-side Mail Forms

These are the contact forms you see everywhere these days. The user fills out a form, clicks submit, and hopes it gets to somebody. This is great for the recipient, because their email address never appears on the site. However, some users don't trust filling out a form and will withhold feedback. Recently I was trying to open a new account with eFax.com. Their order entry page was down so I filled out a form alerting them to the problem. After detailing the problem the form rejected because I didn't already have an account with them. They didn't have a direct email address listed anywhere else, so I became a customer of one of their competitors.

Method 3: Images

I believe the ASCII method will work for a little while. Eventually as that trick becomes more popular, the spiders will get smarter. Who knows maybe the developer that writes the code for those evil little email spiders is reading this article right now. If we created an image with text of our email, the spiders would be truly be defeated. However creating an image for every email address in PhotoShop would be a hassle. And then what happens when we change fonts when the site gets redesigned? Creating the email images by hand isn't an option. We need to automate.

For this version I extended the ImageToText code sample at yyyZ.net.

Method 4: Chopped Javascript

Now that you have an email image or icon, you may want to assist the users so they click on an email address image it behaves as if it were a hypertext link. This means having their email client launched with the TO line filled out for them. In order for this to happen with the email image, we need to hack out a chopped Javascript function. There are many possible ways to write it. In this example I wrote a function that accepts an email address into 3 parts. It then reassembles the email and launches the email client. How you chop up the email address is up to you. Also feel free to change the sequence of the parameters.
<script language="JavaScript" type="text/javascript">  
function postage(one,two,three){ 
   window.location = 'mailto:'+one+two+three;} 
</script> 
<img src="123.jpg" alt="email" width="100" 
    height="40" border="0" 
    onmouseover="this.style.cursor='hand'" 
    onclick="postage('larryking@c','n','n.com');"/> 

Lab Demos

Mask Email Image Generator (Email Obfuscator)

Mask Email ASCII Generator

PHP Version of Masking Email Addresses

Labels: , , , ,

 

Blogger Label List for FTP Accounts (Classic ASP)

For an overview on Blogger Label Lists read Blogger Label List for FTP Accounts (ASP.NET). Below is the code used to create a Label List using Classic ASP with VBScript.

Modify the first line to point to your label folder. Then correct the spelling of bl0gger-labels. It is purposely misspelled as to not throw off the count on this page. Save this code with a .asp file extension and then use a server-side include to place in onto your page template.
'-- add your label directory here
labelDir = Server.MapPath("/myblog/labels/") 
'-- Check for Directory
Set FSO = Server.CreateObject("Scripting.FileSystemObject")
If FSO.FolderExists(labelDir) Then
    Response.Write "<ul>"
    Set labelFolder = FSO.GetFolder(labelDir)
    Set labelBlogs = labelFolder.Files
    For each label in labelBlogs
        Set labelFile = FSO.GetFile(label)
        Set labelStream = labelFile.OpenAsTextStream (1, -2)    
        iLabelCount = 0
        '-- Read the file line by line
        Do While Not labelStream.AtEndOfStream                
            Line = labelStream.readline
            LineCount = Sgn(InStr(Line,"bl0gger-labels"))                
            iLabelCount = iLabelCount + LineCount
        Loop                                
        Response.Write "<li>" + Replace(labelFile.Name,".asp","") & " (" + CSTR(iLabelCount) & ")</li>"
        Set labelStream = nothing
        Set labelFile = nothing
    Next
    Response.Write "</ul>"
Else
    Response.Write "<p>No labels in folder: " & labelDir & "</p>"
End If
Set FSO = nothing    
Below is an example of using a server-side include inside a Classic ASP page.
<div id="divLabelList">
<!--#include virtual="/inc/theme.asp"-->
</div>
Back in the day, Ev would have been proud of me.

Labels: , ,

 

Outer Joins Against A Date Range

This article is written for SQL Server 2000, which supports user-defined functions and OUTER JOINS. All examples come from the Northwind database which is part of the default installation of SQL Server.

In SQL an OUTER JOIN is where you want to return all the elements in a table regardless of whether they exist in the second table. The most common case for using an OUTER JOIN is when you need a COUNT or SUM value even when the aggregate function returns a zero. Before we get to the heart of this article, let me use two queries to demonstrate the power of a basic OUTER JOIN.

Customers and Order COUNT

There are 2 ways to tackle this request. The first is with an INNER JOIN. It will only return Customers that have at least one order. The second way is with an OUTER JOIN. It will return all Customers, even those with no Orders.
-- A: INNER JOIN (returns 89 customers, all of which have at least 1 order) 
SELECT C.CompanyName, COUNT(R.OrderID) AS OrderCount 
FROM Customers C 
INNER JOIN Orders R ON C.customerID = R.customerID 
GROUP BY C.CompanyName 
ORDER BY COUNT(R.OrderID) DESC 

-- B: OUTER JOIN (returns all 91 customers, 2 of which have 0 orders) 
SELECT C.CompanyName, COUNT(R.OrderID) AS OrderCount 
FROM Customers C 
LEFT OUTER JOIN Orders R ON C.customerID = R.customerID 
GROUP BY C.CompanyName 
ORDER BY COUNT(R.OrderID) DESC 

Orders by Day of the Week for July 1996

The first OUTER JOIN was straight-forward. But what if the entity you need to perform an OUTER JOIN upon doesn't exist in the database? One such entity that comes to mind is date. Let's suppose the sales manager is trying to determine which day she is going to paint the office based upon the day of the week with historically the least amount of orders. The least amount could be zero, so we need to perform an OUTER JOIN. But how can you perform an OUTER JOIN against a range of dates? Three ways come to mind.

1. Create a TABLE of dates (or digits) using a SQL statement. This could be a temporary or permanent table.
2. Get verbose with your SQL and write long CASE statements.
3. Use the 2 user-defined functions I'm providing to you in this article.

The problem with #1 is now you're executing multiple statements to handle one query. You are creating a TABLE, perform the INSERT and ultimately dropping the TABLE. At this point you'll consider writing a stored procedure to accomplish the task. All for a single query. Too much work. The problem with #2 is your SQL can get very long and hard to read and the data is returned more like an Access PIVOT TABLE. The slick way to handle this task is using a user-defined function.
-- C: Using multiple CASE and SUM statement (long, ugly, and tabular) 
SELECT SUM(CASE DATEPART(dw,OrderDate) WHEN 0 THEN 1 ELSE 0 END) AS MonSales, 
SUM(CASE DATEPART(dw,OrderDate) WHEN 1 THEN 1 ELSE 0 END) AS TueSales, 
SUM(CASE DATEPART(dw,OrderDate) WHEN 2 THEN 1 ELSE 0 END) AS WedSales, 
SUM(CASE DATEPART(dw,OrderDate) WHEN 3 THEN 1 ELSE 0 END) AS ThuSales, 
SUM(CASE DATEPART(dw,OrderDate) WHEN 4 THEN 1 ELSE 0 END) AS FriSales, 
SUM(CASE DATEPART(dw,OrderDate) WHEN 5 THEN 1 ELSE 0 END) AS SatSales, 
SUM(CASE DATEPART(dw,OrderDate) WHEN 6 THEN 1 ELSE 0 END) AS SunSales 
FROM Orders 
WHERE OrderDate BETWEEN '7/1/1996' AND '8/1/1996' 

-- D: Performing an OUTER JOIN against a user-defined function (dbo.udfNumbers) 
SELECT DATENAME(dw,N.number) AS DayName, COUNT(R.orderID) AS OrderCount 
FROM dbo.udfNumbers(0,6) N 
LEFT OUTER JOIN Orders R ON N.number = DATEPART(dw,R.OrderDate) 
AND R.OrderDate BETWEEN '7/1/1996' AND '8/1/1996' 
GROUP BY DATENAME(dw,N.number), N.Number 
ORDER BY N.number

Using dbo.udfNumbers

The dbo.udfNumbers user-defined function returns a TABLE of numbers. It is available for download at the end of this article. The upper range limit is 999,999. If you need larger numbers, modifying the function should be fairly easy.
-- E: Usage dbo.udfNumbers(low range,high range) 
-- will return 6,7,8,9,10,11,12
SELECT number FROM dbo.udfNumbers(6,12)

Orders by Date for July 1996

I use dbo.udfNumbers for those quick queries where I just need a range of digits. For most date-based reporting I use the far more powerful dbo.udfDateTimes. This user-defined function uses the digit logic in dbo.udfNumbers along with SQL Server's date and time function to return ranges of datetime. Query C was based upon Day of the Week, so there were only 7 possible values. What if the sales manager asked for a day by day breakdown of orders for July? That would turn into one long and ugly SQL statement. The dbo.udfDateTimes function creates the date range need for that OUTER JOIN without the long and ugly SQL.
-- F: dbo.udfDateTimes day interval for July 1996 
SELECT dtime AS OrderDate, COUNT(R.orderID) AS OrderCount 
FROM dbo.udfDateTimes ('7/1/1996', '7/31/1996',1,'day') DT 
LEFT OUTER JOIN Orders R ON DT.dtime = R.OrderDate 
GROUP BY dtime

Using dbo.udfDateTimes

Like dbo.udfNumbers, the user-defined function dbo.udfDateTimes returns a TABLE. The first parameter is start date. The second is end date. The third parameter is interval. In most cases this will be set to 1, but if you had a need to see one of the dateparts at an alternate interval, you would modify this parameter. The last parameter is datepart. Those supported are year, quarter, month, week, day, hour and minute. The upper range is 9,999,999, which (like udfNumbers) can easily be modified should need a function that returns more datetimes.

Download

dbo.udfNumbers - function that returns a table of digits.
dbo.udfDateTimes - function that returns a table of datetimes.

Last Words

One last piece of advice is to get your datetime range correct first before attempting to OUTER JOIN with it. Only once you know the dbo.udfDateTimes is returning the datetimes you need for the query should you proceed with your OUTER JOIN. Although the Northwind database deals with orders, another great use for these functions is when looking for missing data. In those cases where you need to seek out the datetimes where no data is present, add a HAVING clause set to zero.
-- G: Days in July 1996 with no orders 
SELECT dtime AS OrderDate, COUNT(R.orderID) AS OrderCount 
FROM dbo.udfDateTimes ('7/1/1996', '7/31/1996',1,'day') DT 
LEFT OUTER JOIN Orders R ON DT.dtime = R.OrderDate 
GROUP BY dtime 
HAVING COUNT(R.orderID) = 0

Labels: , ,

 

Digital Colony Copyright © 1999-2008 XHTML   508
This site uses Blogger, which is not 100% XHTML compliant.
Try...Catch Disclaimer: For brevity many examples do not include error handling. That is your responsibility.