让 CheckBoxList 支持多对多关系
以声明式方式实现多对多选择功能的示例。
引言
再一次,我对 ASP.NET (2.0) 中许多控件缺乏基本 GUI 功能感到惊讶,而且寄予厚望的新一代 Web 控件一次又一次地让我们失望!这次我的问题是允许用户为某个项目,从一个复选框列表中(最好使用 CheckBoxList
控件)选择一个或多个相关项目。这种关系存储在数据库中作为多对多关系,其中连接数据库表包含相关项目的键。
我认为 CheckBoxList
控件的一个合理要求是,在从数据库读取时,能够恢复用户之前保存的选择。然而,这并不像应该的那么简单!我在网上看到了许多实现此功能的示例,但没有一个使用新的 SqlDataSource
控件的声明式方法,我这次尝试遵循了这一点……为什么你总是需要扩展基本控件才能获得基本功能!?!
我很抱歉我的消极态度……但通过一些富有创意的(声明式)编程和一些代码修复(非声明式),我设法得到了我想要的(而不必创建自己的派生类)。所以,本文包含解决此问题的代码片段!而且,如果你和我写这篇文章之前的状态一样,也许这篇文章会让你重新找回方向!?
背景
解决此问题的最理想方法是创建我自己的控件,扩展 CheckBoxList
控件的功能。我的方法是创建一个新的属性“DataSelectedField
”,用于在将控件数据绑定到 SqlDataSource
控件时使用,就像 DataTextField
和 DataValueField
一样。但是,我不选择这种方法是因为原则上不想自己完成所有工作,而且 CheckBoxList
应该已经具备了这些功能!此外,我决心给声明式编程风格一个机会!(不过,如果你已经实现了这一点,请告诉我!;-)
代码
我的数据库表是 A 和 B,有一个连接表 A_B,存储它们之间的关联,其中包含外键列 A_B.AID 和 A_B.BID。重点是 A,它需要与零个或多个 B 相关联。以下 Select
语句检索 B 中的所有行,以及它们与特定 A 的潜在关联(读取 LEFT JOIN
)
SELECT B.BID, B.Name,
CASE
WHEN A_B.AID IS NULL THEN CAST(B.BID AS varchar(36))
ELSE (CAST(B.BID AS varchar(36)) + '_' + CAST(A_B.AID AS varchar(36)))
END AS DataValueField
FROM B
LEFT OUTER JOIN A_B ON (B.BID = A_B.BID AND A_B.AID = @AID)
除了 BID 和 Name,查询还返回用于知道是否应预选项目的“DataValueField
”的值,即,预选项目也应该将“<下划线>A_B.AID”连接到它。@AID
是我们关注的当前 A。(所有 ID 都是uniqueidentifier 类型,并在客户端上作为文本处理。)
现在您知道了数据模型。我从 CheckBoxList
开始,然后向下……因此,CheckBoxList
的代码如下所示:
<asp:CheckBoxList ID="checkBoxList" runat="server"
DataSourceID="sqlDataSource"
DataTextField="Name"
DataValueField="DataValueField"
...
</asp:CheckBoxList>
引用的 SqlDataSource
代码如下所示:
<asp:SqlDataSource ID="sqlDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:ConnectionString %>"
SelectCommand="<the query as above>"
InsertCommand="INSERT INTO A_B SELECT @AID, BID FROM B WHERE BID IN ({0})"
DeleteCommand="DELETE FROM A_B WHERE AID = @AID">
<SelectParameters>
<asp:SessionParameter Name="AID" SessionField="ID" />
</SelectParameters>
<InsertParameters>
<asp:SessionParameter Name="AID" SessionField="ID" />
</InsertParameters>
<DeleteParameters>
<asp:SessionParameter Name="AID" SessionField="ID" />
</DeleteParameters>
</asp:SqlDataSource>
请注意 InsertCommand
的格式占位符(即 {0}),稍后将使用它。但在我们深入将信息保存到数据库之前,我们必须准备 CheckBoxList
以显示预选项目,如下所示:
foreach (ListItem listItem in checkBoxList.Items)
{
if (listItem.Value != string.Empty)
{
string [] split = listItem.Value.Split(new Char[] {'_'});
listItem.Value = split[0];
listItem.Selected = (split.Length > 1) ? true : false;
}
}
这是将 DataValueField
转换为 Value
和 Selected
(Checked
,当它从 ListItem
转换为 CheckBox
控件时)字段的地方。此代码应在控件数据绑定后执行,这意味着在 DataBound
事件触发后。
好了,现在我们已经渲染了列表,我们回到保存部分。连接表的核心问题在于,当用户保存时要进行的“更新”实际上是删除未选中的项目并插入新选中的项目。我们需要自己处理这个问题,同时使用插入和删除命令,触发相应的操作。因此,我们不使用 SqlDataSource
的预定义命令,而是从代码中手动触发更新(但我们仍然“声明”了查询)。
这是用于控制更新并调整插入命令的代码:
// Run the DELETE-statement first.
sqlDataSource.Delete();
// Construct the sql INSERT-statement for selected items:
StringBuilder sb = new StringBuilder();
foreach (ListItem listItem in checkBoxList.Items)
{
if (listItem.Selected)
{
if (sb.Length > 0)
sb.Append(",");
sb.Append("'");
sb.Append(listItem.Value);
sb.Append("'");
}
}
if (sb.Length > 0)
{
sqlDataSource.InsertCommand =
string.Format(
sqlDataSource.InsertCommand,
sb.ToString());
sqlDataSource.Insert();
}
代码很简单,首先删除所有内容,然后构造 IN 语句列表以对格式占位符执行 string.Format
,然后添加列表中所有选中的项目。此代码可以放置在任何地方,例如,在 Button
的 Click
事件中。
历史
- 2007-06-29:初始版本……欢迎提供反馈!