真正类似于 Excel 的网格控件
2024-07-21 02:24:28
供稿:网友
注 与本文相关的下载文件的 visual basic_ 版本已于 3 月 4 日进行了更新。如果您是在 3 月 4 日之前下载源代码的,则将需要重新下载该文件以获得 visual basic 文件。
asp.net datagrid 控件生成一个 html 输出结果,此结果看上去确实像 microsoft excel 工作表的 web 副本。另外,该控件支持如选择和就地编辑之类的功能,这些功能又进一步证实了这种相似性。特别是,从支持就地编辑功能这一点来看时,这种相似性就最为明显了。当您单击特殊类型的命令列时,网格会使用文本框(而非静态文本)重绘其内容。与此同时,命令列会更改布局,将编辑链接替换为两个其他链接 — 一个用来保存所做的更改,另一个用来取消所做的更改。整个看上去几乎与 excel 名称框命令栏完全相同。
datagrid 还使程序员有机会在某种程度上自定义所编辑的单元格的布局。这可通过以下方法来实现:使用模板化列来代替绑定列和按钮列,并在模板化列的标记正文中定义编辑模板。
简单地说,datagrid 控件只是为就地编辑提供基础结构,并在保存所做的更改时激发某些事件。为了能够完全编辑,网格组件应当提供三个您可能希望针对数据源执行的基本操作:“插入、“删除和 “更新”。datagrid 用于编辑的基础结构(基本上是 editcommandcolumn 列类)只保证能够执行更新和删除操作。实现 “删除” 功能相对容易些,而且只要求您定义一个命令名为 “删除” 的 buttoncolumn 对象,并等待deletecommand 事件激发。
在本专栏中,您将看到如何扩展 datagrid 控件,使其支持 insert 命令。我们将通过创建一个新类并让其从 datagrid 继承来实现这一点。我们将使该类尽可能多地实现样本代码,以省去某些重复编码。因此,我们将拥有一个激发新事件和更具体事件的控件,而且我们可以使用这个唯一的界面来维护数据库表。
editablegrid 控件
新控件应当具有哪种接口?
的思路是尽可能限制程序员必须编写的代码的数量。该控件将负责向其自身的接口中添加任何新行,然后在需要保存所做的更改时警告用户。这一原则适用于大多数操作,而不应当仅限于 “插入”。在实现就地编辑和删除时,总是必须在 sql 语句周围放一些相对标准的和重复的代码。特别是在实现就地编辑功能时,必须重置所编辑项目的索引,并重新加载和重新绑定更新过的数据源。所有这些样本代码将嵌入到新的 editablegrid 控件中。因此,该控件提供一个名为 addnewrow 的新布尔属性,以及几个自定义事件 — initrow、updateview、“保存数据”、“插入数据” 和 “删除数据”。
当用户希望添加新行时,他(或她)只需将 addnewrow 属性设置为 true 并重新绑定该控件。该操作的其余部分在内部发生。(稍后我将描述此过程的细节。)新行将在编辑模式下绘制,initrow 事件将激发,其目的只是使您有机会将某些字段设置为默认值。
updateview 的角色不与更新操作紧密相关。datagrid 控件不缓存数据源,因此,无论页面何时回发(以便进行分页、排序、编辑、插入或删除),您都需要重新绑定。为了简化编码并尽可能多地嵌入编码,添加了这个新事件。当该网格需要设置其 datasource 属性时,“更新视图” 就会激发。“更新视图” 的典型处理程序将当前的数据源分配给该属性并调用 databind 方法。
当相应的 sql 语句不能进一步延迟执行时,就会激发其他三个事件 — “保存数据”、“插入数据” 和 “删除数据”。在处理其中的任何事件时,可设置和执行 “更新”、“插入” 或 “删除” 语句。您负责检索更新后的数据,并准备和执行该命令。除了 “插入数据”(与 datagrid 编程接口没有紧密关系)以外,“保存数据” 和 “删除数据” 也不同于标准的 updatecommand 和 deletecommand,这是因为它们更具体而且只要求您执行 sql 代码。新事件基本上由 updatecommand 和 deletecommand 的处理程序激发,这些处理程序是 editablegrid 控件在加载时以静默方式定义的。这些内部处理程序负责执行所有其他任务(例如,重置索引)并刷新视图。后者(即刷新视图)是通过激发 updateview 事件来完成的。
设置控件
让我们快速浏览 editablegrid 类。该类的构造函数初始化某些公共属性和受保护的属性,并为基类的几个事件设置默认处理程序。
namespace bwslib
{
public class editablegrid : datagrid
{
public editablegrid()
{
allowfullediting = true;
addnewrow = false;
allowpaging = true;
rejectchanges = false; // internal use
mustinsertrow = false; // internal use
// handlers
init += new eventhandler(oninit);
pageindexchanged += new
datagridpagechangedeventhandler(onpageindexchanged);
itemcreated += new datagriditemeventhandler(onitemcreated);
cancelcommand += new datagridcommandeventhandler(oncancelcommand);
editcommand += new datagridcommandeventhandler(oneditcommand);
updatecommand += new datagridcommandeventhandler(onupdatecommand);
deletecommand += new datagridcommandeventhandler(ondeletecommand);
}
:
}
}
editablegrid 类有一个尚未提到的公共属性 — allowfullediting。该属性成员支持对网格的完全编辑功能。如果您将该属性设置为 false,则该控件将不提供就地编辑或插入功能。究竟执行的是怎样的处理?该控件自动将 allowpaging 设置为 true,并为 pageindexchanged 提供一个处理程序。这意味着 editablegrid 还是比 datagrid 控件好一些,因为它为您提供自动的空闲分页。
当 allowfullediting 设置为 true(默认值)时,editablegrid 控件自动将两个新列追加到网格中。第一列是 editcommandcolumn,它提供就地编辑功能。第二列是 buttoncolumn,它的命令是 delete。这两列都是通过为响应 init 事件而运行的处理程序来添加的。
public void oninit(object sender, eventargs e)
{
if (allowfullediting)
addworkercolumns();
}
private void addworkercolumns()
{
// edit column
editcommandcolumn editcolumn = new editcommandcolumn();
editcolumn.edittext = editcolumntext;
editcolumn.updatetext = editcolumnupdatetext;
editcolumn.canceltext = editcolumncanceltext;
columns.add(editcolumn);
// delete column
buttoncolumn deletecolumn = new buttoncolumn();
deletecolumn.commandname = "delete";
deletecolumn.text = deletecolumntext;
columns.add(deletecolumn);
}
editcolumntext、editcolumnupdatetext、editcolumncanceltext 和 deletecolumntext 确定用来触发编辑和删除操作的按钮的文本。它们在默认时分别为 “编辑”、“确定”、“取消” 和 “删除”。
在将该控件插入到任何 asp.net 页中之前,必须按如下方式注册它:
<%@register tagprefix="expo" namespace="bwslib" assembly="editablegrid" %>
在上面的声明中,可以更改 tagprefix 属性的内容,但是,除了该控件的命名空间和类名可以修改以外,不能更改任何其他内容。下面的代码显示如何在 aspx 页中声明该控件:
<expo:editablegrid id="grid" runat="server"
autogeneratecolumns="false"
font-name="verdana" font-size="xx-small"
cellspacing="0" cellpadding="3"
borderstyle="solid" bordercolor="skyblue" borderwidth="1"
gridlines="both"
pagesize="4"
datakeyfield="employeeid"
oninitrow="initrow"
onupdateview="updateview"
onsavedata="savedata"
oninsertdata="insertdata"
ondeletedata="deletedata">
:
<columns>
:
</columns>
</expo:editablegrid>
示例页从一个名为 employees 的 microsoft access 2000 表读出数据并将生成的 dataset(数据集)存储在 cache 对象中。
private dataset physicaldataread()
{
oledbdataadapter da = new oledbdataadapter(
"select * from employees",
"provider=microsoft.jet.oledb.4.0;data source=c://myemployees.mdb;");
dataset ds = new dataset();
da.fill(ds, "employees");
return ds;
}
图 1 显示该页中控件的输出结果。
图 1. 操作中的 editablegrid 控件
添加和删除列时无需任何额外的代码。可是,必须要指定 updateview 处理程序。但是,正如您可以看到的那样,这是一段必须在多个位置与 datagrid 控件一起使用的代码。然而,由于有 updateview 事件,所以只需指定一次。
public void updateview(object sender, eventargs e)
{
updatedataview();
}
private void updatedataview()
{
dataset ds = (dataset) cache["mydata"];
dataview dv = ds.tables["employees"].defaultview;
grid.datasource = dv;
grid.databind();
}
在上面的控件声明中,还可以看到三个与数据相关的事件的事件处理程序。让我们看一下该控件如何处理它们。
insert 操作
editablegrid 控件不包括任何用来启动 “插入” 操作的用户界面元素。正如对于其他重要功能一样,程序员负责将能够触发网格上给定事件的超级链接或按钮放到页面中。由于添加新项目不是依赖特定行的操作,因此建议您不要使用特殊的命令列(例如,“编辑” 或 “删除”)。客户端代码处理页面级事件,并设置 addnewrow 属性以响应该操作。接着,页面刷新网格,以反映所做的更改。例如,图 1 中的 insert(插入)按钮链接到下面的 on-click 处理程序:
public void oninsert(object sender, eventargs e)
{
grid.addnewrow = true;
updatedataview();
}
“更新数据视图” 是页面级过程,它处理网格数据绑定并设置 “数据源” 属性。
设计的思路是使用户不直接将新记录添加到数据源中,而是只声明其想要实现的目标。因此,在设置 editablegrid 控件的 “数据源” 属性时,该控件会检查 addnewrow 的值。在派生的控件中,因为有多种访问器,所以可以很方便地确定何时读取或写入给定属性。这样做之后,editablegrid 会按如下方式重写 “数据源” 属性:
public override object datasource
{
get {return base.datasource;}
set
{
// standard behavior
base.datasource = value;
// customized behavior
if (allowfullediting)
{
if (addnewrow)
{
addnewrow = false;
insertnewrow();
}
// more code here...
:
}
}
}
在设置 “数据源” 时,只要 addnewrow 属性为 true,就会追加一个新行。insertnewrow 就是用来为插入和行编辑功能设置网格的内部过程。此处进行的重要假设就是网格与 “数据视图” 对象绑定。利用该代码,您可以方便地推广到更普遍的解决方案。
private void insertnewrow()
{
// get the underlying datatable object
datatable dt = ((dataview) datasource).table;
// if any pending changes, stop here to avoid inserting over
// and over again if user refreshes...
datatable tableofpendingchanges = dt.getchanges(datarowstate.added);
if (tableofpendingchanges!= null)
return;
// add the new row
datarow row = dt.newrow();
dt.rows.add(row);
// initialize the row (fire the initrow event)
datagridinitroweventargs dgire = new datagridinitroweventargs();
dgire.row = row;
oninitrow(dgire);
// goto to last page and turn edit mode for the last item
int nnewitemindex = setindexestolastpage(dt);
edititemindex = nnewitemindex;
// tracks that a new row has just been added (internal member)
mustinsertrow = true; // means the table has pending changes
}
在创建基础 “数据表” 对象以后,使用 newrow 方法添加新行,并激发自定义的 initrow 事件,以便用户有机会将某些字段设置为默认值。为了实现其目标,initrow 需要将某些数据向下传递到事件处理程序并接收任何更新。必须安排一个自定义的事件数据结构和一个自定义的事件处理程序签名。datagridinitroweventargs 是事件数据结构的名称,而 datagridinitroweventhandler 是要使用的委托。
public delegate void datagridinitroweventhandler(
object sender, datagridinitroweventargs e);
public event datagridinitroweventhandler initrow;
private void oninitrow(datagridinitroweventargs e)
{
if (initrow != null)
initrow(this, e);
}
datagridinitroweventargs 类继承于 eventargs 并通过简单地添加一个 datarow 成员进行扩展。
public sealed class datagridinitroweventargs : eventargs
{
// represents the row being added. you can use this object
// to set fields to default values.
public datarow row;
}
在激发 initrow 事件之前,该控件用新创建(但仍为空)的 datarow 对象的实时实例填充 row 成员。
datagridinitroweventargs dgire = new datagridinitroweventargs();
dgire.row = row;
oninitrow(dgire);
如果需要设置某些默认值,则可以编写 initrow 处理程序并设置 datarow 对象的字段。
public void initrow(object sender, datagridinitroweventargs e)
{
e.row["lastname"] = "esposito";
}
此处,datasource 额外有一行可模拟新记录的插入操作。该行已经添加到数据源的底部。因此,您应当使网格指向它的最后一页,而且还要考虑到新行进入新页的可能性。例如,如果表中有八个记录,而且您使用的页面大小为四,则在添加新记录时还需要增加一个新页。为了使用户能够编辑新行的内容,只需将 datagrid 的 edititemindex 属性设置为新行的页索引。insertnewrow 完成的最后一步是设置内部数据成员,来跟踪向表中添加新的未提交行的操作。
简言之,该代码将一个空行对象添加到网格的数据源中。该行代表 “数据表” 对象的待定更改,而且如果用户取消该操作,则必须拒绝该行。如果用户移到另一页或者决定单击和编辑同一页上的另一行,也必须拒绝该行。拒绝待定更改是内置处理程序(例如,pageindexchanged、editcommand 和 cancelcommand)通过内部数据成员 rejectchanges 完成的事情之一。(有关更多详细信息,请参阅源代码。)
通过将新行置于编辑模式,即可完成插入阶段,从而进入更新阶段,如下图所示。
图 2. 插入新行
对于 insertnewrow 的主体,我还需要阐明另外一个方面。在获取数据源之后,该方法立即确保没有已添加但尚未提交的行。在设计上,最多可以有一个待定更改,而且如果该代码重新进入有一个已添加的行的过程,那是由于用户刷新了该页面。为了避免反复添加空白行和未提交的行,程序流会跳出代码块。
update 操作
请注意,在图 2 中看不到可在图 1 中看到的 delete 列。为了简化界面,我决定在任何行进入编辑模式时隐藏 delete 列。并且因为有了以前提到的内置事件处理程序,所以此行为在 editablegrid 类中是硬编码的。
update 操作是由以下三个事件执行的:
• editcommand — 开始该操作并以编辑模式呈现行
• updatecommand — 保存所做的更改并还原默认用户界面
• cancelcommand — 取消所做的更改并还原以前的用户界面
用于 editcommand 和 cancelcommand 的典型代码是能够方便地嵌入类中的传统 vanilla 代码。一般来说,此处没有真正需要在页面级解决的内容,但是有时情况也可能与您的特定实例有所不同。
“更新” 操作的应用程序特定的逻辑集中在 “更新命令” 事件中。除了保存任何更改以外,您应当还原网格的一致状态(取消编辑模式、还原 delete 列和拒绝待定的更改),并确保所做的更改随后由用于显示的数据源反映出来。如果您像本例中那样使用缓存数据,则后面的一点至关重要。
public void onupdatecommand(object sender, datagridcommandeventargs e)
{
// clear edit mode
edititemindex = -1;
// show/hide delete column
toggledeletecolumn(true);
// reject changes on the last row
rejectchanges = true; // internal member
// update or insert data
if (mustinsertrow) // internal member
oninsertdata(e);
else
onsavedata(e);
// refresh view
onupdateview(eventargs.empty);
}
updatecommand 处理程序取消当前行的编辑模式,然后打开 delete 列的可见性标志。此时,该表可能有一个待定的更改。因为 “更新命令” 事件会在两种情况(打算将所做的更改保存到现有的行中时;要插入新行时)下激发,所以条件窗体很有意义。内部成员 mustinsertrow 由 insertnewrow 设置并由 datasource 重置,它有助于确定是哪种情况。当代码处理完网格的状态之后,它激发两个连续事件 — 一个事件让页面保存或插入到物理数据源中,另一个事件刷新数据绑定。这就解释了为何 insertdata 和 savedata 处理程序只能执行主要通过 sql 语句来进行的物理更新。
insertdata 和 savedata 事件处理程序的签名与 updatecommand 的相同。
public event datagridcommandeventhandler savedata;
protected virtual void onsavedata(datagridcommandeventargs e)
{
if (savedata != null)
savedata(this, e);
}
public event datagridcommandeventhandler insertdata;
protected virtual void oninsertdata(datagridcommandeventargs e)
{
if (insertdata != null)
insertdata(this, e);
}
在本文中讨论的示例代码中,设置了几个假设,其中一个就是假设网格的数据源是 “数据视图” 对象。这间接表示不支持自定义分页,或者,更确切地讲,必须仔细修改此代码才能处理这样的网格。这同样适用于排序。
所作的第二个重要假设是有关使用 sql 语句进行更新的。
设计思路是,无论进行了什么样的更改,总是激发单个 sql 语句以应用它。此处设计的 editablegrid 不能正确地处理批更新。顺便提一句,在我的 building web solutions with asp.net and ado.net 一书中,在介绍就地编辑时,更详细地讨论了对于网格使用批更新的优缺点。然而,如果您对使用批更新技术感兴趣,请给我发送电子邮件,我将在以后的专栏中介绍此主题。
由于您通过直接的 sql 语句(或者数据源识别为直接语句的内容)进行更新,因此,“更新命令” 事件可以成功地命令拒绝任何更改。这就是批更新方案中的主要区别。
“保存数据” 和 “插入数据” 事件代表执行更新所必需的任务的子集。在执行该命令之后,这些处理程序必须确保网格可以访问和显示刷新数据。在这种情况下,必须更新数据的缓存副本。根据基础数据库架构(任何触发器或任何自动编号的字段),可以决定是完全重新读取还是基于每个字段更新缓存数据。
让我们花些时间来了解如何从网格的编辑模板来检索更新后的数据。我考虑使 editablegrid 控件具有足够的智能,以便从单元格提取值,并将它们填充到 datarow 对象中以充当事件数据。这个方法使得在批方案中更新代码变得微不足道。之所以决定让 asp.net 页负责提取数据,是因为这样您也可以透明地支持编辑模板。下面我将向您展示在不使用任何特殊模板时所必需的代码。
public void savedata(object sender, datagridcommandeventargs e)
{
stringbuilder sb = new stringbuilder("");
sb.append("update employees set ");
sb.append("firstname='{0}',");
sb.append("lastname='{1}',");
sb.append("title='{2}',");
sb.append("country='{3}' ");
sb.append("where employeeid={4}");
string cmd = sb.tostring();
sb = null;
textbox fname = (textbox) e.item.cells[1].controls[0];
textbox lname = (textbox) e.item.cells[2].controls[0];
textbox position = (textbox) e.item.cells[3].controls[0];
textbox country = (textbox) e.item.cells[4].controls[0];
cmd = string.format(cmd,
fname.text, lname.text,
position.text, country.text,
grid.datakeys[e.item.itemindex]);
// executes the command
oledbconnection conn = new oledbconnection(m_connstring);
oledbcommand c = new oledbcommand(cmd, conn);
c.connection.open();
c.executenonquery();
c.connection.close();
// re-read from the database and updates the cache
datafromsourcetomemory();
}
要检索用户在文本框中输入的信息,使用 e.item.cells[n].controls[0] 表达式,其中n 是该列的索引(从 0 开始)。请记住,datagrid 的就地编辑功能允许您通过将 “只读” 属性设置为 true 来将列视为只读。
delete 操作
delete 操作的工作方式与 insert 和 update 操作大体相同。自动创建的列有一个命令名 delete,在单击该按钮时会导致激发 ± 事件。内置的处理程序修复网格的界面,然后依次先后激发 deletedata 和 updateview。
由于删除操作的入侵性比插入或更新操作的强,因此您可能希望使用某个客户端脚本代码,并要求用户在继续之前确认。我在 last month's column 的“对话栏”部分中讨论了该方法。在本月的源代码中,有一个如下图所示的实际实现。
图 3. 在删除之前确认
最终的优化
我提到过 editablegrid 控件支持编辑模板。让我来证明这一结论。在示例代码的第 2 步,我使用该网格中一组稍有不同的列。
<columns>
<asp:boundcolumn runat="server" headertext="id"
datafield="employeeid" readonly="true" />
<asp:templatecolumn runat="server" headertext="name">
<itemtemplate>
<%# databinder.eval(container.dataitem, "lastname") + ", " +
databinder.eval(container.dataitem, "firstname") %>
</itemtemplate>
<edititemtemplate>
<asp:textbox runat="server" id="lname"
text='<%#databinder.eval(container.dataitem, "lastname")%>' />
<asp:textbox runat="server" id="fname"
text='<%#databinder.eval(container.dataitem, "firstname")%>' />
</edititemtemplate>
</asp:templatecolumn>
<asp:boundcolumn runat="server" headertext="position"
datafield="title" />
<asp:boundcolumn runat="server" headertext="country"
datafield="country" />
</columns>
正如您可以看到的那样,有一个模板列,它在单个字段中显示名和姓。此列的编辑模板(您必须指定能够编辑该列的编辑模板)由两个并排的文本框提供。无需在类中进行任何更改,即可获得图 4 中所示的示例。
图 4. 使用编辑模板
另一方面,在使用从文本框中检索更新的文本的方式时,编辑模板需要进行少许调整。现在,可以按名称检索模板中的控件。之所以可以并且建议这样做,是因为您知道控件的 id。
textbox fname = (textbox) e.item.findcontrol("fname");
textbox lname = (textbox) e.item.findcontrol("lname");
textbox position = (textbox) e.item.cells[2].controls[0];
textbox country = (textbox) e.item.cells[3].controls[0];
在本月的源代码中,您将发现该类的 c# 源代码、两个示例 asp.net 页以及我用过的 access 数据库。但是,您将找不到编译过的程序集。我提供了一个(非常)简单的批文件,您可以使用它,针对与 beta 2 及其更高版本兼容的任何版本的 .net clr,将该类编译成为可执行代码。
注 如果您在使下载正确运行时遇到问题,请检查下列步骤:
• 1.将 cs 类编译成为程序集。这可通过打开一个 dos 框并运行 zip 中的 c.bat 批处理文件来完成,或者通过在 visual studio 中创建一个新的类库项目并将类文件添加到这个空白项目中来实现。
• 2.必须使程序集对于示例 aspx 页可用。下载文件中包括的批处理文件将该 dll 复制到 c:/inetpub/wwwroot/bin 文件夹中。如果您恰好有一个不同的路径,请对它进行修改。如果您创建一个虚拟目录,请确保将该 dll 复制到虚拟文件夹的 bin 子文件夹中,而不要复制到 web 服务器根的 bin 文件夹中。
• 3.根据 asp.net 安全设置,在与示例 microsoft access 数据库交互时,可能会遇到难以处理的 updateable query 错误。在这种情况下,请更改示例 .mdb 文件的安全设置。在“windows 资源管理器”中选择该文件,显示属性,然后单击安全选项卡。接着,添加 /aspnet 用户并确保它有权写入和修改该文件。
• 4.刷新 aspx 页面。
对话栏:创建自定义模板
在上一个专栏中,您讨论了 summary 网格组件。是否有机会创建或加载汇总行的自定义模板?这将允许在 aspx 文件中(而非代码中)规定设计的规范。
使用汇总行的自定义模板无疑将是可能的。问题只是在于如何实现?或者,更准确地说,哪种方法最简单?我将在下面介绍几种可能的方法。
• 使用 pagelet: 可以修改应用程序的代码,使其动态加载 pagelet 控件(又称用户控件) — 即 ascx 文件。ascx 文件看上去像一个小型 web 窗体,而且其中的大部分内容都是布局信息。可以使用 page.loadcontrol 来按名称加载 ascx 文件,然后将它添加到 datagriditem 对象的某个单元格的 controls 集合中。
• 模板列:datagrid 的所有列都是模板化列。使用 asp.net 标记和各处的一些代码定义每一列的 。您能够执行的操作就是将 块的结构分成两个截然不同且互斥的部分 — 一部分用于普通行,另一部分用于汇总行。为了确保一次只显示一行,可以处理控件的 “可见” 属性并将该属性与界定汇总行或普通行的条件绑定。
• 编写新控件:从 datagrid 派生一个新类并添加一个新的模板属性。这允许您使用自定义的子标记、按照与输入列模板几乎相同的方式输入汇总行布局。
返回页首
国内最大的酷站演示中心!