自定义数据编辑界面

本文档是 Visual C# 教程 (切换到 Visual Basic 教程)

本教程中,我们将了解怎样自定义可编辑的 GridView 界面,解决方法是将标准的 TextBox 与 CheckBox 控件替换为其他的 Web 输入控件。

« 前一篇教程  |  下一篇教程 »

简介

因为GridView 与 DetailsView 控件使用的BoundField 与 CheckBoxField 能够呈现只读、可编辑、可插入的界面,所以它们能简化修改数据的过程。这些界面不需要加入任何附加的声明式标记或代码,就可以被呈现出来。然而,实际场景中经常需要自定义的界面,BoundField 与 CheckBoxField 的界面却无法做到这点。为了在GridView 或 DetailsView 中自定义编辑或插入界面,我们需要转为使用 TemplateField 。

前一篇教程 中,我们看到了怎样通过添加 Web 验证控件来对数据修改界面进行自定义。本教程中,我们将了解怎样自定义实际的Web 数据集合控件 ,将BoundField 与 CheckBoxField 的标准 TextBox 与 CheckBox 控件替换为其他的 Web 输入控件。尤其是,我们将构建一个可编辑的GridView ,它允许对产品的名称、类别、供应商、断货状态进行更新。当对特定一行进行编辑时,类别与供应商字段将呈现为 DropDownList ,其中包含了可用的类别与供应商集合以供选择。此外,我们还会把 CheckBoxField 的默认 CheckBox 替换为 RadioButtonList 控件,它能提供两个选项:“ Active ”与“ Discontinued ”。

图1:GridView 的编辑界面包括了 DropDownList 与 RadioButton

步骤1 :创建适当的UpdateProduct 重载

本教程中,我们将构建一个可编辑的 GridView ,它允许编辑产品的名称、类别、供应商、断货状态。因此,我们需要一个 UpdateProduct 重载,它要接受五个输入参数 — 这四个产品值加上 ProductID 。与我们之前的重载一样,这个重载将:

  1. 为指定的 ProductID 从数据库中取得产品信息 ,
  2. 更新 ProductName 、CategoryID 、SupplierID 、Discontinued 字段 , 并且
  3. 通过 TableAdapter 的 Update() 方法 , 将更新请求发送给 DAL 。

为简单起见,对这个重载,我没有加上 “ 如果产品标为断货,确保它不是供应商提供的唯一产品” 这条业务规则检查。如果您想要的话,可以随意加上,或者,理想做法是,将这个逻辑分离成一个单独的方法。

下面的代码显示的是 ProductsBLL 类中这个新的 UpdateProduct 重载 :

[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, int? categoryID,
    int? supplierID, bool discontinued, int productID)
{
    Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    Northwind.ProductsRow product = products[0];
    product.ProductName = productName;
    if (supplierID == null) product.SetSupplierIDNull();
      else product.SupplierID = supplierID.Value;
    if (categoryID == null) product.SetCategoryIDNull();
      else product.CategoryID = categoryID.Value;
    product.Discontinued = discontinued;
    // Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

步骤2 :设计可编辑的GridView

添加完 UpdateProduct 重载之后,我们就开始创建我们的可编辑 GridView 。打开 EditInsertDelete 文件夹中的 CustomizedUI.aspx 页面,为设计器添加一个GridView 控件。接下来,从 GridView 的智能标记中创建一个新的 ObjectDataSource 。对 ObjectDataSource 进行配置,使得通过 ProductBLL 类的 GetProducts() 方法得到产品信息,使用我们刚才创建的 UpdateProduct 重载更新产品数据。在INSERT 与 DELETE 选项卡中,从下拉列表中选择 (None)。

图2:将 ObjectDataSource 配置为使用刚才创建的 UpdateProduct 重载

与我们在数据修改教程一直看到的一样,Visual Studio 创建的 ObjectDataSource 的声明式语法会将 OldValuesParameterFormatString 属性赋值为original_{0} 。当然,这在我们的 Business Logic Layer (业务逻辑层) 无法正常工作,因为我们的方法并不期待输入原来的 ProductID 值。因此,与我们在之前教程中所做的一样,花点时间将这个属性赋值从声明式语法中删除,或者,将这个属性的值设为 {0} 。

更改之后 ,ObjectDataSource 的声明式标记应该如下所示 :

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    SelectMethod="GetProducts" TypeName="ProductsBLL"
    UpdateMethod="UpdateProduct">
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

注意,OldValuesParameterFormatString 属性已经被删除了,并且对于我们UpdateProduct 重载期待的每个输入参数,在UpdateParameters 集合中都有一个 Parameter 与之对应。

虽然ObjectDataSource 被配置为仅对产品值的一个子集进行更新,但是现在 GridView 会显示所有产品字段。花点时间编辑 GridView ,使得:

  • 它仅包含 ProductName 、 SupplierName 、 CategoryName BoundField 与 Discontinued CheckBoxField
  • CategoryName 与 SupplierName 字段出现在 Discontinued CheckBoxField 之前(左侧)
  • CategoryName 与 SupplierName BoundField 的 HeaderText 属性被分别设置为“ Category ”与“ Supplier ”
  • 启用编辑支持(选中 GridView 的智能标记中的 Enable Editing 复选框)

进行这些更改之后,编辑器将如图 3 所示,GridView 的声明式语法如下所示。

图3 :从GridView 中删除不需要的字段

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ObjectDataSource1">
    <Columns>
        <asp:BoundField DataField="ProductName"
           HeaderText="ProductName" SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category"
           ReadOnly="True"
           SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier"
           ReadOnly="True"
           SortExpression="SupplierName" />
        <asp:CheckBoxField DataField="Discontinued"
           HeaderText="Discontinued" SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

此时,GridView 的只读功能就完成了。当查看数据时,每个产品都呈现为 GridView 中的一行,显示为产品的名称、类别、供应商、断货状态。

图4 :GridView 的只读界面完成了

注意:在插入、更新、删除数据概述教程中讨论过,必须启用 GridView 的view state (默认),这点非常重要。如果将GridView 的EnableViewState 属性设置为 false ,则可能使多个用户无意中同时删除或编辑记录。有关更多信息,请参见警告:在使用支持编辑和/或删除功能并禁用了查看状态的ASP.NET 2.0 GridViews/DetailsView/FormViews 时的并发问题

步骤3:为 Category 与 Supplier 编辑界面使用DropDownList

回忆一下,ProductsRow 对象包含 CategoryID 、CategoryName 、SupplierID 、SupplierName 属性,这提供了 Products 数据库表中的实际外键 ID 值 ,以及Categories 与 Suppliers 表中的相应Name 值。ProductRow 的CategoryID 与 SupplierID 都可读可写,CategoryName 与 SupplierName 属性被标为只读。

因为CategoryName 与 SupplierName 属性状态为只读,所以对应BoundField 的 ReadOnly 属性也设为true ,当编辑一行时,这些值不会被修改。虽然我们可以将 ReadOnly 属性设为 false ,在编辑时将 CategoryName 与SupplierName BoundField 呈现为 TextBox ,但是当用户试图更新产品时,这种设置会导致异常,原因是没有能够输入 CategoryName 与 SupplierName 的 UpdateProduct 重载。实际上,我们不想创建这种重载,原因有两个:

  • Products 表没有 SupplierName 或 CategoryName 字段,它有的字段是 SupplierID 与 CategoryID 。因此,我们需要向我们的方法传递这些 ID 值,而不是它们的查表值。
  • 要求用户键入供应商或类别的名称这不太理想,这要求用户必须知道可填的类别与供应商,并且要知道他们的正确拼写。

供应商与类别字段应该在只读模式时显示供应商与类别的名称(现在就是这么做的),在被编辑时显示适用选项的下拉列表。通过使用下拉列表,终端用户可以很快看到可选的类别与供应商,这样更容易进行选择。

要实现该功能,我们需要将SupplierName 与 CategoryName BoundField 转换为TemplateField ,它的 ItemTemplate 显示 SupplierName 与 CategoryName 值,它的 EditItemTemplate 使用DropDownList 控件来列出可选的类别与供应商。

添加Categories 与SuppliersDropDownList

首先,将 SupplierName 与 CategoryName BoundField 转换为 TemplateField ,方法是:在GridView 的智能标记中,单击 Edit Columns 链接;选择左下方列表中的 BoundField ;单击 “Convert this field into a TemplateField ” 链接。转换过程将创建一个 TemplateField ,它有一个ItemTemplate 与一个 EditItemTemplate ,见下面的声明式语法:

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        <asp:Label ID="Label1" runat="server"
          Text='<%# Eval("CategoryName") %>'></asp:Label>
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="Label1" runat="server"
          Text='<%# Bind("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

因为BoundField 被标记为只读,所以 ItemTemplate 与 EditItemTemplate 都包含一个 Web 标签 控件,控件的Text 属性被绑定为相应的数据字段(在上面的语法中是 CategoryName)。我们需要修改 EditItemTemplate ,将 Web 标签 控件替换为 DropDownList 控件。

我们在之前的教程中看到过,模板可以通过 设计器 编辑,也可以直接在声明式语法编辑。要通过设计器编辑,请在GridView 的智能标记中单击 Edit Templates 链接,然后选为使用Category 字段的 EditItemTemplate 。删除Web 标签 控件并替换为 DropDownList 控件,将 DropDownList 的ID 属性设为 Categories 。

图5 :删除 TexBox 并向 EditItemTemplate 添加一个DropDownList

接下来,我们需要为DropDownList 填充可选的类别。从 DropDownList 的智能标记中单击 Choose Data Source 链接,然后选择创建一个新的 ObjectDataSource ,命名为CategoriesDataSource 。

图6 :创建一个新的 ObjectDataSource 控件,命名为 CategoriesDataSource

要让这个 ObjectDataSource 返回所有的类别,请将它绑定到 CategoriesBLL 类的 GetCategories() 方法。

图7 :将 ObjectDataSource 绑定到 CategoriesBLL 的 GetCategories() 方法

最后,配置DropDownList 的设置,使得每个 DropDownList ListItem 中的显示都是 CategoryName ,使用CategoryID 字段赋值。

图8 :显示为 CategoryName 字段,使用 CategoryID 赋值

当做完这些更改之后,CategoryName TemplateField 中的 EditItemTemplate 声明式标记将会包含一个 DropDownList 与一个 ObjectDataSource :

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        <asp:DropDownList ID="Categories" runat="server"
          DataSourceID="CategoriesDataSource"
          DataTextField="CategoryName" DataValueField="CategoryID">
        </asp:DropDownList>
        <asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
            OldValuesParameterFormatString="original_{0}"
            SelectMethod="GetCategories" TypeName="CategoriesBLL">
        </asp:ObjectDataSource>
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="Label1" runat="server"
          Text='<%# Bind("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

注意:必须启用 EditItemTemplate 中 DropDownList 的查看状态。我们马上将向DropDownList 的声明式语法添加数据绑定语法,类似 Eval() 与 Bind() 的数据绑定命令只能出现在启用查看状态的控件中。

重复这些步骤,向SupplierName TemplateField 的 EditItemTemplate 添加一个名为 Suppliers 的 DropDownList 。这包括向EditItemTemplate 添加一个 DropDownList ,以及创建另一个 ObjectDataSource 。然而,应该将 Suppliers DropDownList 的 ObjectDataSource 配置为调用 SuppliersBLL 类的 GetSuppliers() 方法。此外,请将 Suppliers DropDownList 配置为显示 CompanyName 字段,使用 SupplierID 字段为其ListItem 赋值。

将DropDownList 添加到两个 EditItemTemplate 之后,在浏览器中加载页面,单击Chef Anton's Cajun Seasoning 产品的 Edit 按钮。如图 9 所示,产品的类别和供应商列呈现为下拉列表,包含了可选的类别与供应商。然而,注意两个下拉列表的默认选项都是第一项 ( 类别为Beverages , 供应商为 Exotic Liquids ),即使 Chef Anton's Cajun Seasoning 是一种 Condiment ,由 New Orleans Cajun Delights 提供。

图9 :默认选项为下拉列表中的第一项

此外,如果单击Update ,将发现产品的 CategoryID 与 SupplierID 都赋 值为NULL 。这两种不期望的行为的产生,是由于 EditItemTemplate 中的DropDownList 没有绑定到底层产品数据的任何数据字段。

将 DropDownList 绑定到 CategoryID 与 SupplierID 数据字段

为了对所编辑产品的类别与供应商下拉列表适当赋值,以及当单击Update 时,将这些值送回到 BLL 的 UpdateProduct 方法,我们需要使用双向数据绑定,将DropDownList 的 SelectedValue 属性绑定到CategoryID 与 SupplierID 数据字段。为了使 Categories DropDownList 达到这点,您可以直接将SelectedValue='<%# Bind("CategoryID") %>' 添加到声明式语法。

或者,您可以设置DropDownList 的数据绑定,方法是通过 设计器 来编辑模板,并从DropDownList 的智能标记中单击 Edit DataBindings 链接。接下来,使用双向数据绑定指明 SelectedValue 属性应该被绑定到 CategoryID 字段(见图 10)。重复声明式代码或设计器过程,将 SupplierID 数据字段绑定到 Suppliers DropDownList 。

图10 :使用双向数据绑定将 CategoryID 绑定到 DropDownList 的 SelectedValue 属性

将绑定应用于两个 DropDownList 的 SelectedValue 属性之后,所编辑产品的类别与供应商列将默认为当前产品的值。单击Update 时,所选下拉列表项的 CategoryID 与 SupplierID 值将被传送到 UpdateProduct 方法。图 11 显示了添加数据绑定语句之后的本教程;请留意 Chef Anton's Cajun Seasoning 的下拉列表选项是怎样正确显示为 Condiment 与 New Orleans Cajun Delights 的。

图11 :默认选项为所编辑产品的当前类别与供应商

处理NULL 值

Products 表中的 CategoryID 与 SupplierID 列可以为 NULL ,但是EditItemTemplate 中的 DropDownList 并不包含一个能表示 NULL 值的列表项。这有两个后果:

  • 用户不能使用我们的界面将产品类别或供应商从非NULL 值变为 NULL 值
  • 如果产品具有 NULL CategoryID 或 SupplierID ,那么单击 Edit 按钮将导致异常。这是因为在 Bind() 语句中 CategoryID(或 SupplierID)返回的 NULL 值并没有映射到 DropDownList 中的一个值( 当 DropDownList 的SelectedValue 属性被设为它列表项集合中不存在的一个值时,DropDownList 就会抛出异常 ) 。

为了支持NULL CategoryID 与 SupplierID 值,我们需要为每个 DropDownList 添加另一个 ListItem ,以表示NULL 值。在使用DropDownList 的主/明细筛选 教程中,我们看到了怎样为数据绑定的DropDownList 添加另外一个 ListItem ,这包括将 DropDownList 的 AppendDataBoundItems 属性设为 true 以及手动添加另外的 ListItem 。然而,在前一篇教程中,我们添加了Value 为 -1 的 ListItem 。但是,ASP.NET 中的数据绑定逻辑会自动将空字符串转换为 NULL 值,反之亦然。因此对于本教程,我们需要将 ListItem 的 Value 设 为空字符串。

首先,将两个 DropDownList 的 AppendDataBoundItems 属性设为 true 。接下来,添加 NULL ListItem ,方法是为每个DropDownList 添加下面的 <asp:ListItem> 元素,声明式标记类似下面:

<asp:DropDownList ID="Categories" runat="server"
    DataSourceID="CategoriesDataSource" DataTextField="CategoryName"
    DataValueField="CategoryID" SelectedValue='<%# Bind("CategoryID") %>'
    AppendDataBoundItems="True">
    <asp:ListItem Value="">(None)</asp:ListItem>
</asp:DropDownList>

我选择了使用 “(None) ” 作为这个ListItem 的 Text 值,但是如果您愿意,可以将它也变为空字符串。

注意:我们在使用DropDownList 的主/ 明 细筛选 教程中看到,可以通过设计器将ListItem 添加到 DropDownList ,方法是单击 Properties 窗口中的DropDownList 的 Items 属性( 这将显示 ListItem Collection Editor )。然而对于本教程,务必通过声明式语法添加NULL ListItem 。如果您使用 ListItem Collection Editor ,那么当赋值为空字符串时,生成的声明式语法将完全忽略Value 设置,创建出这样的声明式标记:<asp:ListItem>(None)</asp:ListItem>. 尽管看起来没有什么问题,丢失的Value 会使得 DropDownList 使用Text 属性顶替它。这意味着如果选择了这个NULL ListItem ,那么就会试图将值 “(None) ” 赋给CategoryID ,这就会导致异常。通过明确设置 Value="" ,当选择 NULL ListItem 时,才会将NULL 值赋给 CategoryID 。

对Suppliers DropDownList 重复这些步骤。

有了这个附加的 ListItem ,现在编辑界面可以将 NULL 值赋给 Product 的 CategoryID 与 SupplierID 字段了,如图12 中所示。

图12 : 选择 (None) 会将产品的类别或供应商赋值为 NULL (单击此处显示实际大小的图像

步骤4 :为断货状态使用RadioButton

目前,使用的是CheckBoxField 来表现产品的 Discontinued 数据字段,这将在只读行呈现一个禁用的复选框,在正在编辑行呈现一个启用的复选框。虽然这个用户界面通常是合适的,但是如果需要,我们可以使用TemplateField 来 对它自定义。对于本教程,我们将CheckBoxField 变为 TemplateField ,它使用 RadioButtonList 控件,控件有两个选项— “Active ” 与 “Discontinued ” — 从这里,用户可以指定产品的 Discontinued 值。

首先,将Discontinued CheckBoxField 转换为 TemplateField ,这需要创建一个 TemplateField ,它有一个 ItemTemplate 与一个EditItemTemplate 。两个模板都包括一个 CheckBox ,它的Checked 属性绑定到 Discontinued 数据字段,两者的唯一区别是 ItemTemplate 的 CheckBox 的 Enabled 属性被设为 false 。

请将ItemTemplate 与 EditItemTemplate 中的CheckBox 替换为RadioButtonList 控件,将两个 RadioButtonList 的 ID 属性设为 DiscontinuedChoice 。接下来,表明每个 RadioButtonList 都应该包含两个单选按钮,其中一个的标签为 “Active ”,值为 “False ”,一个的标签为 “Discontinued ”,值为 “True ” 。要达到这个目标,您或者可以直接通过声明式语法输入 <asp:ListItem> 元素,或者通过设计器,使用 ListItem Collection Editor 。图13 显示的是指定两个单选按钮选项后的 ListItem Collection Editor 。

图13 :为 RadioButtonList 添加 “Active ” 与 “Discontinued ” 选项

因为ItemTemplate 中的 RadioButtonList 应该是不可编辑的,所以请将它的Enabled 属性设为 false ,将 EditItemTemplate 中RadioButtonList 的 Enabled 属性保持为 true 不变(默认)。这将使得非编辑行中的单选按钮为只读,但是允许用户更改编辑行的 RadioButton 值。

我们仍然需要为 RadioButtonList 控件的 SelectedValue 属性赋值,使得根据产品的 Discontinued 数据字段,选中适当的单选按钮。与本教程前面探讨过的DropDownList 一样,或者可以直接将这个数据绑定语法添加到声明式标记中,或者可以使用RadioButtonList 智能标记中的 Edit DataBindings 链接做到。

添加完两个 RadioButtonList 并配置它们之后,Discontinued TemplateField 的声明式标记看起来应该像这样:

<asp:TemplateField HeaderText="Discontinued" SortExpression="Discontinued">
    <ItemTemplate>
        <asp:RadioButtonList ID="DiscontinuedChoice" runat="server"
          Enabled="False" SelectedValue='<%# Bind("Discontinued") %>'>
            <asp:ListItem Value="False">Active</asp:ListItem>
            <asp:ListItem Value="True">Discontinued</asp:ListItem>
        </asp:RadioButtonList>
    </ItemTemplate>
    <EditItemTemplate>
        <asp:RadioButtonList ID="DiscontinuedChoice" runat="server"
            SelectedValue='<%# Bind("Discontinued") %>'>
            <asp:ListItem Value="False">Active</asp:ListItem>
            <asp:ListItem Value="True">Discontinued</asp:ListItem>
        </asp:RadioButtonList>
    </EditItemTemplate>
</asp:TemplateField>

做完这些更改之后,Discontinued 列就从一列复选框转化为了一列单选按钮对(见图 14 )。当编辑产品时,选择适当的单选按钮,通过选择另一个单选按钮并单击 Update 就可以更新产品的断货状态。

图14 :Discontinued 复选框被替换成了单选按钮对

注意: 因为 Products 数据库中的 Discontinued 列不可能有NULL 值,所以我们不需要考虑在界面中捕捉 NULL 信息。然而,如果 Discontinued 列可能包含 NULL 值,我们就需要为列表添加第三个单选按钮,将它的Value 设为空字符串(Value="" ),与我们对类别与供应商的 DropDownList 所做的一样。

小结

虽然BoundField 与 CheckBoxField 可以自动呈现只读、编辑、插入界面,但是它们缺少自定义能力。然而,我们通常需要自定义编辑或插入界面,可能要添加验证控件(如我们在前面教程中看到的)或自定义数据集用户界面(如我们在本教程中看到的)。使用 TemplateField 来自定义界面可以归纳为如下步骤:

  1. 添加 TemplateField 或将已有的 BoundField 或 CheckBoxField 转换为 TemplateField
  2. 按需要扩展界面
  3. 使用双向数据绑定,将适当的数据字段绑定到新添加的 Web 控件

除了使用内置的 ASP.NET Web 控件之外,您还可以使用自定义的、编译的服务器控件与用户控件对TemplateField 模板进行自定义。

快乐编程 !



下一篇教程