ASP.NET WebForms:GridView 里的 DropDownList 绑定到不存在的值时,如何统一显示空白

| .NET | 2 Reads

最近在处理 ASP.NET WebForms 的 GridView 时,遇到了一个很典型的问题:

GridView 里的 DropDownList 是通过 SqlDataSource 绑定选项的,但数据库里传进来的值,未必一定存在于当前 DropDownList.Items 里面。

比如:

数据库值:999
DropDownList 选项:001, 002, 003

这时候如果直接写:

SelectedValue='<%# Bind("XXX") %>'

WebForms 很容易直接报错:

'ddlXXX' has a SelectedValue which is invalid because it does not exist in the list of items.

WinForms 和 WebForms 的差异

在 WinForms 里,如果绑定到了不存在的选项,很多情况下画面上会自动显示为空白。

但 ASP.NET WebForms 的 DropDownList 没有这种自动兜底机制。

WebForms 的逻辑比较严格:

SelectedValue 必须存在于 Items 中

如果不存在,就会抛异常。

所以不能期待它像 WinForms 一样“找不到就自动显示空白”。

不推荐的写法

下面这种写法虽然方便,但风险比较高:

<asp:DropDownList ID="ddlXXX" runat="server"
    DataSourceID="sdsXXX"
    DataTextField="Name"
    DataValueField="Code"
    SelectedValue='<%# Bind("XXX") %>'>
</asp:DropDownList>

问题在于,Bind("XXX") 会在绑定过程中直接设置 SelectedValue

如果这个值不存在于 Items 里,就可能在你做任何补救处理之前先报错。

推荐做法:统一在 DataBound 里安全设置

比较稳的做法是:

不要在 aspx 里直接写 SelectedValue='<%# Bind(...) %>'
而是在 DropDownList 的 DataBound 事件里统一判断

也就是先让 DropDownList 完成自己的选项绑定,然后再判断数据库里的值是否存在。

如果存在,就正常选中。

如果不存在,就选中空白项。

共通方法

可以先准备一个共通方法:

Protected Sub SafeSetDropDownValue(ddl As DropDownList, value As Object)
    Dim v As String = If(value Is Nothing OrElse value Is DBNull.Value, "", value.ToString())

    ' 值为空时,显示空白
    If String.IsNullOrEmpty(v) Then
        If ddl.Items.FindByValue("") Is Nothing Then
            ddl.Items.Insert(0, New ListItem("", ""))
        End If

        ddl.SelectedValue = ""
        Return
    End If

    ' 值存在于 DropDownList 中时,正常选中
    If ddl.Items.FindByValue(v) IsNot Nothing Then
        ddl.SelectedValue = v
    Else
        ' 值不存在时,显示空白
        If ddl.Items.FindByValue("") Is Nothing Then
            ddl.Items.Insert(0, New ListItem("", ""))
        End If

        ddl.SelectedValue = ""
    End If
End Sub

然后在各个 DropDownListDataBound 事件里调用:

Protected Sub ddlXXX_DataBound(sender As Object, e As EventArgs)
    Dim ddl As DropDownList = CType(sender, DropDownList)
    Dim row As GridViewRow = CType(ddl.NamingContainer, GridViewRow)

    Dim value As Object = DataBinder.Eval(row.DataItem, "XXX")

    SafeSetDropDownValue(ddl, value)
End Sub

aspx 侧可以这样写:

<asp:DropDownList ID="ddlXXX" runat="server"
    DataSourceID="sdsXXX"
    DataTextField="Name"
    DataValueField="Code"
    AppendDataBoundItems="True"
    OnDataBound="ddlXXX_DataBound">
    <asp:ListItem Text="" Value=""></asp:ListItem>
</asp:DropDownList>

这样处理后的效果

数据库值存在于选项中:

数据库值:001
选项:001, 002, 003
结果:显示 001

数据库值不存在于选项中:

数据库值:999
选项:001, 002, 003
结果:显示空白

数据库值本来就是空:

数据库值:NULL / ""
结果:显示空白

为什么不建议继续用 Bind

如果坚持使用:

SelectedValue='<%# Bind("XXX") %>'

理论上也可以通过 DataBinding 事件提前插入一个项目来避免报错。

例如:

Protected Sub ddlXXX_DataBinding(sender As Object, e As EventArgs)
    Dim ddl As DropDownList = CType(sender, DropDownList)
    Dim row As GridViewRow = CType(ddl.NamingContainer, GridViewRow)

    Dim value As String = Convert.ToString(DataBinder.Eval(row.DataItem, "XXX"))

    If Not String.IsNullOrEmpty(value) Then
        If ddl.Items.FindByValue(value) Is Nothing Then
            ddl.Items.Insert(0, New ListItem("", value))
        End If
    End If
End Sub

但这个做法有一个问题:

Text = ""
Value = "999"

也就是说,画面上虽然显示空白,但实际选中的值仍然是 999

如果用户不修改这个下拉框,更新时这个旧值可能又会被提交回去。

所以如果目标是“值不存在时显示空白,并且视为未选择”,那么更推荐取消 SelectedValue='<%# Bind(...) %>',改成在 DataBound 里自己控制。

总结

ASP.NET WebForms 的 DropDownList 不像 WinForms 那样会自动容错。

在 GridView 里使用 DropDownList 时,如果数据库值不一定存在于选项列表中,最稳的处理方式是:

取消 aspx 里的 SelectedValue='<%# Bind(...) %>'
在 DropDownList 的 DataBound 事件中使用 Items.FindByValue 判断
存在就选中,不存在就显示空白

这样可以一次性解决 GridView 内多个 DropDownList 绑定到不存在选项时报错的问题,也能避免因为旧数据、权限过滤、选项变更等原因导致页面直接崩掉。

This article was last edited at