After playing around with ASP.NET 2.0's GridView control I was ready to put it to use.  Problem was, ASP.NET 2.0's release will be...well when it's ready.  In the interest of not having to write code for each datagrid's sorting and paging, I came up with the following.  It works server-side, maintains it's state (even with a delete or modification that requires you to rebind the grid, and is a snap to implement.  I have to agree that DataGrid's ViewState takes up some bloat in the page, but if you don't care...maybe modify this below to use querystrings or something.

You can pretty much copy and paste what's below, but I will explain the important parts below the code.  (Note: I just pretty much copy and pasted directly from my own working code so if you use you'll probably want to put in your own comments :-P )

    
    1 using System;
    2 using System.Web.UI.WebControls;
    3 using System.Web;
    4 
    5 namespace SATK.Web.UI.WebControls
    6 {
    7   /// <summary>
    8   /// <hr />
    9   /// <para>
   10   /// This data grid is customized for our specific
   11   /// look and feel.  It also extends the functionality
   12   /// with security implementation, empty data
   13   /// messages and other goodies.
   14   /// </para>
   15   /// <hr />
   16   /// <h1>Release history</h1>
   17   /// <code>
   18   /// DEVELOPER VERSION DATE        DESCRIPTION
   19   /// W.Tilton  1.0    12/20/2004  Initial Creation
   20   ///  W.Tilton  1.1    01/18/2005  Added whereClause to binding delegate
   21   ///  W.Tilton  1.2    03/08/2005  Added ability to have more than one grid per page
   22   ///  </code>
   23   ///  <hr />
   24   /// </summary>
   25   public class DataGrid : System.Web.UI.WebControls.DataGrid
   26   {
   27     private string _emptyDataMessage, _heading;
   28     private bool _isPaged = true;
   29     private int _pageSize = 20;
   30     /// <summary>
   31     /// A delegate that must be set in order for this object to function properly.  Specifies which helper method
   32     /// will populate (bind) data in the given datagrid.
   33     /// </summary>
   34     public delegate void BindDelegate(Web.UI.WebControls.DataGrid dg, string whereClause, string orderByColumns, bool isAscending);
   35 
   36     private BindDelegate bind;
   37 
   38     /// <summary>
   39     /// This method tells the datagrid object which method the system should use to bind the instance of a grid that you have
   40     /// created.
   41     /// <see cref="Web.UI.WebControls.DataGrid.BindDelegate"/>
   42     /// </summary>
   43     /// <param name="bd">Web.UI.WebControls.DataGrid.BindDelegate</param>
   44     /// <example>
   45       /// <code>
   46       ///    dg.SetBindDelegate(new Web.UI.WebControls.DataGrid.BindDelegate(Web.UI.Helpers.MyHelper.Bind));
   47       /// </code>
   48       /// <code>
   49       ///    dg.SetBindDelegate(new Web.UI.WebControls.DataGrid.BindDelegate(Method.That.Will.Bind.This.DataGrid));
   50       /// </code>
   51     /// </example>
   52     public void SetBindDelegate(BindDelegate bd)
   53     {
   54       bind = bd;
   55     }
   56     /// <summary>
   57     /// The amount of records that are displayed on the grid.
   58     /// </summary>
   59     /// <remarks>Defaults to 20.</remarks>
   60     public override int PageSize
   61     {
   62       get
   63       {
   64         return this._pageSize;
   65       }
   66       set
   67       {
   68         this._pageSize = value;
   69       }
   70     }
   71 
   72 
   73     /// <summary>
   74     /// The message that is displayed when their are no items in the datagrid.
   75     /// </summary>
   76     /// <example>
   77     ///    <code>&lt;satk:DataGrid runat="Server" EmptyDataMessage="No records found!"&gt;&lt;/satk:DataGrid&gt;</code>
   78     /// </example>
   79     public string EmptyDataMessage
   80     {
   81       get
   82       {
   83         if(this._emptyDataMessage==null)
   84           return "";
   85         else
   86           return this._emptyDataMessage;
   87       }
   88       set { this._emptyDataMessage = value; }
   89     }
   90     /// <summary>
   91     ///
   92     /// </summary>
   93     public bool IsPaged
   94     {
   95       get { return this._isPaged; }
   96       set { this._isPaged = value; }
   97     }
   98     /// <summary>
   99     ///
  100     /// </summary>
  101     public string Heading
  102     {
  103       get
  104       {
  105         if(this._heading==null)
  106           return "";
  107         else
  108           return this._heading;
  109       }
  110       set { this._heading = value; }
  111     }
  112     /// <summary>
  113     /// Customized data grid, with secure columns
  114     /// </summary>
  115     public DataGrid()
  116     {
  117       this.Width = new Unit(100d,UnitType.Percentage);
  118       this.CellPadding = 3;
  119       this.AutoGenerateColumns = false;
  120       this.CssClass = "datagrid";
  121 
  122       this.AllowSorting = true;
  123 
  124       this.HeaderStyle.CssClass = "th";
  125       this.AlternatingItemStyle.CssClass = "alt";
  126       this.SelectedItemStyle.CssClass = "selected";
  127 
  128       if(this.IsPaged)
  129       {
  130         this.AllowPaging = true;
  131         this.PagerStyle.Mode = PagerMode.NumericPages;
  132         this.PagerStyle.CssClass = "pages";
  133         this.PagerStyle.HorizontalAlign = System.Web.UI.WebControls.HorizontalAlign.Right;
  134       }
  135     }
  136     /// <summary>
  137     /// Bind the grid using existing values
  138     /// </summary>
  139     /// <param name="b">A delegate that is used to bind the specific information.</param>
  140     public void BindGrid(BindDelegate b)
  141     {
  142       if(ViewState[this.UniqueID+"WhereClause"]==null)
  143         ViewState[this.UniqueID+"WhereClause"] = "1=1";
  144 
  145       if(ViewState[this.UniqueID+"SortExpression"]==null)
  146         ViewState[this.UniqueID+"SortExpression"] = "1";
  147 
  148       if(ViewState[this.UniqueID+"SortOrder"]==null)
  149         ViewState[this.UniqueID+"SortOrder"] = true;
  150 
  151       HttpContext.Current.Trace.Warn("Sort Order: " + ViewState[this.UniqueID+"SortOrder"]);
  152       HttpContext.Current.Trace.Warn("Sort Expression: " + ViewState[this.UniqueID+"SortExpression"]);
  153       HttpContext.Current.Trace.Warn("Where Clause: " + ViewState[this.UniqueID+"WhereClause"]);     
  154 
  155       b(this,ViewState[this.UniqueID+"WhereClause"].ToString(),ViewState[this.UniqueID+"SortExpression"].ToString(),Convert.ToBoolean(ViewState[this.UniqueID+"SortOrder"]));
  156     }
  157     /// <summary>
  158     /// Takes the given values and passes it to the given Helper method that will bind this datagrid.
  159     /// </summary>
  160     /// <param name="b"></param>
  161     /// <param name="whereClause"></param>
  162     /// <param name="orderByColumns"></param>
  163     /// <param name="isAscending"></param>
  164     public void BindGrid(BindDelegate b, string whereClause, string orderByColumns, bool isAscending)
  165     {
  166       if(whereClause==null)
  167         ViewState[this.UniqueID+"WhereClause"] = "1=1";
  168       else
  169         ViewState[this.UniqueID+"WhereClause"] = whereClause;
  170 
  171       if(orderByColumns==null)
  172         ViewState[this.UniqueID+"SortExpression"] = "1";
  173       else
  174         ViewState[this.UniqueID+"SortExpression"] = orderByColumns;
  175 
  176       ViewState[this.UniqueID+"SortOrder"] = isAscending;
  177 
  178       HttpContext.Current.Trace.Warn("Sort Order: " + isAscending);
  179       HttpContext.Current.Trace.Warn("Sort Expression: " + ViewState[this.UniqueID+"SortExpression"]);
  180       HttpContext.Current.Trace.Warn("Where Clause: " + ViewState[this.UniqueID+"WhereClause"]);     
  181 
  182       b(this,ViewState[this.UniqueID+"WhereClause"].ToString(),ViewState[this.UniqueID+"SortExpression"].ToString(),Convert.ToBoolean(ViewState[this.UniqueID+"SortOrder"]));
  183     }
  184     /// <summary>
  185     ///
  186     /// </summary>
  187     /// <param name="writer"></param>
  188     protected override void Render(System.Web.UI.HtmlTextWriter writer)
  189     {
  190       writer.Write("<h1>" + this.Heading + "</h1>");
  191       if(this.Items.Count==0)
  192       {
  193         writer.Write(this.EmptyDataMessage);
  194       }
  195       else
  196       {
  197         base.Render (writer);
  198       }
  199     }
  200 
  201     /// <summary>Event that fires when a user has specified a sort order.</summary>
  202     /// <param name="e"></param>
  203     protected override void OnSortCommand(DataGridSortCommandEventArgs e)
  204     {
  205       // Switch it up, the sort command was clicked so we need to do opposite sorting that we had prior
  206       ViewState[this.UniqueID+"SortOrder"] = !Convert.ToBoolean(ViewState[this.UniqueID+"SortOrder"]);
  207 
  208       // Grab the sort expression, so that if the index changes we'll keep the sort order
  209       ViewState[this.UniqueID+"SortExpression"] = e.SortExpression;
  210 
  211       this.SortGrid(Convert.ToBoolean(ViewState[this.UniqueID+"SortOrder"]));
  212       base.OnSortCommand (e);
  213     }
  214     /// <summary>Event that fires when a user has specified which page they would like to view within paged records.</summary>
  215     /// <param name="e"></param>
  216     protected override void OnPageIndexChanged(DataGridPageChangedEventArgs e)
  217     {
  218       this.CurrentPageIndex = e.NewPageIndex;
  219 
  220       this.SortGrid(Convert.ToBoolean(ViewState[this.UniqueID+"SortOrder"]));
  221       base.OnPageIndexChanged (e);
  222     }   
  223     private void SortGrid(bool isAscending)
  224     {
  225       if(ViewState[this.UniqueID+"WhereClause"]==null)
  226         ViewState[this.UniqueID+"WhereClause"] = "1=1";
  227 
  228       if(ViewState[this.UniqueID+"SortExpression"]==null)
  229         ViewState[this.UniqueID+"SortExpression"] = "1";
  230 
  231       if(ViewState[this.UniqueID+"SortOrder"]==null)
  232         ViewState[this.UniqueID+"SortOrder"] = true;
  233 
  234       HttpContext.Current.Trace.Warn("Sort Order: " + isAscending);
  235       HttpContext.Current.Trace.Warn("Sort Expression: " + ViewState[this.UniqueID+"SortExpression"]);
  236       HttpContext.Current.Trace.Warn("Where Clause: " + ViewState[this.UniqueID+"WhereClause"]);
  237       this.BindGrid(bind,ViewState[this.UniqueID+"WhereClause"].ToString(),ViewState[this.UniqueID+"SortExpression"].ToString(),isAscending);
  238     }
  239   }
  240 }
 
This extended datagrid allows for me to quickly create a new datagrid that has the same look and feel as the rest.  If I want to change the look and feel, I just change the CSS and done!  Binding data to the grid is accomplished in a few steps (takes me 2 minutes due to the fact that I have great CodeSmith templates that work in conjunction with this control).
 
First I have to develop a method that I can set my delegate to that will bind the correct data to the grid.  I have a presentation business layer type class UI.Helper that helps me with this.  Basically, its a method that takes in the DataGrid we're working with and binds data to it.
 
   19 
   20     /// Binds users to a datagrid
   21     ///
   22     ///
   23     ///
   24     ///
   25     public static void Bind(SATK.Web.UI.WebControls.DataGrid dg, string whereClause, string orderByColumns, bool isAscending)
   26     {
   27         dg.DataSource = SATK.BusinessLogic.User.LoadCollection(whereClause,orderByColumns.Split(','),isAscending);
   28         dg.DataBind();
   29      }
 
Now on my page I will need to set the datagrid's Bind method by using the delegate.
 

   15     ///
   16     protected SATK.Web.UI.WebControls.DataGrid dgUsers;
...
   23     private SATK.Web.UI.WebControls.DataGrid.BindDelegate bd;
   24 
   25     private void Page_Load(object sender, System.EventArgs e)
   26     {
...
   32       bd = new SATK.Web.UI.WebControls.DataGrid.BindDelegate(SATK.Web.UI.Helpers.UserHelper.Bind);
   33       dgUsers.SetBindDelegate(bd);
 
then, we'll just need to Bind the grid.
 
   38       if(!Page.IsPostBack)
   39       {
...
   42         dgUsers.BindGrid(bd,null,"DateAccountCreated",true);
   43       }
 
On the HTML page you work it just like you would a DataGrid (I've also created my own custom columns, you can use regular asp:BoundColumn, TemplateColumn etc):
 
<%@ Register TagPrefix="satk" Namespace="SATK.Web.UI.WebControls" Assembly="SATK.Web.UI" %>
<%@ Register TagPrefix="satk" Namespace="SATK.Web.UI.WebControls" Assembly="SATK.Web.UI" %>
...
<satk:DataGrid Runat="server" ID="dgUsers" EmptyDataMessage="No users found.">
 <Columns>
  <satk:BoundColumn SortExpression="FirstName" DataField="FirstName" HeaderText="FirstName" />
  <satk:BoundColumn DataField="LastName" SortExpression="LastName" HeaderText="LastName" />
  <satk:BoundColumn DataField="Username" SortExpression="Username" HeaderText="Username" />
 </Columns> 
</satk:DataGrid>
 
And that's it.  Honestly, there is a bit of code above, but once do it once, you'll be able to create sortable, pagable grids quickly.  If you have something different setup for your business layer objects, feel free to modify the delegate and the other code and you'll have your own custom grid.