GridView 內建選取按鈕功能單一,若需單選列表 (如點選即顯示詳細資料),可加入 RadioButton 列。關鍵挑戰:HTML radio 需共用 name 屬性才能互斥,但 ASP.NET 控制項渲染時會產生唯一 ID,導致無法分組。本文示範 JavaScript 強制同步群組與伺服端取得選取值。
<asp:GridView ID="gvProducts" runat="server"
DataSourceID="odsProducts"
AutoGenerateColumns="False"
DataKeyNames="ProductID"
OnRowDataBound="gvProducts_RowDataBound"
OnSelectedIndexChanged="gvProducts_SelectedIndexChanged">
<Columns>
<asp:TemplateField HeaderText="選取">
<ItemTemplate>
<asp:RadioButton ID="rbSelect" runat="server"
GroupName="ProductSelection"
AutoPostBack="True"
OnCheckedChanged="rbSelect_CheckedChanged" />
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="ProductID" HeaderText="ID" ReadOnly="True" />
<asp:BoundField DataField="ProductName" HeaderText="產品名稱" />
<asp:BoundField DataField="UnitPrice" HeaderText="單價" DataFormatString="{0:C2}" HtmlEncode="False" />
<asp:BoundField DataField="CategoryName" HeaderText="類別" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="odsProducts" runat="server" TypeName="BLL.ProductsBLL" SelectMethod="GetProducts" />
<hr />
<h3>選取的產品詳細資料</h3>
<asp:DetailsView ID="dvProductDetail" runat="server"
AutoGenerateRows="False" Visible="False">
<Fields>
<asp:BoundField DataField="ProductID" HeaderText="ID" />
<asp:BoundField DataField="ProductName" HeaderText="名稱" />
<asp:BoundField DataField="UnitPrice" HeaderText="單價" DataFormatString="{0:C2}" HtmlEncode="False" />
<asp:BoundField DataField="UnitsInStock" HeaderText="庫存" />
<asp:BoundField DataField="CategoryName" HeaderText="類別" />
<asp:BoundField DataField="SupplierName" HeaderText="供應商" />
</Fields>
</asp:DetailsView>
ASP.NET 渲染 RadioButton 時,GroupName 會作為 HTML name 屬性,但因每列在不同 NamingContainer 中,實際輸出的 name 可能為 ctl00$MainContent$gvProducts$ctl02$rbSelect 等,導致無法互斥。
<script type="text/javascript">
function syncRadioButtons(clickedRb) {
// 找到同群組的所有 RadioButton (透過 GroupName 末尾匹配)
var groupSuffix = '_' + clickedRb.name.split('_').pop(); // 取得 rbSelect 部分
var inputs = document.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].type == 'radio' && inputs[i].name.endsWith(groupSuffix) && inputs[i] != clickedRb) {
inputs[i].checked = false;
}
}
}
</script>
protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
RadioButton rb = (RadioButton)e.Row.FindControl("rbSelect");
// 注入 onclick 呼叫同步函式
rb.Attributes["onclick"] = "syncRadioButtons(this);";
// 可選:高亮選中列
if (rb.Checked) e.Row.CssClass = "selected-row";
}
}
<ItemTemplate>
<input type="radio" name="ProductSelection" value='<%# Eval("ProductID") %>'
onclick="this.form.__EVENTTARGET.value=''; this.form.__EVENTARGUMENT.value='';" />
</ItemTemplate>
<asp:Button ID="btnViewDetail" runat="server" Text="檢視詳細" OnClick="btnViewDetail_Click" />
protected void btnViewDetail_Click(object sender, EventArgs e)
{
string selectedID = Request.Form["ProductSelection"];
if (!string.IsNullOrEmpty(selectedID))
{
dvProductDetail.DataSource = new BLL.ProductsBLL().GetProductByID(int.Parse(selectedID));
dvProductDetail.DataBind();
dvProductDetail.Visible = true;
}
}
protected void rbSelect_CheckedChanged(object sender, EventArgs e)
{
RadioButton rb = (RadioButton)sender;
if (rb.Checked)
{
GridViewRow row = (GridViewRow)rb.NamingContainer;
int productID = (int)gvProducts.DataKeys[row.RowIndex].Value;
dvProductDetail.DataSource = new BLL.ProductsBLL().GetProductByID(productID);
dvProductDetail.DataBind();
dvProductDetail.Visible = true;
// 高亮選中列
foreach (GridViewRow r in gvProducts.Rows) r.CssClass = "";
row.CssClass = "selected-row";
}
}
<style>
.selected-row { background-color: #ffffcc !important; }
.selected-row td { border-top: 2px solid #ffcc00; border-bottom: 2px solid #ffcc00; }
</style>
RadioButton 單選天然不需跨頁保持 (同時間只能選一項),但若需記住使用者上次選取:
private int? LastSelectedProductID
{
get { return ViewState["LastSelectedProductID"] as int?; }
set { ViewState["LastSelectedProductID"] = value; }
}
protected void gvProducts_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
gvProducts.PageIndex = e.NewPageIndex;
gvProducts.DataBind();
// 還原選中狀態 (若選中項在當前頁)
if (LastSelectedProductID.HasValue)
{
foreach (GridViewRow row in gvProducts.Rows)
{
if ((int)gvProducts.DataKeys[row.RowIndex].Value == LastSelectedProductID.Value)
{
RadioButton rb = (RadioButton)row.FindControl("rbSelect");
rb.Checked = true;
rbSelect_CheckedChanged(rb, EventArgs.Empty); // 觸發詳細資料載入
break;
}
}
}
}
protected void rbSelect_CheckedChanged(object sender, EventArgs e)
{
RadioButton rb = (RadioButton)sender;
if (rb.Checked)
{
GridViewRow row = (GridViewRow)rb.NamingContainer;
int productID = (int)gvProducts.DataKeys[row.RowIndex].Value;
LastSelectedProductID = productID;
// ... 載入詳細資料
}
}
| 特性 | TemplateField RadioButton | CommandField SelectButton |
|---|---|---|
| 視覺呈現 | 圓形單選按鈕,直觀 | 連結/按鈕文字「選取」 |
| 互斥邏輯 | 需 JS/原生 input 修正 | 框架自動處理 (SelectedIndex) |
| AutoPostBack | 支援 (需 JS 同步) | 內建 PostBack |
| 取得選取值 | DataKeys[RowIndex] 或 Request.Form | SelectedDataKey / SelectedIndex |
| 樣式客製 | 高 (CSS/JS 完全控制) | 中 (ControlStyle/RowStyle) |
GridView RadioButton 列雖需額外 JavaScript 修正群組名稱,但提供更直觀的單選體驗。關鍵技巧:GroupName 設定、RowDataBound 注入 onclick 同步函式、CheckedChanged 取得 DataKey 載入詳細資料。現代專案建議改用客戶端框架 (Vue/React/Blazor) 管理選取狀態,或採用原生 input type="radio" 配合按鈕提交,簡化伺服端邏輯。