典型需求:一個頁面同時編輯 **主表** (公司基本資料) 與 **從表** (多筆部門資料),部門列數動態增減。傳統 PostBack 會導致動態控制項狀態遺失,本文示範使用 **JavaScript 動態建立表列** + **ASP.NET AJAX PageMethods** 將資料打包傳至後端 `WebMethod`,實現無刷新提交與伺服器端集中處理。
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="DynamicTableDemo.aspx.cs" Inherits="Demo.DynamicTableDemo" %>
<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
<head runat="server">
<title>動態主從表單 AJAX 提交示範</title>
<style>
body { font-family: Microsoft JhengHei; margin: 20px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 15px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f2f2f2; }
.btn { padding: 6px 12px; margin-right: 5px; cursor: pointer; }
.message { margin-top: 10px; padding: 10px; border-radius: 4px; }
.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
</style>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true" />
<h2>公司與部門資料維護</h2>
<!-- 主表區塊 -->
<fieldset>
<legend>公司基本資料</legend>
<table>
<tr>
<td>公司名稱:</td>
<td><input type="text" id="txtCompanyName" style="width:300px" /></td>
<td>公司類型:</td>
<td>
<select id="ddlCompanyType">
<option value="1">國營企業</option>
<option value="2">民營企業</option>
<option value="3">外資企業</option>
</select>
</td>
</tr>
<tr>
<td>公司規模:</td>
<td><input type="text" id="txtCompanySize" style="width:300px" /></td>
</tr>
</table>
</fieldset>
<!-- 從表區塊 (動態列) -->
<fieldset>
<legend>部門資料
<button type="button" class="btn" onclick="addRow()">+ 新增部門</button>
</legend>
<table id="tblDepartments">
<thead>
<tr>
<th style="width:30%">部門名稱</th>
<th style="width:30%">經理</th>
<th style="width:30%">電話</th>
<th style="width:10%">操作</th>
</tr>
</thead>
<tbody>
<!-- 預設一列 -->
<tr data-row="1">
<td><input type="text" class="dept-name" placeholder="部門名稱" /></td>
<td><input type="text" class="dept-manager" placeholder="經理" /></td>
<td><input type="text" class="dept-phone" placeholder="電話" /></td>
<td><button type="button" class="btn" onclick="delRow(this)">刪除</button></td>
</tr>
</tbody>
</table>
</fieldset>
<button type="button" class="btn" onclick="submitData()" style="background:#007bff;color:white;border:none;padding:10px 20px;font-size:16px;">提交儲存</button>
<div id="resultMsg" class="message" style="display:none;"></div>
</form>
<script>
let rowIndex = 1; // 全域列計數器
// 新增列
function addRow() {
rowIndex++;
const tbody = document.querySelector('#tblDepartments tbody');
const tr = document.createElement('tr');
tr.dataset.row = rowIndex;
tr.innerHTML = `
<td><input type="text" class="dept-name" placeholder="部門名稱" /></td>
<td><input type="text" class="dept-manager" placeholder="經理" /></td>
<td><input type="text" class="dept-phone" placeholder="電話" /></td>
<td><button type="button" class="btn" onclick="delRow(this)">刪除</button></td>
`;
tbody.appendChild(tr);
}
// 刪除列
function delRow(btn) {
const rows = document.querySelectorAll('#tblDepartments tbody tr');
if (rows.length <= 1) { alert('至少保留一列'); return; }
btn.closest('tr').remove();
// 可選:重新編號 data-row
}
// 收集資料並呼叫 PageMethod
function submitData() {
// 1. 主表資料
const companyName = document.getElementById('txtCompanyName').value.trim();
const companySize = document.getElementById('txtCompanySize').value.trim();
const companyType = document.getElementById('ddlCompanyType').value;
if (!companyName) { alert('請輸入公司名稱'); return; }
// 2. 從表資料
const deptRows = document.querySelectorAll('#tblDepartments tbody tr');
const departments = [];
deptRows.forEach(tr => {
const name = tr.querySelector('.dept-name').value.trim();
const manager = tr.querySelector('.dept-manager').value.trim();
const phone = tr.querySelector('.dept-phone').value.trim();
if (name || manager || phone) { // 至少有一欄有值才送出
departments.push({ Name: name, Manager: manager, Phone: phone });
}
});
if (departments.length === 0) { alert('請至少填寫一筆部門資料'); return; }
// 3. 呼叫 PageMethod (非同步)
showMessage('資料送出中...', 'info');
PageMethods.SaveCompanyData(companyName, companySize, companyType, departments, onSuccess, onFailure);
}
// 成功回呼
function onSuccess(result) {
showMessage(result.Message, result.Success ? 'success' : 'error');
if (result.Success) {
// 可選:清空表單、重導向、更新 UI
console.log('伺服器回傳 ID:', result.Data);
}
}
// 失敗回呼
function onFailure(error) {
showMessage('AJAX 呼叫失敗:' + error.get_message(), 'error');
}
// 顯示訊息
function showMessage(text, type) {
const div = document.getElementById('resultMsg');
div.textContent = text;
div.className = 'message ' + type;
div.style.display = 'block';
}
</script>
</body>
</html>
using System;
using System.Collections.Generic;
using System.Web.Services;
using System.Web.Script.Services;
public partial class DynamicTableDemo : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e) { }
// 必須加上 [WebMethod] 且為 static
[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public static AjaxResult SaveCompanyData(
string companyName,
string companySize,
string companyType,
List departments)
{
try
{
// 1. 驗證 (實務應更完整)
if (string.IsNullOrWhiteSpace(companyName))
return new AjaxResult { Success = false, Message = "公司名稱不可為空" };
// 2. 建立主表實體
var company = new CompanyEntity
{
Name = companyName,
Size = companySize,
Type = companyType,
CreateTime = DateTime.Now
};
// 3. 建立從表實體集合
var deptEntities = new List();
foreach (var dto in departments)
{
deptEntities.Add(new DepartmentEntity
{
Name = dto.Name,
Manager = dto.Manager,
Phone = dto.Phone,
Company = company // 導航屬性
});
}
// 4. 存入資料庫 (此處用假資料模擬,實務用 EF / ADO.NET / Dapper)
int newCompanyId = MockDatabase.Save(company, deptEntities);
return new AjaxResult
{
Success = true,
Message = $"儲存成功!公司 ID: {newCompanyId},部門筆數: {deptEntities.Count}",
Data = newCompanyId
};
}
catch (Exception ex)
{
// 記錄日誌
return new AjaxResult { Success = false, Message = "伺服器錯誤:" + ex.Message };
}
}
}
// --- DTO / 實體類別 ---
public class DepartmentDto
{
public string Name { get; set; }
public string Manager { get; set; }
public string Phone { get; set; }
}
public class CompanyEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Size { get; set; }
public string Type { get; set; }
public DateTime CreateTime { get; set; }
}
public class DepartmentEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Manager { get; set; }
public string Phone { get; set; }
public CompanyEntity Company { get; set; }
}
public class AjaxResult
{
public bool Success { get; set; }
public string Message { get; set; }
public object Data { get; set; }
}
// --- 模擬資料庫 ---
public static class MockDatabase
{
private static int _seq = 1000;
public static int Save(CompanyEntity company, List depts)
{
company.Id = ++_seq;
foreach (var d in depts) d.Id = ++_seq;
// 實際寫入 DB 邏輯...
System.Diagnostics.Debug.WriteLine($"Saved Company: {company.Name}, Depts: {depts.Count}");
return company.Id;
}
}
| 技術點 | 說明 |
|---|---|
| ScriptManager.EnablePageMethods="true" | 啟用 PageMethods 功能,自動產生 JS Proxy 類別 (`PageMethods.MethodName`)。 |
| [WebMethod] + static | 只有 `static` 方法且標記 `[WebMethod]` 才能被 PageMethods 呼叫。 |
| [ScriptMethod(ResponseFormat.Json)] | 明確指定回傳 JSON 格式 (預設即為 JSON)。 |
| List<T> 參數自動反序列化 | 前端傳送 JSON 陣列,ASP.NET AJAX 自動反序列化為 `List |
| 動態列 ID 不衝突 | 使用 `class` 選取器 (`.dept-name`) 而非 `id`,避免動態新增時 ID 重複問題。 |
| 資料收集邏輯 | `querySelectorAll` 遍歷 `tbody tr`,提取各 `input.value` 組成 DTO 陣列。 |
| 錯誤處理 | `onFailure` 捕捉網路/伺服器例外,`onSuccess` 處理業務邏輯失敗 (Success=false)。 |
| 現象 | 原因 | 解決 |
|---|---|---|
| PageMethods 未定義 | ScriptManager 缺少 `EnablePageMethods="true"` 或未引用 `MicrosoftAjax.js` | 檢查 ScriptManager 設定,確保頁面有 ` |
| WebMethod 404 / 500 | 方法非 static、缺少 [WebMethod]、路徑錯誤 (需在同一 .aspx) | 確認方法簽章、類別名稱、命名空間 |
| List 參數為 null / Count=0 | 前端 JSON 屬性名稱與 DTO 不符 (如 `name` vs `Name`) | 確保前端物件屬性名稱與 DTO 完全一致 (建議 PascalCase) |
| 動態列事件失效 | UpdatePanel 部分更新後事件未重新綁定 | 本範例未用 UpdatePanel;若用需用 `PageRequestManager` 重新綁定 |
| 中文亂碼 | 頁面編碼非 UTF-8 或 Web.config globalization 設定衝突 | 頁面儲存為 UTF-8,web.config ` |
結合 **原生 JS 動態 DOM 操作** + **ASP.NET AJAX PageMethods**,即可輕鬆實現「主從表單、動態列、無刷新提交」的經典需求。核心在於:前端負責 UI 互動與資料收集 (DTO 序列化),後端 `static WebMethod` 負責業務邏輯與持久化。此模式在 Web Forms 專案中極具實用價值,也是邁向前後端分離架構的良好過渡練習。