GridView 高级技术

汇总脚注

       GridView 的主要目标是显示一组记录,但是你还可以加入一些有趣的信息,如汇总数据。需要如下的操作:

  • 设置 GridView.ShowFooter 为 true ,这样可以显示脚注行(但没有数据)
  • 在 GridView.FooterRow 中加入内容

       本例假设正在处理产品列表,一个简单的汇总可以显示产品总价或均价。第一步是确定何时计算这个信息。如果正使用手工绑定(DataSource),那么可以在数据对象绑定到 GridView 之间就读取它的值并进行计算。但如果使用的是声明性绑定(DataSourceID),就要借助别的技术了。

<asp:SqlDataSource ID="sourceProducts" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
    ProviderName="System.Data.SqlClient" SelectCommand="select ProductID,ProductName,UnitPrice,UnitsInStock from Products">
</asp:SqlDataSource>
<asp:GridView ID="gridProducts" runat="server" DataSourceID="sourceProducts" AllowPaging="true"
    ShowFooter="true" OnDataBound="gridProducts_DataBound">
</asp:GridView>

       为了填充脚注,代码要响应 GridView.DataBound 事件,这个事件在 GridView 填充数据后立刻发生。在这之后,你将不能再访问数据源,但是可以遍历 GridView 的行与列的集合来进行计算和操作脚注:

protected void gridProducts_DataBound(object sender, EventArgs e)
{
    decimal valueInStock = 0;
    foreach (GridViewRow row in gridProducts.Rows)
    {
        decimal price = Decimal.Parse(row.Cells[2].Text);
        int unitInStock = Int32.Parse(row.Cells[3].Text);
        valueInStock += price * unitInStock;
    }
 
    GridViewRow footer = gridProducts.FooterRow;
    footer.Cells[0].ColumnSpan = 3;
    footer.Cells[0].HorizontalAlign = HorizontalAlign.Center;
 
    footer.Cells.RemoveAt(2);
    footer.Cells.RemoveAt(1);
 
    footer.Cells[0].Text = "Total value in stock (on this page): " + valueInStock.ToString("C");
}

       汇总信息与网格的其它行拥有相同数目的列,因此需要设置单元格的 ColumnSpan 属性并移除多余的列。

image

 

 

单个表中的父/子视图

         有时候你希望创建一个父子报表,它按父记录组织的同时显示所有子记录。例如,你可以创建一个完整的按类别组织的产品列表。使用的基本技术就是嵌套 GridView。子 GridView 控件使用 TemplateField 来插入到父 GridView 中。唯一要注意的是,不能在绑定父 GridView 的同时绑定子 GridView,因此此时父行还没有被创建。要等待父 GridView 的 RowDataBound 事件发生。

<asp:GridView ID="gridMaster" runat="server" DataSourceID="sourceCategoies" 
    DataKeyNames="CategoryID" onrowdatabound="gridMaster_RowDataBound"
    AutoGenerateColumns="false">
    <Columns>
        <asp:TemplateField HeaderText="Category">
            <ItemStyle VerticalAlign="Top" Width="20%" />
            <ItemTemplate>
                <br />
<b><%
   1: # Eval("CategoryName") 
%></b>
                <br /><br />
<%
   1: # Eval("Description") 
%>
                <br />
            </ItemTemplate>                
        </asp:TemplateField>
 
        <asp:TemplateField HeaderText="Products">
            <ItemStyle VerticalAlign="Top" />
            <ItemTemplate>
                <asp:GridView ID="gridChild" runat="server" AutoGenerateColumns="false">
                    <HeaderStyle BackColor="Silver" />
                    <Columns>
                        <asp:BoundField DataField="ProductName" HeaderText="Product Name" ItemStyle-Width="400px" />
                        <asp:BoundField DataField="UnitPrice" HeaderText="Unit Price" DataFormatString="{0:C}" />
                    </Columns>
                </asp:GridView>
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>
 
<asp:SqlDataSource ID="sourceCategoies" runat="server" 
    ConnectionString="<%$ ConnectionStrings:Northwind %>" 
    ProviderName="System.Data.SqlClient"
    SelectCommand="select * from Categories">
</asp:SqlDataSource>
 
<asp:SqlDataSource ID="sourceProducts" runat="server" 
    ConnectionString="<%$ ConnectionStrings:Northwind %>" 
    ProviderName="System.Data.SqlClient"
    SelectCommand="select * from products where CategoryID=@CategoryID">
    <SelectParameters>
        <asp:Parameter Name="CategoryID" Type="Int32" />
    </SelectParameters>
</asp:SqlDataSource>
protected void gridMaster_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        GridView gridChild = e.Row.Cells[1].Controls[1] as GridView;
 
        // 获取一个 DataKey 对象集合,这个对象表示控件中的每一行的数据键值
        string cateID = gridMaster.DataKeys[e.Row.DataItemIndex].Value.ToString();
 
        // 设置子 GridView 的数据源参数
        sourceProducts.SelectParameters[0].DefaultValue = cateID;
 
        // 执行 Select()
        object data = sourceProducts.Select(DataSourceSelectArguments.Empty);
        gridChild.DataSource = data;
        gridChild.DataBind();
    }
}

image

解释:

  • 父 GridView 定义了2个列,第一个列组合了类别名称和类别描述,第二个列嵌套了一个子 GridView
  • 子 GridView 并没有设置 DataSourceID 属性,那是因为当父网格被绑定到它的数据源时,子网格的数据都要通过编程来提供
  • 为了绑定子网格,必须响应父网格的 RowDataBound 事件。

 

处理来自数据库的图片

       一个基本的问题是,为了在 HTML 页面上显示图片,需要加入一个图片标签并通过 src 特性链接到一个独立的图像文件上:

<img src="myfile.gif" alt="My Image">

       遗憾的是,如果你希望动态的显示图片,这不会有太大的帮助。虽然可以在代码内设置 src 特性,但是没有办法通过编程设置图片的内容。可以把数据保存为图片文件,然后放到 Web 服务器的硬盘上,但这一过程非常缓慢,浪费空间,而且同时有多个请求且视图写入到同一个文件时还有可能出现并发访问错误。

       有2个办法解决这一问题:

  • 把图片保存到单独的文件中,数据库只需保存文件名,这样用文件名就可以与服务器端的文件进行绑定。
  • 使用一个独立的 ASP.NET 资源,它直接返回二进制数据。为了处理这一任务,需要跳出数据绑定,编写自制的 ADO.NET 代码。

1. 显示二进制数据

       ASP.NET 并没有限制只返回 HTML 内容,实际上可以用 Response.BinaryWrite()返回原始字节而完全忽略网页模型。

       下面的示例通过这项技术访问pubs数据库的 pub_info 表中 logo 字段的二进制图片数据:

protected void Page_Load(object sender, EventArgs e)
{
    string conStr = WebConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;
    SqlConnection conn = new SqlConnection(conStr);
    string sql = "select logo from pub_info where pub_id='1389'";
    SqlCommand cmd = new SqlCommand(sql, conn);
    try
    {
        conn.Open();
        SqlDataReader reader = cmd.ExecuteReader();
        if (reader.Read())
        {
            byte[] bytes = (byte[])reader["logo"];
            Response.ContentType = "image";
            Response.BinaryWrite(bytes);
        }
        reader.Close();
    }
    finally
    {
        conn.Close();
    }
}

image

       使用 BinaryWrite()时,其实已经退出了网页模型,如果你在页面上添加了其他的控件,它们将不会出现。

 

2. 高效读取二进制数据

       二进制数据很容易就变的很大。如果面临的是一个大型的图片文件,先前的示例性能将变得很糟!问题在于它使用了 DataReader,DataReader 每次在内存中加载一整条记录(这比 DataSet 好一些,它一次将结果集全部加载到内存),这对于大型字段来说还是不太理想。

       没有理由把 2M 或更大的图片一次性加载到内存中。一个更好的办法是一段一段的读取并通过 Response.BinaryWrite()逐步写到输出流中。幸好,DataReader 的顺序访问功能支持这一设计。只需将 CommandBehavior.SequentialAccess 的值传给 Command.ExecuteReader()方法即可,然后就可以通过 DataReader.GetBytes()方法来逐块访问行。

protected void Page_Load(object sender, EventArgs e)
{
    string conStr = WebConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;
    SqlConnection conn = new SqlConnection(conStr);
    string sql = "select logo from pub_info where pub_id='1389'";
    SqlCommand cmd = new SqlCommand(sql, conn);
    try
    {
        conn.Open();
 
        // 提供一种方法,以便 DataReader 处理包含带有大二进制值的列的行。
        // SequentialAccess 不是加载整行,而是使 DataReader 将数据作为流来加载。
        // 然后可以使用 GetBytes 或 GetChars 方法来指定开始读取操作的字节位置以及正在返回的数据的有限的缓冲区大小。        
        SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
        if (reader.Read())
        {
            int bufferSize = 100;                   // Size of the buffer.
            byte[] bytes = new byte[bufferSize];    // The buffer of data.
            long bytesRead;                         // The number of bytes read.
            long readFrom = 0;                      // The Starting index.
            Response.ContentType = "image";
 
            // Read the field 100 bytes at a time
            do
            {
                // 5个参数的解释
                // 参数1: 从零开始的列序号。(读取行中的哪一列)
                // 参数2: 字段中的索引,从其开始读取操作。(第一次是0,以后递增bufferSize)
                // 参数3: 要将字节流读入的缓冲区。
                // 参数4: buffer 中写入操作开始位置的索引。
                // 参数5: 要复制到缓冲区中的最大长度。
                bytesRead = reader.GetBytes(0, readFrom, bytes, 0, bufferSize);
                Response.BinaryWrite(bytes);
                readFrom += bufferSize;
            } while (bytesRead == bufferSize); // GetBytes()返回的字节数如果!=100,说明已经是最后一次读取了
        }
        reader.Close();
    }
    finally
    {
        conn.Close();
    }
}

       使用顺序访问时,你必须记住几个限制:

  • 必须以只进流的方式读取数据,读取某一块数据后,自动在流中前进,不可用回头访问
  • 必须按字段在查询中的次序读取字段,假如你先访问了第3个字段,就不能够再访问前2个字段。

3. 将图片和其他内容整合

       如果需要将图片数据和其他控件或 HTML 整合,Response.BinaryWrite()会带来一定的难度。因为使用 BinaryWrite()方法返回原始图像数据时,也就失去了加入额外 HTML 内容的能力。

       为了攻克这一难题,你需要再创建一个页面,由它负责调用生成图片的代码。最好的办法是用一个专门的 HTTP 处理程序产生图片输出从而代替原来的图像生成页面。这个方式让你可以避免完整的 ASP.NET Web 窗体模型带来的负担,因为这里你根本不需要使用它们。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Configuration;
using System.Data;
using System.Data.SqlClient;
 
/// <summary>
///ImageFromDB 的摘要说明
/// </summary>
public class ImageFromDB : IHttpHandler
{
    public ImageFromDB()
    {
    }
 
    #region IHttpHandler 成员
 
    public bool IsReusable
    {
        get { return true; }
    }
 
    public void ProcessRequest(HttpContext context)
    {
        // Get the ID from this request
        string id = context.Request.QueryString["id"];
        if (id == null)
        {
            throw new ApplicationException("Must Specify ID.");
        }
 
        string conStr = WebConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;
        SqlConnection conn = new SqlConnection(conStr);
 
        // 这里最好使用参数化 SQL 
        string sql = "select logo from pub_info where pub_id=@ID";
        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@ID", id);
 
        try
        {
            conn.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
            if (reader.Read())
            {
                int bufferSize = 100;
                byte[] bytes = new byte[bufferSize];
                long bytesRead;
                long readFrom = 0;
                context.Response.ContentType = "image";
 
                do
                {
                    bytesRead = reader.GetBytes(0, readFrom, bytes, 0, bufferSize);
                    context.Response.BinaryWrite(bytes);
                    readFrom += bufferSize;
                } while (bytesRead == bufferSize);
            }
            reader.Close();
        }
        finally
        {
            conn.Close();
        }
    }
 
    #endregion
}

       不要忘记在 web.config 文件中进行注册:

<add verb="GET" path="SempleImageRender.aspx" type="ImageFromDB"/>

       测试:

<asp:GridView ID="GridView1" runat="server" DataSourceID="SqlDataSource1" GridLines="None"
    AutoGenerateColumns="false">
    <Columns>
        <asp:TemplateField>
            <ItemTemplate>
                <table border="1">
                    <tr>
                        <td>
                            <img src='SempleImageRender.aspx?id=<%# Eval("pub_id") %>' />
                        </td>
                    </tr>
                </table>
                <%# Eval("pub_name") %><br />
                <%# Eval("City") %>,
                <%# Eval("state") %>,
                <%# Eval("Country") %><br />
                <br />
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:Pubs %>"
    ProviderName="System.Data.SqlClient" SelectCommand="select * from publishers">
</asp:SqlDataSource>

       效果:

image

       同时在一个列表控件显示所有发行商的图片时,这个方法效率就不高了,因为每次读取图片时都要单独请求一次 HTTP 处理程序(单独访问数据库)。

       可以创建另一个 HTTP 处理程序来解决,该程序在从数据库读取图片前先检查缓存中该图片是否存在。绑定到 GridView 前,你可以先执行返回所有记录的图片数据的查询,并把所有图片保存到缓存中。

 

 

探测并发冲突

       如果 Web 程序允许多人同时更新,很可能会发生两个甚至多个编辑交叠的情形,很可能会用旧的数据覆盖数据库。为了避免这一问题,开发者常常使用完全匹配或基于时间戳的并发策略。

       完全匹配并发策略的一个问题是它会导致编辑失败(A打开编辑,B打开编辑,B先保存了,A再保存就会失败,因为部分字段已经不匹配了)。

       遇到这样的问题时,数据绑定控件根本就不会通知你,它们只是执行一条毫无效果的 UPDATE 语句,因为这不是一个错误条件。如果你决定使用这个策略时,你至少要检查缺失的更新。可以处理 GridView 控件的 RowUpdated 事件或者 DetailsView、FormView、ListView 控件的 ItemUpdated 事件来实现这一检查。在该事件中检查合适的 EventArgs 对象的 (如 GridViewUpdateEventArgs)AffectedRows 属性,如果为 0 ,表示没有任何数据被更新。

protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.AffectedRows == 0)
    {
        Label1.Text = "No records were updated.";
    }
}

       遗憾的是,这对多数用户友好的 Web 程序几乎毫无意义。当记录有若干个字段或者字段拥有详细信息时,这尤其是个问题,因为编辑被简单的丢弃了,用户不得不从零重新开始!

       一个更好的办法是让用户选择,在理想状况下,页面显示记录的当前值,并允许用户选择使用原始的编辑值、取消更新或者再做一些修改后继续提交更新。下面通过一个示例来说明。

 

       首先,从一个 DetailsView 开始,编辑 Northwind 数据库中 Shippers 表的记录,这个表只有3个字段因此很容易使用完全匹配并发策略。(大型多字段表使用等效的时间戳策略会更好一些。)

       下面是对 DetailsView 的简单定义,绑定到控件使用完全匹配的 UPDATE 表达式,并启用了 ConflictDetection 和 OldValuesParameterFormatString 这两个属性:

<asp:DetailsView ID="detailsEditing" runat="server" DataKeyNames="ShipperID" AllowPaging="true"
    AutoGenerateRows="false" DataSourceID="sourceShippers" OnItemUpdated="detailsEditing_ItemUpdated">
    <Fields>
        <asp:BoundField DataField="ShipperID" ReadOnly="true" HeaderText="Shipper ID" />
        <asp:BoundField DataField="CompanyName" HeaderText="Company Name" />
        <asp:BoundField DataField="Phone" HeaderText="Phone" />
        <asp:CommandField ShowEditButton="true" />
    </Fields>
</asp:DetailsView>
<asp:SqlDataSource ID="sourceShippers" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
    SelectCommand="select * from Shippers" UpdateCommand="update Shippers set CompanyName=@CompanyName,Phone=@Phone where ShipperID=@original_ShipperID
and CompanyName=@original_CompanyName and Phone=@original_Phone" ConflictDetection="CompareAllValues"
    OldValuesParameterFormatString="original_{0}">
    <UpdateParameters>
        <asp:Parameter Name="CompanyName" />
        <asp:Parameter Name="Phone" />
        <asp:Parameter Name="original_ShipperID" />
        <asp:Parameter Name="original_CompanyName" />
        <asp:Parameter Name="original_Phone" />
    </UpdateParameters>
</asp:SqlDataSource>

       在这个示例里,代码还需要捕获所有失败的更新,并把 DetailsView 显式保持在编辑模式下。真正的技巧在于重新绑定数据控件,这样 DetailsView 中所有原始值被重置来匹配数据库里的最新值,也就是说,更新一定会成功(如果用户尝试再次更新);同时把错误信息提示面板置为可见:

protected void detailsEditing_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e)
{
    if (e.AffectedRows == 0)
    {
        e.KeepInEditMode = true; // 保持在编辑模式
        detailsEditing.DataBind(); // 重新绑定最新的数据
 
        // 把先前更新失败的用户编辑的值重新放回文本框
        // 这些原本要保存的值被放置在 e.NewValues 集合中
        TextBox txt;
        txt = detailsEditing.Rows[1].Cells[1].Controls[0] as TextBox;
        txt.Text = e.NewValues["CompanyName"].ToString(); 
        txt = detailsEditing.Rows[2].Cells[1].Controls[0] as TextBox;
        txt.Text = e.NewValues["Phone"].ToString();
        errorPanel.Visible = true;
    }
}

       错误信息提示面板页面 code:

<asp:Panel ID="errorPanel" runat="server" Visible="false" EnableViewState="false">
    There is a newer version of this record in the database.<br />
    The current record has the values shown below.<br />
    <br />
    <asp:DetailsView ID="detailsConflicting" runat="server" AutoGenerateRows="false"
        DataSourceID="sourceUpdateVlues">
        <Fields>
            <asp:BoundField DataField="ShipperID" HeaderText="Shipper ID" />
            <asp:BoundField DataField="CompanyName" HeaderText="Company Name" />
            <asp:BoundField DataField="Phone" HeaderText="Phone" />
        </Fields>
    </asp:DetailsView>
    <br />
    * Click <b>Update</b> to override these values with your changes.<br />
    * Click <b>Cancel</b> to abandon your edit.
    <asp:SqlDataSource ID="sourceUpdateVlues" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
        SelectCommand="select * from Shippers where ShipperID=@ShipperID" OnSelecting="sourceUpdateVlues_Selecting">
        <SelectParameters>
            <asp:ControlParameter ControlID="detailsEditing" Name="ShipperID" PropertyName="SelectedValue"
                Type="Int32" />
        </SelectParameters>
    </asp:SqlDataSource>
</asp:Panel>

       最后,没有必要每次都对错误信息提示面板进行数据绑定,只要它不可见(即更新没有出错),就没必要进行数据绑定:

protected void sourceUpdateVlues_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
{
    if (!errorPanel.Visible)
    {
        e.Cancel = true;
    }
}

     开启2个页面都进入编辑状态,一个页面编辑完保存,另一个页面跟着保存,由此产生一个数据不匹配更新错误的信息,测试效果如下:

image

posted on 2012-08-10 16:44  SkySoot  阅读(2510)  评论(0编辑  收藏  举报

导航