真正类似于 Excel 的网格控件(一)

翻译|其它|编辑:郝浩|2005-01-04 09:46:00.000|阅读 1502 次

概述:

# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>


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 的主体,我还需要阐明另外一个方面。在获取数据源之后,该方法立即确保没有已添加但尚未提交的行。在设计上,最多可以有一个待定更改,而且如果该代码重新进入有一个已添加的行的过程,那是由于用户刷新了该页面。为了避免反复添加空白行和未提交的行,程序流会跳出代码块。


标签:

本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com


为你推荐

  • 推荐视频
  • 推荐活动
  • 推荐产品
  • 推荐文章
  • 慧都慧问
扫码咨询


添加微信 立即咨询

电话咨询

客服热线
023-68661681

TOP