`,再 `__doPostBack` 觸發伺服器端事件。
前端 .aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="GridViewSelectDemo.aspx.cs" Inherits="Demo.GridViewSelectDemo" %>
<!DOCTYPE html>
<html><head><title>GridView 列選中互動</title>
<style>
.gv { border-collapse: collapse; width: 100%; font-family: 'Microsoft JhengHei', sans-serif; }
.gv th { background: #007bff; color: white; padding: 10px; text-align: left; cursor: pointer; }
.gv td { padding: 8px 10px; border-bottom: 1px solid #eee; vertical-align: middle; }
.gv tbody tr { cursor: pointer; transition: background 0.2s; }
.gv tbody tr:hover { background: #e3f2fd !important; } /* 懸停最高優先級 */
.gv tbody tr.selected { background: #cce5ff !important; font-weight: bold; } /* 選中狀態 */
.hidden-col { display: none; } /* 隱藏主鍵欄 */
</style></head><body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<h2>員工資料列表 (點選列查看明細)</h2>
<!-- 隱藏欄位:儲存目前選中的 EmployeeID -->
<asp:HiddenField ID="hfSelectedId" runat="server" OnValueChanged="hfSelectedId_ValueChanged" />
<asp:UpdatePanel ID="upGrid" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<asp:GridView ID="gvEmployees" runat="server"
AutoGenerateColumns="False"
DataKeyNames="EmployeeID"
CssClass="gv"
OnRowDataBound="gvEmployees_RowDataBound"
AllowPaging="True" PageSize="10"
OnPageIndexChanging="gvEmployees_PageIndexChanging"
AllowSorting="True"
OnSorting="gvEmployees_Sorting">
<Columns>
<asp:BoundField DataField="EmployeeID" HeaderText="編號" ItemStyle-CssClass="hidden-col" HeaderStyle-CssClass="hidden-col" />
<asp:BoundField DataField="LastName" HeaderText="姓氏" />
<asp:BoundField DataField="FirstName" HeaderText="名字" />
<asp:BoundField DataField="Title" HeaderText="職稱" />
<asp:BoundField DataField="HireDate" HeaderText="到職日" DataFormatString="{0:yyyy/MM/dd}" HtmlEncode="False" />
<asp:BoundField DataField="City" HeaderText="城市" />
</Columns>
<PagerSettings Mode="NumericFirstLast" PageButtonCount="5" />
<PagerStyle CssClass="pagination" HorizontalAlign="Center" />
</asp:GridView>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="gvEmployees" EventName="PageIndexChanging" />
<asp:AsyncPostBackTrigger ControlID="gvEmployees" EventName="Sorting" />
</Triggers>
</asp:UpdatePanel>
<div style="margin-top:20px; padding:15px; background:#f8f9fa; border:1px solid #dee2e6;">
<h4>選中員工明細</h4>
<asp:DetailsView ID="dvDetail" runat="server" AutoGenerateRows="True" CssClass="table table-bordered" />
</div>
</form>
<script>
(function () {
var gv = document.getElementById('<%= gvEmployees.ClientID %>');
var hf = document.getElementById('<%= hfSelectedId.ClientID %>');
var lastTr = null;
if (!gv) return;
// 事件委派:針對 tbody tr
gv.querySelector('tbody').addEventListener('click', function (e) {
var tr = e.target.closest('tr');
if (!tr || tr === gv.querySelector('thead tr')) return;
// 取得 DataKey (EmployeeID) - 從隱藏欄位或 data 屬性
var key = tr.cells[0].innerText.trim(); // 第 0 欄為 EmployeeID (隱藏但存在 DOM)
// 或使用 data-key: tr.dataset.key (見 RowDataBound 註冊)
// 切換 selected class
if (lastTr) lastTr.classList.remove('selected');
tr.classList.add('selected');
lastTr = tr;
// 更新隱藏欄位並觸發伺服器端 ValueChanged
if (hf.value !== key) {
hf.value = key;
// 手動觸發 change 事件 (ASP.NET HiddenField 需此才引發 OnValueChanged)
if ('onpropertychange' in hf) hf.fireEvent('onpropertychange'); // IE
else {
var evt = document.createEvent('HTMLEvents');
evt.initEvent('change', true, false);
hf.dispatchEvent(evt);
}
}
});
// 分頁/排序後重新套用選中狀態 (UpdatePanel 非同步更新後)
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_endRequest(function () {
gv = document.getElementById('<%= gvEmployees.ClientID %>'); // 重新取得
if (gv && hf.value) {
var rows = gv.querySelectorAll('tbody tr');
rows.forEach(function (tr) {
if (tr.cells[0].innerText.trim() === hf.value) {
tr.classList.add('selected');
lastTr = tr;
}
});
}
});
})();
</script>
後端 .aspx.cs
using System;
using System.Data;
using System.Web.UI.WebControls;
namespace Demo
{
public partial class GridViewSelectDemo : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
BindGrid();
}
}
private void BindGrid()
{
// 模擬資料來源 (實務從 DB / EF / Service 取得)
DataTable dt = GetEmployeeData();
gvEmployees.DataSource = dt;
gvEmployees.DataBind();
}
// 核心:RowDataBound 註冊 data-key 與滑鼠樣式
protected void gvEmployees_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
// 1. 取得主鍵 (DataKeyNames 設定的欄位)
string empId = gvEmployees.DataKeys[e.Row.RowIndex].Value.ToString();
// 2. 註冊 data-key 供 JS 讀取 (比解析 innerText 更可靠)
e.Row.Attributes["data-key"] = empId;
// 3. 滑鼠懸停樣式 (CSS :hover 已處理,此處可加強制游標)
e.Row.Style["cursor"] = "pointer";
// 4. 若為目前選中列,加上 selected class (配合 endRequest 重新套用)
if (hfSelectedId.Value == empId)
{
e.Row.CssClass = "selected";
}
}
}
// 隱藏欄位值改變 → 觸發明細查詢
protected void hfSelectedId_ValueChanged(object sender, EventArgs e)
{
if (int.TryParse(hfSelectedId.Value, out int empId))
{
LoadDetail(empId);
}
}
private void LoadDetail(int empId)
{
DataTable dt = GetEmployeeDetail(empId); // 查詢單筆明細
dvDetail.DataSource = dt;
dvDetail.DataBind();
}
// 分頁
protected void gvEmployees_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
gvEmployees.PageIndex = e.NewPageIndex;
BindGrid();
}
// 排序
protected void gvEmployees_Sorting(object sender, GridViewSortEventArgs e)
{
// 簡易排序實作 (實務建議在資料層排序)
DataTable dt = GetEmployeeData();
dt.DefaultView.Sort = e.SortExpression + (e.SortDirection == SortDirection.Ascending ? " ASC" : " DESC");
gvEmployees.DataSource = dt;
gvEmployees.DataBind();
}
// --- 模擬資料存取 ---
private DataTable GetEmployeeData()
{
var dt = new DataTable();
dt.Columns.Add("EmployeeID", typeof(int));
dt.Columns.Add("LastName", typeof(string));
dt.Columns.Add("FirstName", typeof(string));
dt.Columns.Add("Title", typeof(string));
dt.Columns.Add("HireDate", typeof(DateTime));
dt.Columns.Add("City", typeof(string));
dt.Rows.Add(1, "Davolio", "Nancy", "Sales Representative", new DateTime(1992, 5, 1), "Seattle");
dt.Rows.Add(2, "Fuller", "Andrew", "Vice President, Sales", new DateTime(1993, 8, 14), "Tacoma");
dt.Rows.Add(3, "Leverling", "Janet", "Sales Representative", new DateTime(1993, 4, 1), "Kirkland");
dt.Rows.Add(4, "Peacock", "Margaret", "Sales Representative", new DateTime(1993, 5, 3), "Redmond");
dt.Rows.Add(5, "Buchanan", "Steven", "Sales Manager", new DateTime(1993, 10, 17), "London");
dt.Rows.Add(6, "Suyama", "Michael", "Sales Representative", new DateTime(1993, 10, 17), "London");
dt.Rows.Add(7, "King", "Robert", "Sales Representative", new DateTime(1994, 1, 2), "London");
dt.Rows.Add(8, "Callahan", "Laura", "Inside Sales Coordinator", new DateTime(1994, 3, 5), "Seattle");
dt.Rows.Add(9, "Dodsworth", "Anne", "Sales Representative", new DateTime(1994, 11, 15), "London");
return dt;
}
private DataTable GetEmployeeDetail(int empId)
{
var dt = new DataTable();
dt.Columns.Add("Field");
dt.Columns.Add("Value");
// 實務查詢 DB
var row = GetEmployeeData().Select($"EmployeeID = {empId}")[0];
foreach (DataColumn col in row.Table.Columns)
{
dt.Rows.Add(col.ColumnName, row[col]);
}
return dt;
}
}
}
關鍵技術點解析
| 技術點 | 實作方式 | 優勢 |
| 主鍵傳遞 |
DataKeyNames="EmployeeID" + RowDataBound 寫入 data-key 屬性 |
不暴露於可見欄位、JS 精準讀取、支援複合主鍵 |
| 選中狀態持久化 |
HiddenField + OnValueChanged 觸發後端 |
跨回發保持狀態、UpdatePanel 非同步更新後可透過 JS 還原 |
| 樣式隔離 |
CSS :hover 處理懸停,.selected class 處理選中 |
無需 JS 監聽 mouseover/mouseout、效能最佳、無閃爍 |
| 分頁/排序後還原選中 |
PageRequestManager.add_endRequest 重掃 DOM 套用 class |
UpdatePanel 局部更新不會遺失選中視覺狀態 |
| 伺服器端事件觸發 |
HiddenField.ValueChanged → LoadDetail |
符合 Web Forms 事件模型、可直接存取伺服器控件 |
進階擴充:多選 (Checkbox) + 批次操作
<Columns>
<asp:TemplateField HeaderText="選取" ItemStyle-Width="40px">
<ItemTemplate>
<asp:CheckBox ID="chkSelect" runat="server"
value='<%# Eval("EmployeeID") %>'
onclick="toggleRowSelect(this)" />
</ItemTemplate>
</asp:TemplateField>
...
</Columns>
<script>
function toggleRowSelect(cb) {
var tr = cb.closest('tr');
tr.classList.toggle('selected', cb.checked);
// 收集所有勾選的 ID 送至 HiddenField 或直接 AJAX 批次刪除
}
</script>
常見問題與解決
- 點選列無反應:確認 GridView 已渲染 ``、JS 選取器正確、無其他元素攔截 click (如按鈕、連結需 `e.stopPropagation()`)。
- 選中樣式被 :hover 覆蓋:CSS 優先順序 `.selected` 需高於 `:hover`,或用 `!important` (如範例)。
- UpdatePanel 更新後 JS 失效:必須在 `endRequest` 重新綁定事件或採用事件委派 (範例已用委派)。
- DataKey 取值錯誤:`DataKeyNames` 必須與資料來源欄位名稱完全一致、區分大小寫。
現代化替代:前端表格元件 (DataTables, AG Grid, TanStack Table)
若專案允許引入前端框架,強烈建議棄用 GridView,改用:
- DataTables.net:jQuery 套件,支援分頁、排序、搜尋、選中、匯出、響應式,零後端渲染。
- AG Grid (Community):企業級功能、虛擬捲動、Row Selection、Range Selection、Excel 匯出。
- TanStack Table (React/Vue/Solid):Headless UI、TypeScript 優先、完全可控渲染。
後端只提供 GET /api/employees?page=1&size=10&sort=name 回傳 JSON,前端完全掌控互動與狀態。
總結
GridView 列選中高亮的最佳實踐:**CSS 處理視覺狀態 (hover/selected)** + **data-key 屬性攜帶主鍵** + **HiddenField 傳遞選中值觸發後端事件** + **endRequest 還原視覺狀態**。此方案零 ViewState 擴充、相容分頁排序、程式碼清晰可維護。長期專案請規劃遷移至前端表格元件 + Web API 架構。
- 上一篇文章: ASP.NET 巢狀 Repeater 控件完整實作:主從資料雙層綁定教學
- 下一篇文章: C# 取得電腦與平板裝置 MAC 地址完整方案 (WMI + ARP)
Copyright ©
程式碼之家 All Rights Reserved