自定义数据修改界面 (C#)

作者 :Scott Mitchell

下载 PDF

本教程介绍如何通过将标准 TextBox 和 CheckBox 控件替换为备用输入 Web 控件来自定义可编辑 GridView 的界面。

简介

GridView 和 DetailsView 控件使用的 BoundFields 和 CheckBoxFields 简化了修改数据的过程,因为他们能够呈现只读、可编辑和可插入的接口。 无需添加任何其他声明性标记或代码即可呈现这些接口。 但是,BoundField 和 CheckBoxField 的接口缺少实际方案中通常需要的可自定义性。 为了在 GridView 或 DetailsView 中自定义可编辑或可插入的界面,我们需要改用 TemplateField。

前面的教程中 ,我们了解了如何通过添加验证 Web 控件来自定义数据修改接口。 本教程介绍如何自定义实际数据收集 Web 控件,并将 BoundField 和 CheckBoxField 的标准 TextBox 和 CheckBox 控件替换为备用输入 Web 控件。 具体而言,我们将构建一个可编辑的 GridView,允许更新产品名称、类别、供应商和停产状态。 编辑特定行时,类别和供应商字段将呈现为 DropDownLists,其中包含一组可供选择的可用类别和供应商。 此外,我们将用 RadioButtonList 控件替换 CheckBoxField 的默认 CheckBox,该控件提供两个选项:“活动”和“已停止”。

GridView 的编辑界面包括 DropDownLists 和 RadioButtons

图 1:GridView 的编辑界面包括 DropDownLists 和 RadioButtons (单击以查看全尺寸图像)

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

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

  1. 从数据库中检索指定 ProductID的产品信息
  2. 更新 、ProductNameCategoryIDSupplierIDDiscontinued 字段,以及
  3. 通过 TableAdapter 的 Update() 方法将更新请求发送到 DAL。

为简洁起见,对于此特定重载,我省略了业务规则检查,以确保标记为已停产的产品不是其供应商提供的唯一产品。 如果愿意,或者最好将逻辑重构为单独的方法,请随意将其添加到 中。

以下代码显示了 类中的ProductsBLLUpdateProduct重载:

[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。 CustomizedUI.aspx打开 文件夹中的页面,EditInsertDelete并将 GridView 控件添加到Designer。 接下来,从 GridView 的智能标记创建新的 ObjectDataSource。 将 ObjectDataSource 配置为通过 ProductBLL 类的 GetProducts() 方法检索产品信息,并使用 UpdateProduct 刚刚创建的重载更新产品数据。 从“插入”和“删除”选项卡中,从下拉列表中选择“ (无) ”。

将 ObjectDataSource 配置为使用刚刚创建的 UpdateProduct 重载

图 2:将 ObjectDataSource 配置为使用 UpdateProduct 刚刚创建的重载 (单击以查看全尺寸图像)

正如我们在整个数据修改教程中看到的那样,Visual Studio 创建的 ObjectDataSource 的声明性语法将 OldValuesParameterFormatString 属性 original_{0}分配给 。 当然,这不适用于业务逻辑层,因为我们的方法不需要传入原始 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、、 SupplierNameCategoryName BoundFields 和 Discontinued CheckBoxField
  • 显示在 CategoryName checkBoxField) 左侧 Discontinued (之前显示的 和 SupplierName 字段
  • CategoryNameSupplierName BoundFields 的 HeaderText 属性分别设置为“Category”和“Supplier”
  • (检查 GridView 的智能标记中的“启用编辑”复选框启用编辑支持)

完成这些更改后,Designer将类似于图 3,GridView 的声明性语法如下所示。

从 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 中呈现为一行,显示产品名称、类别、供应商和停产状态。

GridView 的 Read-Only 接口已完成

图 4:GridView 的 Read-Only 界面已完成 (单击以查看全尺寸图像)

注意

插入、更新和删除数据概述教程中所述,启用 GridView 视图状态 (默认行为) 至关重要。 如果将 GridView 的 EnableViewState 属性设置为 false,则存在并发用户无意中删除或编辑记录的风险。

步骤 3:对类别和供应商编辑接口使用 DropDownList

回想一下, ProductsRow 对象包含 CategoryIDCategoryNameSupplierIDSupplierName 属性,这些属性提供数据库表中的实际外键 ID 值Products和 和 Suppliers 表中的相应NameCategoriesProductRowCategoryIDSupplierID 可以读取和写入,而 CategoryNameSupplierName 属性标记为只读。

由于 和 SupplierName 属性的CategoryName只读状态,相应的 BoundFields 已将其ReadOnly属性设置为 true,以防止在编辑行时修改这些值。 虽然我们可以将 ReadOnly 属性设置为 false,但在编辑过程中将 和 SupplierName BoundFields 呈现CategoryName为 TextBoxes,但当用户尝试更新产品时,此方法将导致异常,因为没有任何UpdateProduct重载会占用 CategoryNameSupplierName 输入。 事实上,我们不希望创建这样的重载,原因有两个:

  • Products 表没有 SupplierNameCategoryName 字段,但 SupplierID 具有 和 CategoryID。 因此,我们希望方法传递这些特定的 ID 值,而不是其查找表的值。
  • 要求用户键入供应商或类别的名称不太理想,因为它要求用户知道可用的类别和供应商及其正确的拼写。

当处于只读模式时,供应商和类别字段应显示类别和供应商的名称 (,因为它现在) ,并在编辑时显示适用选项的下拉列表。 使用下拉列表,最终用户可以快速查看哪些类别和供应商可供选择,并且可以更轻松地进行选择。

若要提供此行为,我们需要将 和 BoundField 转换为 SupplierName TemplateFields,后者ItemTemplate发出 SupplierNameCategoryName 值,并使用 EditItemTemplate DropDownList 控件列出可用的类别和供应商。CategoryName

Categories添加 和SuppliersDropDownLists

首先,通过以下方式将 SupplierNameCategoryName BoundField 转换为 TemplateFields:单击 GridView 的智能标记中的“编辑列”链接;从左下角的列表中选择 BoundField;然后单击“将此字段转换为 TemplateField”链接。 转换过程将创建包含 ItemTemplateEditItemTemplate的 TemplateField,如下面的声明性语法所示:

<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 被标记为只读,因此 和 EditItemTemplate 都包含ItemTemplate一个标签 Web 控件,该控件的 Text 属性绑定到 (适用的数据字段CategoryName,按照上述语法) 。 我们需要修改 EditItemTemplate,将 Label Web 控件替换为 DropDownList 控件。

正如我们在前面的教程中看到的那样,可以通过 Designer或直接从声明性语法编辑模板。 若要通过Designer对其进行编辑,请单击 GridView 智能标记中的“编辑模板”链接,然后选择使用“类别”字段的 EditItemTemplate。 删除标签 Web 控件并将其替换为 DropDownList 控件,并将 DropDownList 的 ID 属性设置为 Categories

删除 TexBox 并将 DropDownList 添加到 EditItemTemplate

图 5:删除 TexBox 并将 DropDownList 添加到 EditItemTemplate (单击以查看全尺寸图像)

接下来,需要使用可用类别填充 DropDownList。 单击 DropDownList 的智能标记中的“选择数据源”链接,并选择创建名为 CategoriesDataSource的新 ObjectDataSource。

创建名为 CategoriesDataSource 的新 ObjectDataSource 控件

图 6:创建名为 CategoriesDataSource 的新 ObjectDataSource 控件 (单击以查看全尺寸图像)

若要使此 ObjectDataSource 返回所有类别,请将其绑定到 CategoriesBLL 类的 GetCategories() 方法。

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

图 7:将 ObjectDataSource 绑定到 CategoriesBLLGetCategories() 方法 (单击以查看全尺寸图像)

最后,配置 DropDownList 的设置,使 CategoryName 字段显示在每个 DropDownList ListItem 中, CategoryID 并将字段用作值。

显示 CategoryName 字段并将 CategoryID 用作值

图 8:显示 CategoryName 字段和 CategoryID 用作值 (单击以查看全尺寸图像)

进行这些更改后,TemplateField 中 CategoryNameEditItemTemplate声明性标记将同时包含 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() )中,只能出现在启用了视图状态的控件中。

重复这些步骤,将名为 的 Suppliers DropDownList 添加到 SupplierName TemplateField 的 EditItemTemplate。 这将涉及将 DropDownList 添加到 EditItemTemplate 并创建另一个 ObjectDataSource。 Suppliers但是,DropDownList 的 ObjectDataSource 应配置为调用SuppliersBLL类的 GetSuppliers() 方法。 此外,配置 Suppliers DropDownList 以显示字段 CompanyName ,并使用 字段 SupplierID 作为其 ListItem 的值。

将 DropDownLists 添加到这两 EditItemTemplate 个 s 后,在浏览器中加载页面,然后单击 Chef Anton 的 Cajun 调味品的“编辑”按钮。 如图 9 所示,产品的类别和供应商列呈现为下拉列表,其中包含可供选择的可用类别和供应商。 但是,请注意,默认情况下,两个下拉列表中的 第一 个项都选择 (饮料作为类别,异国情调的液体作为供应商) ,即使主厨安东的Cajun调味料是由新奥尔良卡琼喜悦提供的调味品。

默认情况下,Drop-Down Lists 中的第一项处于选中状态

图 9:默认选中 Drop-Down Lists 中的第一项 (单击以查看全尺寸图像)

此外,如果单击“更新”,你会发现产品的 CategoryIDSupplierID 值设置为 NULL。 这两种不需要的行为都是由于 中的 DropDownLists EditItemTemplate 未绑定到基础产品数据中的任何数据字段而引起的。

将 DropDownList 绑定到CategoryIDSupplierID数据字段

若要将编辑的产品类别和供应商下拉列表设置为适当的值,并在单击“更新”时将这些值发送回 BLL 的方法 UpdateProduct ,我们需要使用双向数据绑定将 DropDownLists 的属性 SelectedValue 绑定到 CategoryIDSupplierID 数据字段。 若要使用 Categories DropDownList 完成此操作,可以直接将 添加到 SelectedValue='<%# Bind("CategoryID") %>' 声明性语法。

或者,可以通过Designer编辑模板并单击 DropDownList 的智能标记中的“编辑数据绑定”链接来设置 DropDownList 的数据绑定。 接下来,指示 SelectedValue 应使用双向数据绑定 (属性绑定到 CategoryID 字段,请参阅图 10) 。 重复声明性或Designer过程,将数据SupplierID字段绑定到 Suppliers DropDownList。

使用 Two-Way 数据绑定将 CategoryID 绑定到 DropDownList 的 SelectedValue 属性

图 10:使用 Two-Way 数据绑定将 绑定到 CategoryID DropDownList 的 SelectedValue 属性 (单击 以查看全尺寸图像)

将绑定应用于 SelectedValue 两个 DropDownList 的属性后,已编辑的产品类别和供应商列将默认为当前产品的值。 单击“更新”时, CategoryID 所选下拉列表项的 和 SupplierID 值将传递到 UpdateProduct 方法。 图 11 显示了添加数据绑定语句后的教程;请注意主厨安东的 Cajun 调味料的选定下拉列表项如何正确调味品和新奥尔良 Cajun 欢乐。

默认情况下,已编辑产品的当前类别和供应商值处于选中状态

图 11:默认选择已编辑产品的当前类别和供应商值 (单击以查看全尺寸图像)

处理NULL

CategoryID表中的 ProductsSupplierID 列可以是 NULL,但 中的 DropDownLists EditItemTemplate 不包含表示NULL值的列表项。 这会产生两个后果:

  • 用户无法使用我们的界面将产品的类别或供应商从非值更改为NULLNULL
  • 如果产品具有 NULLCategoryIDSupplierID,则单击“编辑”按钮将导致异常。 这是因为 NULL (或 SupplierID) 在 语句中Bind()返回CategoryID的值不会映射到 DropDownList 中的值, (当其SelectedValue属性设置为不在) 列表项集合中的值时,DropDownList 将引发异常。

为了支持 NULLCategoryIDSupplierID 值,我们需要将另一个 ListItem 添加到每个 DropDownList 以表示 NULL 值。 在 使用 DropDownList 筛选母版/详细信息筛选 教程中,我们了解了如何向数据绑定的 DropDownList 添加附加 ListItem 项,其中涉及将 DropDownList 的 AppendDataBoundItems 属性设置为 true 并手动添加附加 ListItem的 。 但是,在上一教程中,我们添加了 ListItem 的 , Value-1。 但是,ASP.NET 中的数据绑定逻辑会自动将空白字符串转换为 NULL 值,反之亦然。 因此,在本教程中, ListItem我们希望 的 Value 为空字符串。

首先,将两个 DropDownLists 属性 AppendDataBoundItems 都设置为 true。 接下来,通过将以下<asp:ListItem>元素添加到每个 DropDownList 来添加 NULLListItem ,使声明性标记如下所示:

<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可以通过Designer将 属性窗口 (中的 DropDownList 属性添加到 DropDownList Items 中,该属性将显示ListItem集合编辑器) 。 但是,请务必通过声明性语法为本教程添加 NULLListItem 。 如果使用ListItem集合编辑器,在分配空白字符串时,生成的声明性语法将完全省略Value该设置,从而创建声明性标记,如:<asp:ListItem>(None)</asp:ListItem>。 虽然这看起来可能无害,但缺少 Value 会导致 DropDownList 在其位置使用 Text 属性值。 这意味着,如果 NULLListItem 选择此选项,将尝试将值“ (None) ”分配给 CategoryID,这将导致异常。 通过显式设置 Value="",当选择 时ListItemNULL,将向其分配CategoryID一个NULL值。

对“供应商”DropDownList 重复这些步骤。

借助此附加 ListItem功能,编辑界面现在可以将值分配给 NULL Product 的 CategoryIDSupplierID 字段,如图 12 所示。

选择“无 () 为产品的类别或供应商分配 NULL 值

图 12:选择“无 (”) 为产品的类别或供应商分配 NULL 值 (单击以查看全尺寸图像)

步骤 4:将 RadioButtons 用于停产状态

目前,产品 Discontinued 的数据字段使用 CheckBoxField 表示,该字段为只读行呈现禁用复选框,为正在编辑的行呈现已启用复选框。 虽然此用户界面通常适用,但如果需要,可以使用 TemplateField 对其进行自定义。 在本教程中,让我们将 CheckBoxField 更改为使用 RadioButtonList 控件的 TemplateField,其中包含两个选项“活动”和“已停用”,用户可以从中指定产品 Discontinued 的值。

首先,将 Discontinued CheckBoxField 转换为 TemplateField,这将创建具有 和 EditItemTemplateItemTemplate TemplateField。 这两个模板都包含一个 CheckBox,其属性绑定到Discontinued数据字段,这两个模板的唯一区别是ItemTemplate,“CheckBox”的 Enabled 属性设置为 falseChecked

将 和 中的 ItemTemplate CheckBox 替换为 RadioButtonList 控件,并将两个 RadioButtonLists 属性ID都设置为 DiscontinuedChoiceEditItemTemplate 接下来,指示 RadioButtonLists 应各包含两个单选按钮,一个按钮标记为“活动”,值为“False”,一个标记为“已停用”,值为“True”。 为此,可以通过声明性语法直接在 中输入<asp:ListItem>元素,或使用ListItemDesigner中的集合编辑器。 图 13 显示了ListItem指定了两个单选按钮选项后的集合编辑器。

添加

图 13:将“活动”和“已停用”选项添加到 RadioButtonList (单击以查看全尺寸图像)

由于 中的 ItemTemplate RadioButtonList 不应是可编辑的,因此将其 Enabled 属性设置为 false,使 Enabled 属性 true (中 EditItemTemplateRadioButtonList 的默认) 。 这会使未编辑行中的单选按钮成为只读按钮,但允许用户更改已编辑行的 RadioButton 值。

我们仍然需要分配 RadioButtonList 控件 SelectedValue 的属性,以便根据产品 Discontinued 的数据字段选择相应的单选按钮。 与本教程前面介绍的 DropDownList 一样,此数据绑定语法可以直接添加到声明性标记中,也可以通过 RadioButtonLists 智能标记中的“编辑数据绑定”链接进行添加。

添加两个 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) 。 编辑产品时,会选择相应的单选按钮,并通过选择另一个单选按钮并单击“更新”来更新产品的停产状态。

停产的复选框已被单选按钮对替换

图 14:停用的复选框已被单选按钮对替换 (单击以查看全尺寸图像)

注意

由于数据库中的DiscontinuedProducts列不能包含NULL值,因此无需担心在接口中捕获NULL信息。 但是,如果列可能包含值,Discontinued我们希望将第三个单选按钮添加到列表,并将其Value设置为空字符串 (Value="") ,就像类别和供应商 DropDownLists 一NULL样。

总结

虽然 BoundField 和 CheckBoxField 自动呈现只读、编辑和插入接口,但它们缺乏自定义功能。 但是,我们通常需要自定义编辑或插入界面,可能添加验证控件 (,如前面的教程) 所示,或通过自定义数据收集用户界面 (,如本教程) 所示。 可通过以下步骤总结使用 TemplateField 自定义接口:

  1. 添加 TemplateField 或将现有 BoundField 或 CheckBoxField 转换为 TemplateField
  2. 根据需要扩充接口
  3. 使用双向数据绑定将适当的数据字段绑定到新添加的 Web 控件

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

编程快乐!

关于作者

斯科特·米切尔是七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直在使用 Microsoft Web 技术。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上mitchell@4GuysFromRolla.com联系他,也可以通过他的博客(可在 中找到http://ScottOnWriting.NET)。