65.9K
CodeProject 正在变化。 阅读更多。
Home

GridView 增强和修复

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (19投票s)

2008年6月15日

CPOL

6分钟阅读

viewsIcon

76499

downloadIcon

394

增加功能、增强和修复:对列宽有更多控制,防止文本换行,格式化自动生成的列。

引言

你是否曾经需要在 GridView 中控制列宽,特别是当 GridView 的宽度相对于容器(例如:width="100%")时,列总是会将 GridView 的总宽度平分给它们?你只希望每列都占用足够的宽度来显示其内容,而剩余的宽度可以分配给最后一列,但这并没有奏效,是吗?你是否曾经尝试在 GridViewRowStyleHeaderStyle 中使用 Wrap="False",却惊讶地发现它并非在所有浏览器中都有效?本文旨在解决这两个问题,并且它在 IE 和 FF 中都有效。

此外,我还提出了一种在使用 GridView 中自动生成列时解决问题的方法。众所周知,GridView 有一个属性 AutoGenerateColumns,当设置为 true 时,它会自动从 DataSource 生成列。问题在于我们对这些生成列的控制有限(甚至没有)。例如,如果 DataSource 包含一个日期列,显示的数据将不会被格式化,而是显示 (1/1/2008 12:00:00 AM) 而不是只显示 (1/1/2008) 或 (01/01/2008) 等等...这是一个问题,因为我们无法(直接)更改格式以满足我们的要求。解决这个问题的最佳方法是继承 GridView 并根据我们的喜好自定义 GridView。对于那些(像我一样)不想在他们的项目中处理另一个额外引用的人,我还有另一种方法。

背景

我的解决方案依赖于 JavaScript 和 DOM。我正在将元素注入到现有的 HTML 中。代码并不复杂,你不需要被这些令人望而生畏的术语吓到。不过,你需要了解一些 JavaScript 以及 DOM(文档对象模型)的实际含义。如果你发现自己缺乏这些知识,花几分钟时间用 Google 搜索一下就能帮到你。

对于自动生成列的问题,我的解决方案依赖于反射来设置那些没有公共可访问性的成员的值。反射会带来性能损失,但在某些情况下是可以承受的。如果你发现我的解决方案性能较弱(我没有注意到,也没有测试过),那么请尝试替代方案:继承。

图示

以下是一些图片,展示了我的解决方案所实现的功能

这是没有我的解决方案时示例代码的外观

NoneFixed.jpg

这是列已格式化且未更改换行时的外观

WrappingNotFixed.jpg

这是列已格式化且标题换行已修复时的外观

WrappingFixedHeader.jpg

这是所有功能都启用时的样子(标题和数据行的换行都已修复)

AllFixed.jpg

这是未格式化时日期字段的显示方式

AutoGenColsNoFormatting.jpg

这是应用格式化后日期字段的显示方式

AutoGenColsWithFormatting.jpg

列宽和换行

ColumnWidthsAndWrapping.aspx

解决方案分为两部分:JavaScript 位于 ASPX 页面中,注册这些函数调用的代码位于该页面的代码隐藏文件中。

让我们从 JavaScript 函数开始

function formatGrid(el)
{
    el = document.getElementById(el);               
    var numOfCols = 
      el.getElementsByTagName("TR")[1].getElementsByTagName("TD").length;                
    var colGrp = document.createElement("colgroup");                
    var col = document.createElement("col");
    col.setAttribute("span", numOfCols-1);
    col.setAttribute("width", "1");
    col.setAttribute("white-space", "nowrap");
    col.setAttribute("padding-left", "2");
    col.setAttribute("padding-right", "2");
    col.setAttribute("border-width", "2");
    colGrp.appendChild(col);                
    el.insertBefore(colGrp, el.firstChild);                
}

函数 formatGrid 接收文档中元素的 ID,在我们的例子中通常是 GridView。此函数的作用是格式化 GridView,使每列获得足够的宽度来显示其内容,并且**最后一**列获得所有剩余的宽度。请记住,我们通常在这里处理宽度为 100% 的 GridView。代码相对清晰,我将只指出以下几点

  1. 我们正在获取 GridView 中的列数,numOfCols,并在后面使用 numOfCols - 1
  2. ColGroup 元素负责对列进行分组并为其分配通用属性。
  3. Col 元素是 ColGroup 元素的子元素,它将覆盖在 ColGroup 元素上设置的内容,这就是为什么在 Col 元素上设置属性就足够了(我使用的是简单术语,欲了解更多信息:请查看 http://www.w3.org/TR/html401/struct/tables.html#h-11.2.4.1)。
  4. span 属性指示 Col 元素将影响多少列;在我们的例子中,我们需要限制除最后一列之外的所有列的宽度。
  5. 唯一必需的属性是 spanwidth 属性,其他属性是可选的。

以下函数解决了换行问题,我们可能需要 GridView 标题中的“名字”等文本显示在同一行,而不是换行并分成两行。同样,这也适用于数据行,当数据很长时,可能会被迫换到另一行,从而分成两行或更多行。我们可能需要它们作为整体显示在一行中。现在,使用以下函数,实现起来更容易

function addNoWrapSpan(el, noWrap)
{
    var span = document.createElement("span");
    while(el.childNodes.length > 0)
    {
        var child = el.firstChild;
        el.removeChild(child);
        span.appendChild(child);
    }                
    if(noWrap)
    span.style.whiteSpace = "nowrap";
    else
    span.style.whiteSpace = "inherit";
    el.appendChild(span);
}
function setNoWrap(el, noWrapTH, noWrapTD)
{
    el = document.getElementById(el);
    if(noWrapTH)
    {
        var allTHs = el.getElementsByTagName("TH");                
        for(var i = 0; i < allTHs.length; i++)
        {
            addNoWrapSpan(allTHs[i], noWrapTH);
        }
    }
    if(noWrapTD)
    {
        var allTDs = el.getElementsByTagName("TD");                
        for(var i = 0; i < allTDs.length; i++)
        {
            addNoWrapSpan(allTDs[i], noWrapTD);
        }
    }
}

第一个函数 addNoWrapSpan 将提供的元素 el(无论是 td 还是 th)内的所有元素移动到一个动态生成的 span 元素中,该元素根据 noWrap 是 true 还是 false 具有适当的样式。使用的样式是

span.style.whiteSpace = "nowrap"; 

如果 noWrap 为 true,则设置以下样式,如果为 false,则设置以下样式

span.style.whiteSpace = "inherit";

setNoWrap 接收三个参数:el,即我们要配置的元素;noWrapTH,告诉我们是否允许子 TH 换行;以及 noWrapTD,指示是否允许元素 el 内部的 TD 换行。正如你所注意到的,setNoWrap 会在必要时调用 addNoWrapSpan

所附示例项目包含一个示例 GridView

<asp:GridView ID="GridView1" runat="server" 
          AutoGenerateColumns="False" Width="100%" 
          CssClass="GridClass">
    <Columns>
        <asp:BoundField DataField="FirstName" HeaderText="First Name" />
        <asp:BoundField DataField="LastName" HeaderText="Last Name" />               
        <asp:BoundField DataField="Age" HeaderText="Age" />
        <asp:CommandField ShowSelectButton="True" />
    </Columns>
</asp:GridView>

它只是显示代码隐藏中设置的数据,并且还有一个带有选择 LinkButtonCommandField。现在,代码隐藏如下

private DataTable BuildDataSource()
{
    DataTable dt = new DataTable();
    dt.Columns.Add("FirstName", typeof(string));
    dt.Columns.Add("LastName", typeof(string));
    dt.Columns.Add("Age", typeof(int));
    
    DataRow dr;
    
    dr = dt.NewRow();
    dr["FirstName"] = "John the first";
    dr["LastName"] = "Doe";
    dr["Age"] = 23;
    dt.Rows.Add(dr);

    dr = dt.NewRow();
    dr["FirstName"] = "Clark";
    dr["LastName"] = "Kent";
    dr["Age"] = 28;
    dt.Rows.Add(dr);

    return dt;
}

这是一种用于测试目的的简单数据显示方式;在实际场景中,这将替换为从数据库检索数据的代码

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
    string formatScript = "";
    formatScript += string.Format("formatGrid('{0}');" + 
                    "setNoWrap('{0}', false, true);", 
                    GridView1.ClientID);        
    ClientScript.RegisterStartupScript
        (this.GetType(), "format", formatScript, true);
}

这是我们的 JavaScript 被投入使用的地方。请注意,我正在调用 setNoWrap 并为 noWrapTH 参数分配 false;这意味着如果其内容很长,标题仍将换行,你可能需要将其更改为 true

自动生成列

AutoGeneratedColumns.aspx

这里我描述了一种允许我们在自动生成的列中格式化日期的方法(AutoGenerateColumns =true)。

private void FormatDatesInGridView(GridView gv, GridViewRow gvr)
{
    if (gv.DataSource != null)
    {
        DataTable dt = null;

        if (gv.DataSource is DataView)
        {
            dt = ((DataView)gv.DataSource).Table;
        }
        else
            if (gv.DataSource is DataSet)
            {
                dt = ((DataSet)gv.DataSource).Tables[0];
            }
            else
                if (gv.DataSource is DataTable)
                {
                    dt = (DataTable)gv.DataSource;
                }

        if (gvr.RowType == DataControlRowType.DataRow)
        {
            foreach (TableCell tc in gvr.Cells)
            {
                DataControlFieldCell dcfc = (DataControlFieldCell)tc;
                if (dcfc.ContainingField is AutoGeneratedField)
                {
                    AutoGeneratedField agf = (AutoGeneratedField)dcfc.ContainingField;
                    if (agf.DataType == typeof(DateTime))
                    {
                        System.Reflection.FieldInfo fi;
                        fi = typeof(BoundField).GetField("_dataFormatString", 
                             System.Reflection.BindingFlags.Instance | 
                             System.Reflection.BindingFlags.NonPublic);
                        fi.SetValue(agf, "{0:dd/MM/yyyy}");

                        fi = typeof(DataControlField).GetField("_statebag", 
                             System.Reflection.BindingFlags.Instance | 
                             System.Reflection.BindingFlags.NonPublic);
                        ((StateBag)fi.GetValue(agf))["DataFormatString"] = 
                                              "{0:dd/MM/yyyy}";

                        System.Reflection.MethodInfo mi;
                        mi = typeof(BoundField).GetMethod("OnFieldChanged", 
                             System.Reflection.BindingFlags.NonPublic | 
                             System.Reflection.BindingFlags.Instance);
                        mi.Invoke(agf, null);
                        ((BoundField)agf).HtmlEncode = false; //Fix for Dates Formatting
                    }
                }
            }
        }
    }
}

名为 FormatDatesInGridView 的方法将从要格式化的 GridViewRowCreated 事件处理程序内部调用。此方法接收两个参数:GridView 和当前行。调用事件处理程序将如下所示

protected void GridView1_RowCreated(object sender, GridViewRowEventArgs e)
{
    if(e.Row.RowType == DataControlRowType.DataRow)
        FormatDatesInGridView(GridView1, e.Row);
}

该方法首先确定 GridViewDataSource(如果你有其他类型作为 DataSource,可以修改)。然后,它检查当前 GridViewRow 的所有单元格,如果 AutoGeneratedFieldContainingField 类型为 DateTime,我们使用反射执行以下操作

  • _dataFormatString 设置为 "{0:dd/MM/yyyy}"(可以根据需要更改)。
  • 通过设置 _statebag["DataFormatString"] 的值,将 Viewstate["DataFormatString"] 设置为相同的值 "{0:dd/MM/yyyy}"
  • 调用 OnFieldChanged 方法以应用这些更改(此方法最终会导致绑定发生)。
  • 更新:显然,HtmlEncode 在某些环境中有时为 true,这阻止了我们控制日期的格式。这就是为什么我添加了将其设置为 false 的行。

就这样,日期字段现在已格式化。我如何想出这些步骤是通过使用 Reflector 并检查类如何在内部工作。

更新历史

  • 2009 年 2 月 12 日:日期格式化的 HtmlEncode 修复。
  • 2008 年 8 月 3 日:自动生成列。
  • 2008 年 6 月 22 日:添加了插图部分。
© . All rights reserved.