前回GridviewやTableコントロールから出力されたtable要素の行ヘッダのみを別tableに切り出すExtenderを作成してみました。今回はさらに列ヘッダも固定化するようなExtenderを作成してみました。
- 今回作成するExtenderには制限があります。
列固定化をする場合は、列ヘッダの行ヘッダもしくは列ヘッダデータ行ヘッダ(設定による)の幅を指定するようにして下さい。そうしないと列幅が最大になる場合があります。つまり、GridViewのAutoGenerateSelectButtonなどを使えないということです。
確認環境
- 確認したWebブラウザ IE 7
- 開発環境: Visual Studio 2008 Professional(英語版)
- .NET 3.5 (Ajax Control Toolkitは使っていないです。
- 確認したWeb Control: GridView
.NET 3.5のインストールされた環境でテストしていますが、サーバにはASP.NET AJAX Extention 1.0 がインストールされている環境なら、.NET2.0環境でも動作するのかなと思います。確認はまったくしていませんが。GridViewでしか試していませんが、Tableでも問題ないと思います。確認していませんが。
結果のサンプルを掲載します。左上と右下にスクロールした結果です。
1. 実装方法
行ヘッダの固定化と方法は同じです。列ヘッダを別tableに取り出し、列ヘッダ固定用の行ヘッダtable、列ヘッダ固定用の行データtable、行ヘッダ固定用のtable,行データようのtableの4つのtableに分解して、その4つのtableをさらに1つの2*2のtableに含めるようにしています。
2.ソリューションの作成
Visual Studio 2008を起動して、新規の空のソリューションを作成します。ここでは、ComponentGeek.Extenderというソリューション名で作成したとします。
3.エクステンダーの作成
ソリューションエクスプローラ上のソリューションを右クリック→[Add]→[New Project]でAdd New Projectダイアログを表示し、Project typeにWebを選択、TemplateにASP.NET AJAX Server Control Extenderを選択し、プロジェクト名をComponentGeek.FreezeHeaderExtenderとして、[OK]ボタンをクリックして、プロジェクトを新規作成します。
作成されたjs,cs,resxファイルをそれぞれ、FreezeHeaderBehavior.js,FreezeHeaderBehavior.resx,FreezeHeaderExtender.csという名前にリネームします。
3.1 Extenderの編集
FreezeHeaderExtender.csを開き、次のように編集します。ヘッダ部のCss,ボディ部のCssをオプションで設定できるようにしています。プロパティのWidth,Heightはボディの幅、高さを表します。さらに今回は、行ヘッダ行数,列ヘッダ数を指定できるようにしています。FreezeHeaderAdjustWidthModeを追加し、table間の列幅を整えるときのモードを指定できるようにしています。通常はヘッダの幅に合わせます。
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml.Linq;
using System.ComponentModel;
namespace ComponentGeek.FreezeHeaderExtender
{
/// <summary>
/// Summary description for FreezeHeaderExtender
/// </summary>
[TargetControlType(typeof(Control))]
public class FreezeHeaderExtender : ExtenderControl
{
public FreezeHeaderExtender()
{
}
/// <summary>
/// ヘッダテーブルのCssClass
/// </summary>
public string HeaderCssClass { get; set; }
/// <summary>
/// ボディテーブルのCssClass
/// </summary>
public string BodyCssClass { get; set; }
/// <summary>
/// ボディ部の幅
/// </summary>
public Unit Width { get; set; }
/// <summary>
/// ボディ部の高さ
/// </summary>
public Unit Height { get; set; }
private FreezeHeaderAdjustWidthMode _widthAdjustMode = FreezeHeaderAdjustWidthMode.Header;
/// <summary>
/// 幅調整モード
/// </summary>
[DefaultValue(FreezeHeaderAdjustWidthMode.Header)]
public FreezeHeaderAdjustWidthMode WidthAdjustMode
{
get { return _widthAdjustMode; }
set { _widthAdjustMode = value; }
}
public int _headerColumnCount = 0;
/// <summary>
/// ヘッダ列の数
/// </summary>
[DefaultValue(0)]
public int HeaderColumnCount
{
get { return _headerColumnCount; }
set { _headerColumnCount = value; }
}
public int _headerRowCount = 1;
/// <summary>
/// ヘッダ行の行数
/// </summary>
[DefaultValue(1)]
public int HeaderRowCount
{
get { return _headerRowCount; }
set { _headerRowCount = value; }
}
protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(System.Web.UI.Control targetControl)
{
if (!targetControl.HasControls()) return new ScriptBehaviorDescriptor[]{};
ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor("ComponentGeek.FreezeHeaderBehavior", targetControl.ClientID);
if (!string.IsNullOrEmpty(HeaderCssClass))
{
descriptor.AddProperty("headerCssClass", HeaderCssClass);
}
if (!string.IsNullOrEmpty(BodyCssClass))
{
descriptor.AddProperty("bodyCssClass", BodyCssClass);
}
if (Height != null)
{
descriptor.AddProperty("height", Height.ToString());
}
if (Width != null)
{
descriptor.AddProperty("width", Width.ToString());
}
descriptor.AddProperty("widthAdjustMode", WidthAdjustMode);
descriptor.AddProperty("headerColumnCount", HeaderColumnCount);
descriptor.AddProperty("headerRowCount", HeaderRowCount);
Page.Request.Browser.Browser.GetTypeCode();
return new ScriptBehaviorDescriptor[]{descriptor};
}
protected override IEnumerable<ScriptReference> GetScriptReferences()
{
yield return new ScriptReference("ComponentGeek.FreezeHeaderExtender.FreezeHeaderBehavior.js", this.GetType().Assembly.FullName);
}
}
/// <summary>
/// 幅調整モード
/// - Default : 幅の広いほうに合わせる
/// - Header : ヘッダ行の幅に合わせる
/// - Data : データ行の幅に合わせる
/// </summary>
public enum FreezeHeaderAdjustWidthMode
{
Default = 0,
Header,
Data
}
}
3.2 javascriptの編集
FreezeHeaderBehavior.jsを開いて次のように編集します。ComponentGeek.FreezeHeaderAdjustWidthMode型(enum型)のプロパティ_widthAdjustModeをFreezeHeaderBehaviorに追加しています。
/// <reference name="MicrosoftAjax.js"/>
Type.registerNamespace("ComponentGeek");
ComponentGeek.FreezeHeaderBehavior = function(element) {
ComponentGeek.FreezeHeaderBehavior.initializeBase(this, [element]);
this._headerCssClass = null; // ヘッダのCSSクラス
this._bodyCssClass = null; // データのCSSクラス
this._headerDiv = null; // ヘッダのdiv
this._headerTable = null; // ヘッダのtable
this._headerRowCount = 1; // 行ヘッダ数
this._bodyDiv = null; // データのdiv
this._bodyTable = null; // データのtable
this._columnHeaderDiv = null; // 列ヘッダのヘッダのdiv
this._columnHeaderTable = null; // 列ヘッダのヘッダのtable
this._columnHeaderRowDiv = null; // 列ヘッダのデータのdiv
this._columnHeaderRowTable = null; // 列ヘッダのデータのtable
this._headerColumnCount = 0; // 列ヘッダ数
this._rootDiv = null; // ルートdiv
this._width = 400; // 幅のデフォルト
this._height = 200; // 高さのデフォルト
this._vScrollBarWidth = 17; // browser depend
this._hScrollBarHeight = 17; // browser depend
this._widthAdjustMode = ComponentGeek.FreezeHeaderAdjustWidthMode.Default;
}
ComponentGeek.FreezeHeaderBehavior.prototype = {
initialize: function() {
ComponentGeek.FreezeHeaderBehavior.callBaseMethod(this, 'initialize');
// Add custom initialization here
this.createFreezeTable();
this.adjustColWidth();
this.adjustRowHeight();
$addHandler(this._bodyDiv, "scroll", Function.createDelegate(this, this._onScroll));
},
dispose: function() {
//Add custom dispose actions here
if(this._bodyDiv){
$clearHandlers(this._bodyDiv);
}
ComponentGeek.FreezeHeaderBehavior.callBaseMethod(this, 'dispose');
},
get_headerCssClass : function(){
return this._headerCssClass;
},
set_headerCssClass : function(value){
this._headerCssClass = value;
},
get_bodyCssClass : function(){
return this._bodyCssClass;
},
set_bodyCssClass : function(value){
this._bodyCssClass = value;
},
get_width : function(){
return this._width;
},
set_width : function(value){
this._width = value;
},
get_height : function(){
return this._height;
},
set_height : function(value){
this._height = value;
},
get_vScrollBarWidth : function(){
return this._vScrollBarWidth;
},
set_vScrollBarWidth : function(value){
this._vScrollBarWidth = value;
},
get_widthAdjustMode : function(){
return this._widthAdjustMode;
},
set_widthAdjustMode : function(value){
this._widthAdjustMode = value;
},
get_headerColumnCount : function(){
return this._headerColumnCount;
},
set_headerColumnCount : function(value){
this._headerColumnCount = value;
},
get_headerRowCount : function(){
return this._headerRowCount;
},
set_headerRowCount : function(value){
this._headerRowCount = value;
},
_onScroll : function(e){
this._headerDiv.scrollLeft = this._bodyDiv.scrollLeft;
if(this._columnHeaderRowDiv){
this._columnHeaderRowDiv.scrollTop = this._bodyDiv.scrollTop;
}
},
_showIfDisplayNone : function(){
if(this._headerTable && this._headerTable.style.display == "none")
this._headerTable.style.display = "";
if(this._bodyTable && this._bodyTable.style.display == "none")
this._bodyTable.style.display = "";
if(this._columnHeaderTable && this._columnHeaderTable.style.display == "none")
this._columnHeaderTable.style.display = "";
if(this._columnHeaderRowTable && this._columnHeaderRowTable.style.display == "none")
this._columnHeaderRowTable.style.display = "";
},
populateColumnHeader : ComponentGeek$FreezeHeaderBehavior$populateColumnHeader,
populateColumnHeaderRow : ComponentGeek$FreezeHeaderBehavior$populateColumnHeaderRow,
populateHeader : ComponentGeek$FreezeHeaderBehavior$populateHeader,
populateBody : ComponentGeek$FreezeHeaderBehavior$populateBody,
createFreezeTable : ComponentGeek$FreezeHeaderBehavior$createFreezeTable,
adjustColWidth : ComponentGeek$FreezeHeaderBehavior$adjustColWidth,
adjustRowHeight : ComponentGeek$FreezeHeaderBehavior$adjustRowHeight
}
function ComponentGeek$FreezeHeaderBehavior$createFreezeTable(){
// テーブルを作成する
var table;
if(this.get_element().tagName == "TABLE") table = this.get_element();
else{
var tables = get_element().getElementByTagName("table");
if(tables.length == 0) throw Error.argument('element', 'table is not found');
table = tables[0];
}
this._rootDiv = document.createElement("div");
table.parentElement.insertBefore(this._rootDiv, table);
this.populateHeader(table);
this.populateBody(table);
this.populateColumnHeader();
this.populateColumnHeaderRow();
// div直下にさらにtableを作成して追加
var t = document.createElement("table");
t.cellSpacing = 0;
t.cellPadding = 0;
t.border = 0;
var b = document.createElement("tbody");
t.appendChild(b);
var r = document.createElement("tr");
var d = document.createElement("td");
r.appendChild(d);
if(this._columnHeaderDiv){
d.appendChild(this._columnHeaderDiv);
d = document.createElement("td");
r.appendChild(d);
}
d.appendChild(this._headerDiv);
b.appendChild(r);
r = document.createElement("tr");
d = document.createElement("td");
r.appendChild(d);
if(this._columnHeaderRowDiv){
d.appendChild(this._columnHeaderRowDiv);
d = document.createElement("td");
r.appendChild(d);
}
d.appendChild(this._bodyDiv);
b.appendChild(r);
this._rootDiv.appendChild(t);
this._showIfDisplayNone();
// 構築結果の出力
// デバッグ情報を出力したい場合はid=traceConsoleのtextareaを作成すること
Sys.Debug.traceDump(this._rootDiv.outerHTML, "ComponentGeek.FreezeHeaderBehavior OuterHtml");
Sys.Debug.traceDump(this, "ComponentGeek.FreezeHeaderBehavior trace");
}
function ComponentGeek$FreezeHeaderBehavior$populateHeader(orgTable){
// create header div
this._headerDiv = document.createElement("div");
this._headerDiv.style.width = this._width;
this._headerDiv.style.overflow = "hidden";
this._headerDiv.style.position = "relative";
// create header table
this._headerTable = orgTable.cloneNode();
this._headerTable.style.height = ""; // 高さは引き継がない
if(this._headerCssClass && this._headerCssClass.length > 0){
this._headerTable.className = this._headerCssClass;
}
this._headerTable.style.tableLayout = "fixed";
this._headerTable.style.marginRight = this._vScrollBarWidth;
// create header content
var headerBody = document.createElement("tbody");
if(this._headerRowCount > 0 && orgTable.rows.length >= this._headerRowCount){
for(var i=0;i<this._headerRowCount;++i){
var headerRow = orgTable.rows(0);
headerBody.appendChild(headerRow);
}
}
this._headerTable.appendChild(headerBody);
this._headerDiv.appendChild(this._headerTable);
}
function ComponentGeek$FreezeHeaderBehavior$populateBody(bodyTable){
// create body div
this._bodyDiv = document.createElement("div");
this._bodyDiv.style.height = this._height;
this._bodyDiv.style.width = this._width;
this._bodyDiv.style.overflow = "scroll";
this._bodyDiv.appendChild(bodyTable);
this._bodyTable = bodyTable;
this._bodyTable.style.tableLayout = "fixed";
if(this._bodyCssClass && this._bodyCssClass.length > 0){
this._bodyTable.className = this._bodyCssClass;
}
}
function ComponentGeek$FreezeHeaderBehavior$adjustColWidth(){
/// <summary>列幅を一致させる</summary>
if(this._headerTable.rows.length == 0 || this._bodyTable.rows.length == 0) return;
var row = this._headerTable.rows(0); // ヘッダ
for(var i=0;i< row.cells.length;++i){
var hw = row.cells(i).offsetWidth;
var bw = this._bodyTable.rows(0).cells(i).offsetWidth;
if(hw > bw){
this._bodyTable.rows(0).cells(i).style.width = hw;
row.cells(i).style.width = hw;
}else{
this._bodyTable.rows(0).cells(i).style.width = bw;
row.cells(i).style.width = bw;
}
}
if(this._columnHeaderTable && this._columnHeaderRowTable){
var hrow = this._columnHeaderTable.rows(0); // ヘッダ
var drow = this._columnHeaderRowTable.rows(0);
for(var i=0;i< hrow.cells.length;++i){
var hw = hrow.cells(i).offsetWidth;
var dw = drow.cells(i).offsetWidth;
var w = "auto";
if(this._widthAdjustMode == ComponentGeek.FreezeHeaderAdjustWidthMode.Default){
if(hw > dw){
w = hw;
}else{
w = dw;
}
}else if(this._widthAdjustMode == ComponentGeek.FreezeHeaderAdjustWidthMode.Header){
w = hw;
}else if(this._widthAdjustMode == ComponentGeek.FreezeHeaderAdjustWidthMode.Data){
w = dw;
}
hrow.cells(i).style.width = w;
drow.cells(i).style.width = w;
}
}
}
function ComponentGeek$FreezeHeaderBehavior$adjustRowHeight(){
/// <summary>ヘッダの高さを大きいほうに一致させる</summary>
if(this._columnHeaderTable && this._headerTable){
var leftRow = this._columnHeaderTable.rows(0);
var rightRow = this._headerTable.rows(0);
var lh = leftRow.offsetHeight;
var rh = rightRow.offsetHeight;
if(lh > rh){
leftRow.style.height = lh;
rightRow.style.height = lh;
}else{
leftRow.style.height = rh;
rightRow.style.height = rh;
}
}
}
function ComponentGeek$FreezeHeaderBehavior$populateColumnHeader(){
if(!this._headerTable) return;
if(this._headerColumnCount == 0) return;
// create column header
this._columnHeaderDiv = document.createElement("div");
this._columnHeaderTable = this._headerTable.cloneNode(false);
var headerBody = document.createElement("tbody");
this._columnHeaderTable.appendChild(headerBody);
for(var i=0;i<this._headerTable.rows.length;++i){
var newRow = this._headerTable.rows(i).cloneNode(false);
headerBody.insertBefore(newRow);
//var newRow = headerBody.insertRow(i);
var orgRow = this._headerTable.rows(i);
for(var j = 0;j < this._headerColumnCount;++j){
var cell = orgRow.cells(0);
newRow.appendChild(cell);
}
}
this._columnHeaderTable.style.marginRight = 0;
this._columnHeaderDiv.appendChild(this._columnHeaderTable);
this._columnHeaderDiv.style.position = "relative";
}
function ComponentGeek$FreezeHeaderBehavior$populateColumnHeaderRow(){
if(!this._bodyTable) return;
if(this._headerColumnCount == 0) return;
// create column header
this._columnHeaderRowDiv = document.createElement("div");
this._columnHeaderRowTable = this._bodyTable.cloneNode(false);
var headerBody = document.createElement("tbody");
this._columnHeaderRowTable.appendChild(headerBody);
for(var i=0;i<this._bodyTable.rows.length;++i){
var newRow = this._bodyTable.rows(i).cloneNode(false);
headerBody.insertBefore(newRow);
var orgRow = this._bodyTable.rows(i);
for(var j=0;j<this._headerColumnCount;++j){
var cell = orgRow.cells(0);
newRow.appendChild(cell);
}
}
this._columnHeaderRowDiv.appendChild(this._columnHeaderRowTable);
this._columnHeaderRowDiv.style.height = this._height;
this._columnHeaderRowDiv.style.overflow = "hidden";
this._columnHeaderRowDiv.style.position = "relative";
this._columnHeaderRowTable.style.tableLayout = "fixed";
this._columnHeaderRowTable.style.marginBottom = this._hScrollBarHeight;
this._columnHeaderRowTable.style.marginRight = 0;
this._columnHeaderRowTable.id = this._columnHeaderRowTable.id + "_ColumnHeaderRow";
if(this._bodyCssClass && this._bodyCssClass.length > 0){
this._columnHeaderRowTable.className = this._bodyCssClass;
}
}
ComponentGeek.FreezeHeaderBehavior.registerClass('ComponentGeek.FreezeHeaderBehavior', Sys.UI.Behavior);
ComponentGeek.FreezeHeaderAdjustWidthMode = function(){};
ComponentGeek.FreezeHeaderAdjustWidthMode.prototype =
{
Default : 0, // サイズの大きいほうに合わせる
Header : 1, // ヘッダ行の幅に合わせる
Data : 2 // データ行の1行目の幅に合わせる
}
ComponentGeek.FreezeHeaderAdjustWidthMode.registerEnum("ComponentGeek.FreezeHeaderAdjustWidthMode");
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
3.3 AssemblyInfoの編集
プロジェクトのAssemblyInfo.cs(Properties内にあります)を開いて、末尾のWebResourceAttribute,ScriptResourceAttributeを今回編集したファイル名に一致するように既定で生成された内容を編集します。編集箇所を青色にしています。
....(略)
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: WebResource("ComponentGeek.FreezeHeaderExtender.FreezeHeaderBehavior.js", "text/javascript")]
[assembly: ScriptResource("ComponentGeek.FreezeHeaderExtender.FreezeHeaderBehavior.js",
"ComponentGeek.FreezeHeaderExtender.FreezeHeaderBehavior", "ComponentGeek.FreezeHeaderExtender.Resource")]
4. サンプルプロジェクトの作成
使用例を掲載します。テスト用のWebサイトを作成し、プロジェクトの参照に3で作成したプロジェクトを追加して、Default.aspxを表示したときにToolboxにFreezeHeaderExtenderが表示されるようにします。
あとは、次のようにDefault.aspx,Default.aspx.csを開いて編集します。
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Defaut.aspx.cs" Inherits="WebSite.Defaut" %>
<%@ Register Assembly="ComponentGeek.FreezeHeaderExtender" Namespace="ComponentGeek.FreezeHeaderExtender"
TagPrefix="cc1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
<style type="text/css">
.SelectStyle
{
background-color: Red;
}
.BackColor
{
background-color: #FFFFAA;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<asp:UpdatePanel runat="server" ID="pnlUpdate">
<ContentTemplate>
<div>
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="false" AutoGenerateSelectButton="false"
SelectedRowStyle-CssClass="SelectStyle" Style="display: none;">
<HeaderStyle BackColor="LightBlue" />
<RowStyle Height="20" CssClass="BackColor" />
<Columns>
<asp:CommandField ButtonType="Link" SelectText="選択" ShowSelectButton="true">
<HeaderStyle Width="50px" />
</asp:CommandField>
<asp:BoundField DataField="col1" HeaderText="1" SortExpression="col1">
<ItemStyle Wrap="true" />
<HeaderStyle Width="60px" />
</asp:BoundField>
<asp:BoundField DataField="col2" HeaderText="2" SortExpression="col1">
<ItemStyle Wrap="true" />
<HeaderStyle Width="80px" />
</asp:BoundField>
<asp:BoundField DataField="col3" HeaderText="3" SortExpression="col1">
<ItemStyle Wrap="true" />
<HeaderStyle Width="100px" />
</asp:BoundField>
</Columns>
</asp:GridView>
<cc1:FreezeHeaderExtender ID="FreezeHeaderExtender1" runat="server" TargetControlID="GridView1"
Width="200px" Height="100px" BodyCssClass="" HeaderColumnCount="1" HeaderRowCount="1" />
<br />
<asp:Button runat="server" ID="btnPb" Text="osite" />
</div>
</ContentTemplate>
</asp:UpdatePanel>
<textarea id="traceConsole" cols="100" rows="20"></textarea>
</form>
</body>
</html>
Default.aspx.cs
using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
namespace WebSite
{
public partial class Defaut : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//if (Page.IsPostBack)
//{
DataTable tbl = new DataTable();
tbl.Columns.Add("col1");
tbl.Columns.Add("col2");
tbl.Columns.Add("col3");
tbl.LoadDataRow(new object[] { "cell11", "cell12", "cell13" }, false);
tbl.LoadDataRow(new object[] { "cell21", "cell22", "cell23" }, false);
tbl.LoadDataRow(new object[] { "cell31", "cell32", "cell33" }, false);
tbl.LoadDataRow(new object[] { "cell41", "cell42", "cell43" }, false);
tbl.LoadDataRow(new object[] { "cell51", "cell52", "cell53" }, false);
tbl.LoadDataRow(new object[] { "cell61aaaaaaaaaa", "cell62aaaaaaaaaa", "cell63aaaaaaaaaa" }, false);
tbl.LoadDataRow(new object[] { "cell71", "cell72", "cell73" }, false);
tbl.LoadDataRow(new object[] { "cell81", "cell82", "cell83" }, false);
tbl.LoadDataRow(new object[] { "cell91", "cell92", "cell93" }, false);
tbl.AcceptChanges();
GridView1.DataSource = tbl;
GridView1.DataBind();
//}
}
}
}
5. まとめ
説明は以上です(今回も説明はほとんどしていませんが。)。突っ込み、アドバイス、要望大歓迎です。仕事でASP.NET AJAXを使った経験を活かして行列ヘッダ固定化を俺式ではありますが、作ってみました。お試しになられた場合、スタイルシートの設定によってうまく表示されないかも知れませんが、ご容赦ください。もし、コードの詳細な説明等が欲しいなどのご要望があれば、説明を加えたいと思います。
今後の実装としては、せっかく作成したテーブルの縦横幅変更機能を組み込むようにしてみようと思います。って行ヘッダ固定Extenderを作成したときも書いたことなんですけどね。
エクステンダーは掲載内容どおり作ればできるので、コンパイル済みdllのみをダウンロードできるようにしました。ここからダウンロードして下さい。
注意:エクステンダーを適用しても、一瞬標準サイズのテーブルが表示されます。その動きがいやな場合は、GridViewなどのスタイルをdisplayをnoneに設定しておき、スクリプトのcreateFreezeTableメソッドの最後で、displayのnoneを解除するように変更すれば、標準サイズのテーブルが表示されなくなります。
デバッグ情報を表示したいばあはidがtraceConsoleのtextareaをWebフォームに配置すると出力されます。
申し訳ありませんが、上記のCommentに個人情報が入っているので、削除お願い致します。