您的位置:首页 > 编程语言 > ASP

[A1.ASP-NET]Gridview控件高效异步更新与实验(翻译)

2008-01-09 17:30 711 查看
1. 前言

前些天在http://www.ajaxmatters.com上找到一篇关于不使用UpdatePanel来实现Gridview相关ajax操作的文章,回家按部就班地做了一个实验,感觉不错,里面涉及到的编程技术比我在国内网站上查到的要更全面一些,特此翻译全文,以飨读者。

2. 全文翻译

【译者注: 文章来自www.ajaxmatters.com论坛】

【上篇】

本文详细描述了如何使用.net的ICallbackEventHandler接口开发包括排序(sorting), 分页显示(paging), 改变页长(page length change)AJAX Gridview控件, 我在下面会介绍到具体编程,读者也可以在文章最后下载到所有的源码。

本例开发的Gridview控件的基本功能如下(所有操作都是异步的):

点击列名旁边的箭头升序或者降序排列数据

翻页

改变每页显示的数目

在本例中,我们将会用到一个ASP.NET中最为强大的特性 - RenderControl。该方法能使我们方便地(在服务器端 - 译者注)通过HtmlTextWriter和StirngWriter对象访问到一个控件的HTML代码。
示例:


using (StringWriter sw = new StringWriter())




...{




HtmlTextWriter htw = new HtmlTextWriter(sw);




_grid.RenderControl(htw);




htw.Flush();




string result = sw.ToString();




}

我们获取到grid控件的html格式的代码并赋给一个string变量 - 这个工作是在绑定控件数据源之后做的。

现在,我们从开发UI代码开始一步一步完成这个示例程序:
首先,在网页的<form>标签内写下如下代码,创建一个Gridview和Dropdownlist控件:


<div id="Gridview" >


<asp:GridView EnableViewState="false" runat="server" id="_grid" OnRowDataBound="_grid_RowDataBound" AllowPaging="True" >


</asp:GridView>




<br />




</div> Change page length to --




<asp:DropDownList ID="ddl" runat="server">




</asp:DropDownList>

注意Gridview控件的RowDataBound事件已经激活了。

下一步,我们来创建一个DataTable, 作为Gridview控件的数据源:


public DataTable _sampleData




...{






get ...{




DataTable dt = (DataTable)ViewState["DataTable"];




if(dt == null)






...{




dt = new DataTable();




dt.Columns.Add(new DataColumn("Contact Name",typeof(string)));




dt.Columns.Add(new DataColumn("Company Name", typeof(string)));




dt.Columns.Add(new DataColumn("City", typeof(string)));




dt.Columns.Add(new DataColumn("Country", typeof(string)));








dt.Rows.Add(new object[] ...{ "Maria Anders" ,"Alfreds Futterkiste","Berlin","Germany"});






dt.Rows.Add(new object[] ...{ "Ana Trujillo" ,"Emparedados y helados ","México D.F.","Mexico"});






dt.Rows.Add(new object[] ...{ "Antonio Moreno", "Antonio Moreno Taquería", "México D.F.","Mexico" });




ViewState["DataTable"] = dt;




}




return dt;




}

我们使用ViewState 而不是Session 变量来存放DataTable.
比如, ViewState["DataTable"] = dt;
用session可以缓存整个网站的变量, ViewState就只存某个页面的。在绑定数据的时候,我们只需要写_grid.DataSource = _sampleData即可。

下面是Gridview的4个操作方法:

排序


private void sortGrid(string Argument , string pageLength)




...{




DataView dv = _sampleData.DefaultView;




result = "";




dv.Sort = Argument;




_grid.DataSource = dv;




_grid.PageSize = Convert.ToInt16(pageLength);




_grid.DataBind();




renderGrid(_grid);




}

翻页


private void changePage(string Argument , string pageLength)




...{


result = "";




_grid.DataSource = _sampleData;




_grid.PageSize = Convert.ToInt16(pageLength);




_grid.PageIndex = Convert.ToInt16(Argument);




_grid.DataBind();




renderGrid(_grid);




}

改变每页显示长度


private void changePageLength(string Argument, string pageLength)




...{




result = "";




_grid.DataSource = _sampleData;




_grid.PageSize = Convert.ToInt16(Argument);




_grid.DataBind();




renderGrid(_grid);




//pageLength is not used




}

刷新Gridview


private void renderGrid(GridView _grid)




...{


using (StringWriter sw = new StringWriter())




...{




HtmlTextWriter htw = new HtmlTextWriter(sw);




_grid.RenderControl(htw);




htw.Flush();




result = sw.ToString();




}


}

上述4个方法的名称显示了其实现的功能,而前三个的第二个参数'pageLength'是dropdownlist控件选定的值。

我们需要另外4个方法来实现ICallbackEventHandler异步回调 - 两个是javascript方法(其一对服务器端回调,其二用来接收服务器端返回的值并做异步刷新), 两个是服务器端的方法( RaiseCallbackEvent 和GetCallbackResult , 译者注:ICallbackEventHandler的两个接口方法,前面的用来响应客户端回调,后面的用来返回操作结果)。

JavaScript方法如下:


function UpdateGrid(args)




...{




args = args + "$" + window.document.getElementById('ddl').value;




<%= ClientScript.GetCallbackEventReference(this,"args", "ShowResult", null) %>




}

在页面装载完成之后,我们将在页面的源代码里面看到上面的代码编程了这个样子:


function UpdateGrid(args)




...{




args = args + "$" + window.document.getElementById('ddl').value;




WebForm_DoCallback('__Page',args,ShowResult,null,null,false);




}

在回调的时候,我们会触发服务器端的一个事件,因此,UpdateGrid(args)方法需要放在<form>标签之内,否则的话会发生一个JavaScript错误。我们通过Page.ClientScript.RegisterClientScriptBlock方法来注册这个JavaScipt方法,这样的话它将仅出现在<form>标签之内。


function ShowResult(eventArgument,context)




...{


window.document.getElementById('Gridview').innerHTML = eventArgument;


}

这个方法将在服务器端响应客户端之后触发,它负责处理服务器端的返回值。在这里我们只是简单的用返回值替换掉innerHTML属性。innerHTML包含了需要更新的Gridview的HTML代码。

服务器端的代码如下:


public string GetCallbackResult()




...{


return result;


}

该方法返回在ShowResult客户端方法中设置innerHTML的结果。


public void RaiseCallbackEvent(string eventArgument)




...{


string[] args = eventArgument.Split('$');






if (args[0] == "sort") ...{ sortGrid(args[1], args[2]); }






else if (args[0] == "changePage") ...{ changePage(args[1], args[2]); }






else if (args[0] == "changePageLength") ...{ changePageLength(args[1], args[2]); }


}

该函数输入参数形如"changePage$1$10" 或者"sort$1$10" 或者 "changePageLength$1$10" 或者 "sort$Contact Name Asc" 或者 “sort$Contact Name Desc$10”, 我们用字符串解析函数得到下列参数列表:

操作名称(Action);

Gridview控件的页码(Page Index of the Gridview);

Dropdownlist的每页显示长度(Page size from the dropdownlist)

在RaiseCallbackEvent方法中我们调用了一些处理具体操作的函数,每个函数都会设置Gridview更新之后的HTML代码给返回参数。

页面装载代码:


if (!IsPostBack)




...{


_grid.DataSource = _sampleData;




_grid.DataBind();




ddl.Items.Add("10");




ddl.Items.Add("20");




ddl.Items.Add("30");




ddl.Attributes.Add("onchange", "javascript:UpdateGrid('changePageLength$' + this.value);");


}

这里设置Gridview控件每页的显示长度。

现在实现Gridview的RowDataBound事件,为了列排序功能,修改了Gridview控件的列头:


if (e.Row.RowType == DataControlRowType.Header)




...{




for (int i = 0; i < e.Row.Cells.Count; i++)




...{




e.Row.Cells[i].Text = string.Format("{0}<img alt="Ascending" src="Images/up.jpg" onclick="UpdateGrid('sort${0} Asc')"; /><img alt="Descending" src="Images/down.jpg" onclick="UpdateGrid('sort${0} desc') "; />", e.Row.Cells[i].Text);




}


}

列头上加上了上下箭头,点击他们的时候将调用UpdateGrid方法(在客户端 - 译者注)回调服务器端的升序和降序排列方法。

随后我们对页码行(Page Row)做一些绑定时的修改,使页码能够在点击的时候调用UpdateGrid方法。


else if (e.Row.RowType == DataControlRowType.Pager)




...{




GridView gdv = (GridView)sender;




int _pageCount = gdv.PageCount;




e.Row.Cells[0].Text = "";




for (int i = 0; i < _pageCount; i++)




...{


HyperLink hyp = new HyperLink();




hyp.Text = i.ToString() + " ";




hyp.Attributes.Add("href", "javascript:UpdateGrid('changePage$" + i + "');");




e.Row.Cells[0].Controls.Add(hyp);




Label l = new Label();




l.Text = " ";




e.Row.Cells[0].Controls.Add(l);




hyp = null;


}


}

_pageCount 用来保存Gridview控件的页数,为了显示页码,我们使用了HyperLink + Label +空格 的方式。

最后,把EnableViewState属性设置为false,用来减轻页面的负担。

【下篇】
上篇我们使用了ICallbackEventHandler接口实现了一个AJAX的Gridview控件,功能如下:

点击列名旁边的上下小箭头实现升序和降序排列的功能

翻页

改变每页显示长度

本篇将讨论如何通过双击数据格(grid cell)来修改格子里的数据,实现服务器端数据更新的时候来异步刷新页面的功能。关键点如下:

在Page Load的时候只绑定一次数据到Gridview控件

只有当更新数据要求传到服务器端的时候才更新Gridview控件

UI代码:


<div id="Gridview">


<asp:GridView EnableViewState="false" runat="server" id="_grid" OnRowDataBound="_grid_RowDataBound">


</asp:GridView>


<span id ="ServerMsg"></span>


</div>


<br />


<input type=button value="Update" onclick="javascript: JSUpdateTable ();" />




<script language="javascript">...


function UpdateGrid(args)




...{


<%= ClientScript.GetCallbackEventReference(this,"args", "ShowResult", null) %>;


}


</script>

使用一个DataTable作为数据源:


public DataTable _sampleData




...{


get




...{


DataTable dt = (DataTable) Session["DataTable"];


if(dt == null)




...{


dt = new DataTable();


dt.Columns.Add(new DataColumn("Contact Name",typeof(string)));


dt.Columns.Add(new DataColumn("Company Name", typeof(string)));


dt.Columns.Add(new DataColumn("City", typeof(string)));


dt.Columns.Add(new DataColumn("Country", typeof(string)));






dt.Rows.Add(new object[] ...{ "Maria Anders" ,"Alfreds Futterkiste","Berlin","Germany"});




dt.Rows.Add(new object[] ...{ "Ana Trujillo" ,"Emparedados y helados ","México D.F.","Mexico"});




dt.Rows.Add(new object[] ...{ "Antonio Moreno", "Antonio Moreno Taquería", "México D.F.","Mexico" });


Session["DataTable"] = dt;


}


return dt;


}


set




...{


Session["DataTable"] = value;


}


}

注意:在这里我们使用Session而不是ViewState存放DataTable变量,因为我们需要在服务器端更新数据,如果使用ViewState的话需要对页面进行回发(post back)才行。

使用下面的代码把Datatable绑定到Gridview控件: _grid.DataSource = _sampleData; 更新的时候: _sampleData = _tempTable, _tempTable是更新了数据之后的临时DataTable变量。

页面类继承ICallbackEventHandler接口:
public partial class Default: System.Web.UI.Page, ICallbackEventHandler

添加RaiseCallbackEvent方法和GetCallbackResult方法:


public void RaiseCallbackEvent(string eventArgument)




...{


string[] args = eventArgument.Split('$');


if (args[0] == "updateTable") updateTable(args[1]);


}




public string GetCallbackResult()




...{


return result;


}

result 是定义在整个页面上的全局变量。

为了让数据格(cell)能够响应双击事件变成textbox或者dropdownlist, 我们把Gridview和dataTable都看做一个二维数组,里面的任何数据格都同列、行相关联起来(如同row[i][j]),每一个数据格都有一个唯一的id值同DataTable里面的数据对应起来。

RowDataBound事件实现:


protected void _grid_RowDataBound(object sender, GridViewRowEventArgs e)




...{


if (e.Row.RowType == DataControlRowType.DataRow)




...{


for (int i = 0; i < e.Row.Cells.Count; i++)




...{


e.Row.Cells[i].Attributes.Add("ondblclick", "javascript:MakeCellEditable(this);");


e.Row.Cells[i].Attributes.Add("id", _rowNumber + "_" + i);


}


_rowNumber += 1;


}


}

这里规定的每个数据格的id是_rowNumber + "_" + i, 这样就同row[i][j]结构对应起来了,定位数据格的时候我们可以用解析函数解析id,从而或得[i]和[j]的值。

JavaScript方法MakeCellEditable:


function MakeCellEditable(obj)




...{


if(!window.document.getElementById(obj.id + "_input"))




...{


obj.innerHTML = "<input id="+ obj.id + "_input" + " type=text value='" + obj.innerText + "'/>"


}


window.document.getElementById(obj.id + "_input").focus();


}

接下来是得到所有的客户端已经修改的值并把他们用异步回调的方法送到服务器端去更新DataTable,Update按钮的onclick方法如下:


function JSUpdateTable()




...{


var ddl = window.document.getElementById('Gridview');


var ddl1 = ddl.getElementsByTagName('input');


var data = "";


for(i = 0 ; i < ddl1.length ; i++)




...{


ddlId[i] = ddl1[i].id; //ddlId is a global array in JS we will use it in step 9


if(i == 0 ) data = ddl1[i].id + "|" + ddl1[i].value;


else data = data + "~" + ddl1[i].id + "|" + ddl1[i].value;


}


UpdateGrid('updateTable$'+data);


}

在服务器端的RaiseCallbackEvent方法中将调用updateTable方法:


private void updateTable(string _data)




...{


string[] _NewData = _data.Split('~');


DataTable _tempTable = _sampleData;


for (int i = 0; i < _NewData.Length; i++)




...{


string [] _changedTxt = _NewData[i].Split('|');


string[] _rowCol = _changedTxt[0].Split('_');


_tempTable.Rows[Convert.ToInt32(_rowCol[0])][Convert.ToInt32(_rowCol[1])] = _changedTxt[1];


}


_sampleData = _tempTable;


result = "SUCCESS";


}

GetCallbackResult()方法将吧string 类型的result发回客户端,在刚开始的时候我们注册了UpdateGrid(args)方法, <%= ClientScript.GetCallbackEventReference(this, "args", "ShowResult", null)%>; 这里的ShowResult就是客户端用来异步更新Gridview的HTML代码的js方法,将在服务器端执行GetCallbackResult之后自动调用。


var ddlId = new Array();//Elements are added in step 7


// This array contain ids of textboxes


function ShowResult(eventArgument ,context)




...{


formObj = window.document;


if(eventArgument == "SUCCESS")




...{


for(j = 0 ; j < ddlId.length ; j++)




...{


var ids = ddlId[j].split("_");


formObj.getElementById(ids[0] + "_" + ids[1]).innerHTML = formObj.getElementById(ddlId[j]).value;


}


document.getElementById('ServerMsg').innerText = "Data has been updated Successfully...";


}


}

在page_load事件里面绑定Gridview的初始数据:


protected void Page_Load(object sender, EventArgs e)




...{


if (!IsPostBack)




...{


_grid.DataSource = _sampleData;


_grid.DataBind();


}


}

在编辑数据格的时候,还可以考虑使用dropdownlist或者其他控件来代替textbox,另外,翻页事件有可能对数据格的定位造成麻烦,所以我们可以考虑在DataTable上加上一列,其值对应的是Table的行,保证能够通过row[i][j]来定位到正确的数据格上。这个列可以隐藏在Gridview控件内,在添加/删除数据的时候也可以用到。

3. 实验代码及说明
【功能】

Ajax Sorting

Ajax Paging

Ajax Changing page size

Ajax Deleting

GenDataHelper类负责提供数据源:


using System;


using System.Data;


using System.Configuration;


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.Text;






/**//// <summary>


/// Summary description for GenDataHelper


/// </summary>


public class GenDataHelper




...{


public static DataTable GetAsyncUserList()




...{


DataTable dt = new DataTable();


dt.Columns.Add("ID", typeof(int));


dt.Columns.Add("Name", typeof(string));


dt.Columns.Add("Email", typeof(string));




dt.PrimaryKey = new DataColumn[] ...{ dt.Columns["ID"] };




DataRow dr;


dr = dt.NewRow();


dr["ID"] = 1;


dr["Name"] = "Chen";


dr["Email"] = "Chen@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 2;


dr["Name"] = "Lee";


dr["Email"] = "Lee@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 3;


dr["Name"] = "Fang";


dr["Email"] = "Fang@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 4;


dr["Name"] = "Jiang";


dr["Email"] = "Jiang@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 5;


dr["Name"] = "Dang";


dr["Email"] = "Dang@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 6;


dr["Name"] = "Song";


dr["Email"] = "Song@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 7;


dr["Name"] = "Kong";


dr["Email"] = "Kong@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 8;


dr["Name"] = "Liu";


dr["Email"] = "Liu@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 9;


dr["Name"] = "Zhu";


dr["Email"] = "Zhu@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 10;


dr["Name"] = "Qian";


dr["Email"] = "Qian@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 11;


dr["Name"] = "Martin";


dr["Email"] = "Martin@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 12;


dr["Name"] = "Seer";


dr["Email"] = "Seer@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 13;


dr["Name"] = "Freeman";


dr["Email"] = "Freeman@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 14;


dr["Name"] = "Breeman";


dr["Email"] = "Martin@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 15;


dr["Name"] = "Pressman";


dr["Email"] = "Pressman@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 16;


dr["Name"] = "Norman";


dr["Email"] = "Norman@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 17;


dr["Name"] = "Noyman";


dr["Email"] = "Noyman@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 18;


dr["Name"] = "Gudeman";


dr["Email"] = "Gudeman@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 19;


dr["Name"] = "Helman";


dr["Email"] = "Helman@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 20;


dr["Name"] = "Geman";


dr["Email"] = "Geman@ethos.com.cn";


dt.Rows.Add(dr);




dr = dt.NewRow();


dr["ID"] = 21;


dr["Name"] = "Aman";


dr["Email"] = "Aman@ethos.com.cn";


dt.Rows.Add(dr);




return dt;


}


}

测试页面:




<%...@ Page Language="C#" AutoEventWireup="true" CodeFile="AsyncGridViewPage.aspx.cs" Inherits="AsyncGridViewPage" %>




<!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>




<script type="text/javascript" language="javascript">...


var pageIndex;




function UpdateGrid(args)




...{


args = args + "$" + $get('ddlPagesize').value;


<%= ClientScript.GetCallbackEventReference(this, "args", "OnShowResult", null) %>


}




function OnShowResult(eventArgument, context)




...{


if(eventArgument == "SUCCESS")




...{


$get('ServerMsg').innerText = "Data has been updated successfully.";


}


else if(eventArgument == "FAILURE")




...{


$get('ServerMsg').innerText = "Error Occurred.. Please try again..";


}


else




...{


try




...{


$get('GridviewDiv').innerHTML = eventArgument;


}


catch(ex)




...{


$get('ServerMsg').innerText = "Error Occurred.. Please try again..";


}


}


}




function GridSorting(argument)




...{


argument = argument + " " + $get('hiddenSortingValue').value;


if( $get('hiddenSortingValue').value == "ASC")


$get('hiddenSortingValue').value = "DESC";


else


$get('hiddenSortingValue').value = "ASC";


UpdateGrid(argument);


}




function GridDeleting(argument)




...{


if(confirm("Delete?"))




...{


argument = argument + "$" + pageIndex;


UpdateGrid(argument);


}


}




function SetPageIndex(index)




...{


pageIndex = index;


}




</script>


</head>


<body>


<form id="form1" runat="server">


<asp:ScriptManager ID="sm" runat="server"></asp:ScriptManager>


<div>


<h3>Test Async Gridview</h3>


<hr />


</div>


<div align="left">


Please select page size:




<%...--<asp:DropDownList ID="ddlPagesize" runat="server">


<asp:ListItem Text="5" Value="5" Selected="TRUE"></asp:ListItem>


<asp:ListItem Text="10" Value="10"></asp:ListItem>


<asp:ListItem Text="15" Value="15"></asp:ListItem>


</asp:DropDownList>--%>


<select id="ddlPagesize" onchange="javascript:UpdateGrid('CallbackChangePageSize$' + this.value);">


<option value="10" selected="selected">10</option>


<option value="20">20</option>


<option value="30">30</option>


</select>


</div>


<div align="center" id="GridviewDiv">


<asp:GridView id="ContactGrid" runat="server" PageSize="10" AllowPaging="True" GridLines="None" ForeColor="#333333" CssClass="wordBreakStyle" CellPadding="4"


DataKeyNames="ID" AutoGenerateColumns="False" Width="60%" OnRowDataBound="ContactGrid_RowDataBound">


<Columns>


<asp:TemplateField visible="true" HeaderText="ID" SortExpression="ID">


<ItemTemplate>


<asp:Label ID="lblID" runat="server" Text='<%# Bind("ID") %>'></asp:Label>


</ItemTemplate>


<ItemStyle Width="10%" />


<HeaderStyle ForeColor="White" />


</asp:TemplateField>


<asp:BoundField DataField="Name" HeaderText="User name" SortExpression="Name">


<ItemStyle Width="20%" />


<HeaderStyle ForeColor="White" />


</asp:BoundField>


<asp:BoundField DataField="Email" HeaderText="Email" SortExpression="Email" >


<HeaderStyle ForeColor="White" />


<ItemStyle Width="30%" />


</asp:BoundField>


<asp:TemplateField HeaderText="Remove">


<ItemTemplate>




<%...--<asp:LinkButton ID="lnkRemoveContact" Text="Remove" runat="server" CommandName="CmdRemove" OnClientClick="javascript:alert('delete!');"></asp:LinkButton> --%>


<asp:HyperLink runat="server" id="aRemove">Remove</asp:HyperLink>


</ItemTemplate>


<ItemStyle Width="10%" />


</asp:TemplateField>


</Columns>


<EditRowStyle BackColor="#999999" />


<SelectedRowStyle BackColor="#E2DED6" Font-Bold="True" ForeColor="#333333" />


<AlternatingRowStyle BackColor="White" ForeColor="#284775" />


<FooterStyle BackColor="#5D7B9D" Font-Bold="True" ForeColor="White" />


<RowStyle BackColor="#F7F6F3" ForeColor="#333333" />


<PagerStyle BackColor="#284775" ForeColor="White" HorizontalAlign="Center" />


<PagerSettings FirstPageText="FirstPage" LastPageText="LastPage"


NextPageText="NextPage" PreviousPageText="PreviousPage" />


<HeaderStyle BackColor="#5D7B9D" Font-Bold="True" ForeColor="White" />


</asp:GridView>


</div>


<div>


<input type="hidden" id="hiddenSortingValue" value="DESC" />


<br />


<span id="ServerMsg"></span>


</div>


</form>


</body>


</html>



后台代码:


using System;


using System.Data;


using System.Configuration;


using System.Collections;


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.IO;




public partial class AsyncGridViewPage : System.Web.UI.Page, ICallbackEventHandler




...{


private string result = String.Empty;






-- Page Events --#region -- Page Events --


protected void Page_Load(object sender, EventArgs e)




...{ }




protected override void OnLoad(EventArgs e)




...{


base.OnLoad(e);




if (!IsPostBack)




...{


this.LoadTestData();


}


}




private void LoadTestData()




...{


if (Session["TestData"] == null)




...{


Session["TestData"] = GenDataHelper.GetAsyncUserList();


}




this.ContactGrid.DataSource = Session["TestData"];


this.ContactGrid.DataBind();


}


#endregion -- Page Event --






-- GridView Events --#region -- GridView Events --


protected void ContactGrid_RowDataBound(object sender, GridViewRowEventArgs e)




...{


if (e.Row.RowType == DataControlRowType.Pager)




...{


GridView grid = (GridView)sender;


int pageCount = grid.PageCount;


e.Row.Cells[0].Text = String.Empty;


for (int i = 0; i < pageCount; i++)




...{


HyperLink hyp = new HyperLink();


int showNum = i + 1;


hyp.Text = "[" + showNum.ToString() + "]";


hyp.Attributes.Add("href", "javascript:UpdateGrid('CallbackChangePage$" + i.ToString() + "');");


hyp.Style.Add("color", "White");


e.Row.Cells[0].Controls.Add(hyp);


Label l = new Label();


l.Text = " ";


e.Row.Cells[0].Controls.Add(l);


hyp = null;


}


}


else if (e.Row.RowType == DataControlRowType.Header)




...{


GridView grid = (GridView)sender;


for (int i = 0; i < e.Row.Cells.Count - 1; i++)




...{


// e.Row.Cells[e.Row.Cells.Count - 1].Style.Add("display", "none");


HyperLink hyper = new HyperLink();


hyper.Text = e.Row.Cells[i].Text;


hyper.Attributes.Add("href", "javascript:GridSorting('CallbackSorting$" + grid.Columns[i].SortExpression + "');");


hyper.Style.Add("text-decoration", "none");


hyper.Style.Add("color", "White");


e.Row.Cells[i].Controls.Add(hyper);


hyper = null;


}


}


else if (e.Row.RowType == DataControlRowType.DataRow)




...{


GridView grid = (GridView)sender;


HyperLink hyper = (HyperLink)e.Row.FindControl("aRemove");


if (hyper != null)




...{


hyper.Text = "Remove";


hyper.Attributes.Add("href", "javascript:SetPageIndex(" + grid.PageIndex + "); GridDeleting('CallbackDeleting$" + ((Label)e.Row.Cells[0].FindControl("lblID")).Text + "');");


hyper.Style.Add("color", "Blue");


}


}




}




#endregion -- GridView Events --






-- Page Callback Event Handling --#region -- Page Callback Event Handling --


void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument)




...{


try




...{


string[] args = eventArgument.Split('$');


if (args[0] == "CallbackChangePage")




...{


this.Callback_ChangePage(args[1],args[2]);


}


else if (args[0] == "CallbackChangePageSize")




...{


this.Callback_ChangePageSize(args[1]);


}


else if (args[0] == "CallbackSorting")




...{


this.Callback_Sorting(args[1], args[2]);


}


else if (args[0] == "CallbackDeleting")




...{


this.Callback_Deleting(args[1], args[2],args[3]);


}


}


catch




...{


result = "FAILURE";


}


}




string ICallbackEventHandler.GetCallbackResult()




...{


return result;


}




// 'Paging' ajax event


private void Callback_ChangePage(string pageIndex, string pageSize)




...{


result = "";


this.ContactGrid.PageIndex = Convert.ToInt32(pageIndex);


this.ContactGrid.PageSize = Convert.ToInt32(pageSize);


this.ReBindData();


this.SetGridHtml(this.ContactGrid); // set new grid's html


}




// 'Chaning PageSize' ajax event


private void Callback_ChangePageSize(string pageSize)




...{


result = "";


this.ContactGrid.PageIndex = 0;


this.ContactGrid.PageSize = Convert.ToInt32(pageSize);


this.ReBindData();


this.SetGridHtml(this.ContactGrid);


}




// 'Sorting' ajax event


private void Callback_Sorting(string sort, string pageSize)




...{


result = "";


this.ContactGrid.PageSize = Convert.ToInt32(pageSize);


this.ReBindData(sort);


this.SetGridHtml(this.ContactGrid);


}




// 'Deleting ' ajax event


private void Callback_Deleting(string id, string pageIndex, string pageSize)




...{


result = "";


this.ContactGrid.PageIndex = Convert.ToInt32(pageIndex);


this.ContactGrid.PageSize = Convert.ToInt32(pageSize);


this.DeleteRow(Convert.ToInt32(id));


this.ReBindData();


this.SetGridHtml(this.ContactGrid);


}




private void ReBindData()




...{


if (Session["TestData"] != null)




...{


this.ContactGrid.DataSource = Session["TestData"];


this.ContactGrid.DataBind();


}


}




private void ReBindData(string sort)




...{


if (Session["TestData"] != null)




...{


DataView dv = ((DataTable)Session["TestData"]).DefaultView;


dv.Sort = sort;


this.ContactGrid.DataSource = dv;


this.ContactGrid.DataBind();


}


}




private void DeleteRow(int id)




...{


if (Session["TestData"] != null && id >= 0)




...{


DataTable dt = (DataTable)Session["TestData"];


DataRow dr = dt.Rows.Find(id);


if (dr != null)




...{


dt.Rows.Remove(dr);


dt.AcceptChanges();


Session["TestData"] = dt;


}


}


}




private void SetGridHtml(GridView grid)




...{


if(grid != null)




...{


using (StringWriter sw = new StringWriter())




...{


HtmlTextWriter htw = new HtmlTextWriter(sw);


grid.RenderControl(htw);


htw.Flush();


result = sw.ToString();


}


}


}


#endregion -- Page Callback Event Handling --




}



4. 总结
本文涉及到的难点在于调用ICallbackEventHandler接口之后如何刷新一个复杂控件,也就是说程序员要手工实现以前UpdatePanel代替我们完成的工作。译者以前曾经考虑过从服务器端传输一个记录集的字符串到客户端再解析,重写控件中结果集部分的HTML代码,但这样做工作量会很大而且极易出错,本文提供的方法则可以适用于任何复杂控件的异步刷新,可以说全文的闪光点就在于此。

5. 附录: 原文

[FROM http://www.ajaxmatters.com/]
This article details the development of an AJAX-enabled grid using ICallbackEventHandler, with operations which include sorting, paging and page length change. I will work through the code in sequence, but it may help to download to the entire code sample here

The basic features of the gird are as follows (All operations are asynchronous)

Sort in ascending or in descending order by clicking on the arrows next to column name.
Change current page.
Change page length.
In this example we will use one of the most powerful features of ASP.Net - RenderControl.

Using this method we are able to access the HTML of a control. To do this we will have to also use HtmlTextWriter and StringWriter as follows

e.g.

using (StringWriter sw = new StringWriter())

{

HtmlTextWriter htw = new HtmlTextWriter(sw);

_grid.RenderControl(htw);

htw.Flush();

string result = sw.ToString();

}

The result string will contain the HTML format of the grid control. We will now convert the grid control to HTML after binding the data.

We will start by developing the UI and code in following steps:

Enter the following code in the <form> tag of the page to create a GridView and a Dropdownlist.
<div id="Gridview" >

<asp:GridView EnableViewState="false" runat="server" id="_grid" OnRowDataBound="_grid_RowDataBound" AllowPaging="True" >

</asp:GridView>

<br />

</div> Change page length to --

<asp:DropDownList ID="ddl" runat="server">

</asp:DropDownList>

Please note that RowDataBound event of the grid has been activated.

Now we will create a DataTable to be used as DataSource for the GridView.
public DataTable _sampleData

{

get {

DataTable dt = (DataTable)ViewState["DataTable"];

if(dt == null)

{

dt = new DataTable();

dt.Columns.Add(new DataColumn("Contact Name",typeof(string)));

dt.Columns.Add(new DataColumn("Company Name", typeof(string)));

dt.Columns.Add(new DataColumn("City", typeof(string)));

dt.Columns.Add(new DataColumn("Country", typeof(string)));

dt.Rows.Add(new object[] { "Maria Anders" ,"Alfreds Futterkiste","Berlin","Germany"});

dt.Rows.Add(new object[] { "Ana Trujillo" ,"Emparedados y helados ","México D.F.","Mexico"});

dt.Rows.Add(new object[] { "Antonio Moreno", "Antonio Moreno Taquería", "México D.F.","Mexico" });

ViewState["DataTable"] = dt;

}

return dt;

}

In the above code we have used ViewState instead of a Session variable.

i.e. ViewState["DataTable"] = dt;

This will preserve the table for that page. Using Session variable the table would be available throughout the website, but using ViewState the table will only be available for that page.

To bind this table to the GridView we simply use _grid.DataSource = _sampleData;

We will write following four functions for grid operations

private void sortGrid(string Argument , string pageLength)
{

DataView dv = _sampleData.DefaultView;

result = "";

dv.Sort = Argument;

_grid.DataSource = dv;

_grid.PageSize = Convert.ToInt16(pageLength);

_grid.DataBind();

renderGrid(_grid);

}

private void changePage(string Argument , string pageLength)

{

result = "";

_grid.DataSource = _sampleData;

_grid.PageSize = Convert.ToInt16(pageLength);

_grid.PageIndex = Convert.ToInt16(Argument);

_grid.DataBind();

renderGrid(_grid);

}

private void changePageLength(string Argument, string pageLength)

{

result = "";

_grid.DataSource = _sampleData;

_grid.PageSize = Convert.ToInt16(Argument);

_grid.DataBind();

renderGrid(_grid);

//pageLength is not used

}

private void renderGrid(GridView _grid)

{

using (StringWriter sw = new StringWriter())

{

HtmlTextWriter htw = new HtmlTextWriter(sw);

_grid.RenderControl(htw);

htw.Flush();

result = sw.ToString();

}

}

The names of the above functions indicates their functionality, the second parameter of the first three functions is “pageLength” this is the value of the dropdown list

We need four functions for ICallBackEventHandler - two are JavaScript functions (one for Callback to server and another for displaying the response data from server). And two are Server side functions (RaiseCallbackEvent(string eventArgument) and GetCallbackResult())

JavaScript functions are as follows:

function UpdateGrid(args)

{

args = args + "$" + window.document.getElementById('ddl').value;

<%= ClientScript.GetCallbackEventReference(this,"args", "ShowResult", null) %>

}

After rendering, if we open the source of the page; above function will appear as

function UpdateGrid(args)

{

args = args + "$" + window.document.getElementById('ddl').value;

WebForm_DoCallback('__Page',args,ShowResult,null,null,false)

}

When we want to do a Callback, we are actually firing an event which is related to the Webform. Therefore the UpdateGrid(args) function needs to be placed in the <form> tag, otherwise we will get a JavaScript error.

We register this function by using Page.ClientScript.RegisterClientScriptBlock so the function will appear in <form></form> tag only.

function ShowResult(eventArgument,context)

{

window.document.getElementById('Gridview').innerHTML = eventArgument;

}

This function will be fired after server sends a response back to client, thus this function will handle the server response. We have simply put the response as innerHTML of the Div tag. The innerHTML contains the HTML of the updated grid.

The server-Side functions are as follows

public string GetCallbackResult()
{

return result;

}

This function simply returns the result which we have put as innerHTML in the ShowResult function.

public void RaiseCallbackEvent(string eventArgument)

{

string[] args = eventArgument.Split('$');

if (args[0] == "sort") { sortGrid(args[1], args[2]); }

else if (args[0] == "changePage") { changePage(args[1], args[2]); }

else if (args[0] == "changePageLength") { changePageLength(args[1], args[2]); }

}

In this function the eventArgument will appear as “changePage$1$10” or “sort$1$10” or “changePageLength$1$10” or “sort$Contact Name Asc$10” or “sort$Contact Name Desc$10” in the rendered page

If we split this on “$”, we will get

Action .
Page index of the Grid.
Page size from the dropdown list (i.e. id is ‘ddl’).
(In changePageLength we don’t need args[2]; I have kept is just to simplify the code)

In RaiseCallbackEvent we have called the function which we have decleared in POINT 3, each function in point 3 will do the respective action and return a HTML string of the Grid.

Add following code

protected void Page_Load(object sender, EventArgs e)

if (!IsPostBack)

{

_grid.DataSource = _sampleData;

_grid.DataBind();

ddl.Items.Add("10");

ddl.Items.Add("20");

ddl.Items.Add("30");

ddl.Attributes.Add("onchange", "javascript:UpdateGrid('changePageLength$' + this.value);");

}

[At this point if we compile the code we will able to chage the page length ]

Now we will write code in the RowDataBound event of the GridView - i.e. in protected void _grid_RowDataBound(object sender, GridViewRowEventArgs e)

For adding the columnwise sorting functionality in grid, we will modify columnheader row of the GridView.

if (e.Row.RowType == DataControlRowType.Header)

{

for (int i = 0; i < e.Row.Cells.Count; i++)

{

e.Row.Cells[i].Text = string.Format("{0}<img alt=/"Ascending/" src=/"Images/up.jpg/" onclick=/"UpdateGrid('sort${0} Asc')/"; /><img alt=/"Descending/" src=/"Images/down.jpg/" onclick=/"UpdateGrid('sort${0} desc') /"; />", e.Row.Cells[i].Text);

}

}/

We have added up and down images and an onclick event of either of them will call the UpdateGrid function (Ref. Point 4) for sorting Ascending and Descending order repectively. [At this point you can complie the code and the grid will be sortable].

After this we will modify the Pager row in such a way that we can use it to change the page index of the grid using UpdateGrid function.(Ref. point 4 public void RaiseCallbackEvent)

else if (e.Row.RowType == DataControlRowType.Pager)

{

GridView gdv = (GridView)sender;

int _pageCount = gdv.PageCount;

e.Row.Cells[0].Text = "";

for (int i = 0; i < _pageCount; i++)

{

HyperLink hyp = new HyperLink();

hyp.Text = i.ToString() + " ";

hyp.Attributes.Add("href", "javascript:UpdateGrid('changePage$" + i + "');");

e.Row.Cells[0].Controls.Add(hyp);

Label l = new Label();

l.Text = " ";

e.Row.Cells[0].Controls.Add(l);

hyp = null;

}

}

_pageCount contains the pagecount of the GridView. We have to display pages as Numbers so we used a HyperLink and a Label to add space after each page number.

Finally, we can set EnableViewState="false" this will not affect the functionality but the extra code of the ViewState will not come on client side which leads to lighter page.

The entire code for the sample can be downloaded here

-------------------------------------------------------------------------------------------------------------------------------------------

In my previous article of Ajax enabled grid using ICallbackEventHandler I outline the creation of a grid with the following operations

Sort the grid in ascending or in descending order by clicking on the arrows next to a column name.
Change pages of the grid.
Change page length of the grid.

In this article we will discuss editing the grid with the principal goal of doubleclicking on a grid cell to enable editing of that cell then edit the content of the cell and finally update server side data without refreshing the page.

The key advantages of this grid are as follows

Data will be bind to grid only once, i.e. on page load.
Only modified data goes to server to update the grid.

The basic UI will contain one Gridview and one update button. We will bind data to grid on page load.

Simply copy paste following code in the <form> tag of the page
<div id="Gridview">
<asp:GridView EnableViewState="false" runat="server" id="_grid" OnRowDataBound="_grid_RowDataBound">
</asp:GridView>
<span id ="ServerMsg"></span>
</div>
<br />
<input type=button value="Update" onclick="javascript: JSUpdateTable ();" />
<script language="javascript">
function UpdateGrid(args)
{
<%= ClientScript.GetCallbackEventReference(this,"args", "ShowResult", null) %>;
}
</script>

We have activated RowDataBound event of the grid.

We will maintain a datatable, so that we can avoid fetching data from database. We will assume that we have a datatable and we define get and set properties for the table as follows.
public DataTable _sampleData
{
get {
DataTable dt = (DataTable) Session["DataTable"];
if(dt == null)
{
dt = new DataTable();
dt.Columns.Add(new DataColumn("Contact Name",typeof(string)));
dt.Columns.Add(new DataColumn("Company Name", typeof(string)));
dt.Columns.Add(new DataColumn("City", typeof(string)));
dt.Columns.Add(new DataColumn("Country", typeof(string)));

dt.Rows.Add(new object[] { "Maria Anders" ,"Alfreds Futterkiste","Berlin","Germany"});
dt.Rows.Add(new object[] { "Ana Trujillo" ,"Emparedados y helados ","México D.F.","Mexico"});
dt.Rows.Add(new object[] { "Antonio Moreno", "Antonio Moreno Taquería", "México D.F.","Mexico" });
Session["DataTable"] = dt;
}
return dt;
}
set
{
Session["DataTable"] = value;
}
}

We can write a function which will return a datatable, and the get property will simply return that table. We have used a session variable instead of viewstate to store the table; this is because we need to update data on the server side, if we use viewstate we would need to do post back of the page.

To bind this table to the grid we can simply set the datasource of the grid to the datatabe : _grid.DataSource = _sampleData;
And to set new data table we will add _sampleData = _tempTable;

Here _tempTable is the updated table after updating the grid on client side.

We will observe the typical ICallbackEventHandler procedure and add the following points on server side page, i.e. in c# code.
Inherit page class by ICallbackEventHandler interface : public partial class Default : System.Web.UI.Page , ICallbackEventHandler
Add RaiseCallbackEvent(string eventArgument) and GetCallbackResult() as follows :
public void RaiseCallbackEvent(string eventArgument)
{
string[] args = eventArgument.Split('$');
if (args[0] == "updateTable") updateTable(args[1]);
}

public string GetCallbackResult()
{
return result;
}

The result is a global variable declared on the page.

We are following the same convention for RaiseCallbackEvent(string eventArgument) which we have used in my last article ( Ajax enabled grid using ICallbackEventHandler )

To make the cell editable on by a double-click we need to add javascript which will convert the cell in to textbox or a dropdownlist in such way that we can keep track of each updated cell.
We can view a grid and and datatable as a two dimentional arrays with any cell being referenced by a row,column combination (i.e. row[i][j]).

As we have discussed earlier; we will look at the grid as a two dimentional array. Thus we will maintain unique id for each cell so that cell id will give the position of that cell in the datatable.
Here, understand that grid data is an exact copy of the datatable so row[i][j] in datatable will match with row[i][j] of grid.

In step 1 we activated RowDataBound event of the grid. In this step we will write the code for RowDataBound event.
Define _rowNumber as global variable for same page.
int _rowNumber = 0;

protected void _grid_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
for (int i = 0; i < e.Row.Cells.Count; i++)
{
e.Row.Cells[i].Attributes.Add("ondblclick", "javascript:MakeCellEditable(this);");
e.Row.Cells[i].Attributes.Add("id", _rowNumber + "_" + i);
}
_rowNumber += 1;
}
}

To make cell editable we have add:

e.Row.Cells[i].Attributes.Add("ondblclick","javascript:MakeCellEditable(this);");

MakeCellEditable(this) will write this JavaScript function in next step.

To identify each cell as unique cell and to mach it with corrosponding datatable entry, we will give id to each cell in such a way that it will follow row[i][j] structure.
i.e. e.Row.Cells[i].Attributes.Add("id", _rowNumber + "_" + i);

And we are increasing row number each time. Observe the way we have given id, we will get [i][j] position just by spliting the is on “_”.

To make cell editable we will write following function in JavaScript
function MakeCellEditable(obj)
{
if(!window.document.getElementById(obj.id + "_input"))
{
obj.innerHTML = "<input id="+ obj.id + "_input" + " type=text value='" + obj.innerText + "'/>"
}
window.document.getElementById(obj.id + "_input").focus();
}

This function will convert the cell into text box and set focus in that text box. Check the way we have assinged theid to the text box (i.e.[i]_[j]_input obj is the cell)

Up to this point we have made the cell editable - now we need to select all updated values and send them to server to update the datatable.
Onclick of the Update button has a javascript function named UpdateTable() (refer to step 1). This function is listed below:

function JSUpdateTable()
{
var ddl = window.document.getElementById('Gridview');
var ddl1 = ddl.getElementsByTagName('input');
var data = "";
for(i = 0 ; i < ddl1.length ; i++)
{
ddlId[i] = ddl1[i].id; //ddlId is a global array in JS we will use it in step 9
if(i == 0 ) data = ddl1[i].id + "|" + ddl1[i].value;
else data = data + "~" + ddl1[i].id + "|" + ddl1[i].value;
}
UpdateGrid('updateTable$'+data);
}

The UpdateGrid function will call RaiseCallbackEvent(string eventArgument) on the server.
Recall step 3 where we defined RaiseCallbackEvent(string eventArgument) and used the updateTable function. On the updateTable is a server-side function which is responsible for updating the Datatable. The function will be as follows.

private void updateTable(string _data)
{
string[] _NewData = _data.Split('~');
DataTable _tempTable = _sampleData;
for (int i = 0; i < _NewData.Length; i++)
{
string [] _changedTxt = _NewData[i].Split('|');
string[] _rowCol = _changedTxt[0].Split('_');
_tempTable.Rows[Convert.ToInt32(_rowCol[0])][Convert.ToInt32(_rowCol[1])] = _changedTxt[1];
}
_sampleData = _tempTable;
result = "SUCCESS";
}

In step 7 we have seen the javascript function JSUpdateTable() and how we send updated data in the format of string.

For further understanding lets assume that we are sending data to the server and _data = “0_1_input|ABC~3_3_input|XYZ”. Now in the above function observe how we manipulate and parse _data.

result = "SUCCESS"; result is a global string.

After updating the DataTable on the server-side we will send result to client, and if result = "SUCCESS" we will know that the datatable has been updated successfully.

The GetCallbackResult() function (ref. step 3) will send result string to client.

On the client-side we need one function which will automatically get called. In step one we have registered following function
function UpdateGrid(args)
{
<%= ClientScript.GetCallbackEventReference(this,"args", "ShowResult", null) %>;
}
(Please reffer my previouse AJAX Grid article for more details on this function)

ShowResult JavaScript function will get called automatically after the GetCallbackResult(), so now we will write this function.

var ddlId = new Array();//Elements are added in step 7
// This array contain ids of textboxes
function ShowResult(eventArgument ,context)
{
formObj = window.document;
if(eventArgument == "SUCCESS")
{
for(j = 0 ; j < ddlId.length ; j++)
{
var ids = ddlId[j].split("_");
formObj.getElementById(ids[0] + "_" + ids[1]).innerHTML = formObj.getElementById(ddlId[j]).value;
}
document.getElementById('ServerMsg').innerText = "Data has been updated Successfully...";
}
}

The above function is used only remove editable cells (i.e. to remove textboxes and to indicate that the server data has been updated)

On page_load we bind the grid :
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
_grid.DataSource = _sampleData;
_grid.DataBind();
}
}

Further Enhancements

For editing the grid some points need to be considered. We have discussed only about using text boxes for editing but we could also use a dropdownlist or other controls.

If we are using paging and sorting updating a row in the datatable causes a problem for cell referencing. In this case we can add a column to the datatable which corresponds to table row, so if we sort the table and can still maintain the row number (i.e [i] in row[i][j] ). This column will be hidden in the grid so that we can use it for adding and deleting the the data as well.

Code

The full code can be downloaded here

The code contains two aspx pages

EditGridDemo.aspx : This page contains the code in this article.
AjaxifiedGrid.aspx: This page contains the code for fully ajaxified grid.
------------------------------------------------------------------
Oliver Gu

2008-01-09, Haidian District, Beijing

TestProject=[A1.ASP-NET.001]OKTW_Test
--------------------------------------------------------------------
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐