ASP.NET WebForms 預設啟用 `MaintainScrollPositionOnPostBack` 可維持頁面整體捲軸位置,但對於局部 `Panel` 控制項內的捲軸位置則無效。本文提供純 JavaScript 方案,在 PostBack 前記住 Panel 捲軸位置,PostBack 後自動還原。
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="PanelScrollDemo.aspx.cs" Inherits="Demo.PanelScrollDemo" %>
<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
<head runat="server">
<title>Panel 捲軸位置保持示範</title>
<script type="text/javascript">
// 儲存 Panel 捲軸位置
function savePanelScroll() {
var panel = document.getElementById('<%= Panel1.ClientID %>');
var hidden = document.getElementById('<%= hdnScrollPos.ClientID %>');
if (panel && hidden) {
hidden.value = panel.scrollTop;
}
}
// 還原 Panel 捲軸位置
function restorePanelScroll() {
var panel = document.getElementById('<%= Panel1.ClientID %>');
var hidden = document.getElementById('<%= hdnScrollPos.ClientID %>');
if (panel && hidden && hidden.value !== '') {
panel.scrollTop = parseInt(hidden.value, 10);
}
}
// 頁面載入完成自動還原
window.onload = restorePanelScroll;
</script>
</head>
<body onclick="savePanelScroll()">
<form id="form1" runat="server">
<asp:HiddenField ID="hdnScrollPos" runat="server" />
<div style="padding: 20px;">
<h3>Panel 內容區域 (固定高度 300px, 內容超過會出現捲軸)</h3>
<asp:Panel ID="Panel1" runat="server" Height="300px" Width="500px"
ScrollBars="Vertical" BorderWidth="1" BorderColor="Gray" BorderStyle="Solid">
<asp:TreeView ID="TreeView1" runat="server"
OnTreeNodeExpanded="TreeView1_TreeNodeExpanded"
OnTreeNodeCollapsed="TreeView1_TreeNodeCollapsed">
<Nodes>
<asp:TreeNode Text="根節點 1" Value="1" Expanded="True">
<asp:TreeNode Text="子節點 1-1" Value="11" />
<asp:TreeNode Text="子節點 1-2" Value="12" />
<asp:TreeNode Text="子節點 1-3" Value="13" />
</asp:TreeNode>
<asp:TreeNode Text="根節點 2" Value="2">
<asp:TreeNode Text="子節點 2-1" Value="21" />
<asp:TreeNode Text="子節點 2-2" Value="22" />
</asp:TreeNode>
<asp:TreeNode Text="根節點 3" Value="3">
<asp:TreeNode Text="子節點 3-1" Value="31" />
</asp:TreeNode>
</Nodes>
</asp:TreeView>
<br /><br />
<asp:Button ID="btnPostback" runat="server" Text="觸發 PostBack" OnClick="btnPostback_Click" />
<asp:Label ID="lblTime" runat="server" ForeColor="Blue" />
</asp:Panel>
</div>
</form>
</body>
</html>
public partial class PanelScrollDemo : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
lblTime.Text = "首次載入:" + DateTime.Now.ToLongTimeString();
}
}
protected void btnPostback_Click(object sender, EventArgs e)
{
lblTime.Text = "PostBack 時間:" + DateTime.Now.ToLongTimeString();
// 模擬一些處理
System.Threading.Thread.Sleep(500);
}
protected void TreeView1_TreeNodeExpanded(object sender, TreeNodeEventArgs e)
{
lblTime.Text = "展開節點:" + e.Node.Text + " - " + DateTime.Now.ToLongTimeString();
}
protected void TreeView1_TreeNodeCollapsed(object sender, TreeNodeEventArgs e)
{
lblTime.Text = "收合節點:" + e.Node.Text + " - " + DateTime.Now.ToLongTimeString();
}
}
<script type="text/javascript">
var PanelScrollManager = (function() {
var panels = {}; // panelClientID -> hiddenFieldClientID
function register(panelClientId, hiddenClientId) {
panels[panelClientId] = hiddenClientId;
}
function saveAll() {
for (var panelId in panels) {
var panel = document.getElementById(panelId);
var hidden = document.getElementById(panels[panelId]);
if (panel && hidden) hidden.value = panel.scrollTop;
}
}
function restoreAll() {
for (var panelId in panels) {
var panel = document.getElementById(panelId);
var hidden = document.getElementById(panels[panelId]);
if (panel && hidden && hidden.value !== '') {
panel.scrollTop = parseInt(hidden.value, 10);
}
}
}
// 自動綁定 body click
document.body.onclick = saveAll;
window.onload = restoreAll;
return { register: register };
})();
// 頁面註冊 (可在各 UserControl 中呼叫)
PanelScrollManager.register('<%= Panel1.ClientID %>', '<%= hdnScrollPos1.ClientID %>');
PanelScrollManager.register('<%= Panel2.ClientID %>', '<%= hdnScrollPos2.ClientID %>');
</script>
public static class PanelScrollHelper
{
public static void RegisterScrollPersistence(this Page page, Panel panel, HiddenField hidden)
{
string script = $@"
PanelScrollManager.register('{panel.ClientID}', '{hidden.ClientID}');
";
ScriptManager.RegisterStartupScript(page, page.GetType(),
"PanelScroll_" + panel.ClientID, script, true);
}
}
// 使用
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
this.RegisterScrollPersistence(Panel1, hdnScrollPos1);
this.RegisterScrollPersistence(Panel2, hdnScrollPos2);
}
}
若 Panel 在 `UpdatePanel` 內進行非同步回傳:
function restorePanelScroll() {
var panel = $get('<%= Panel1.ClientID %>');
var hidden = $get('<%= hdnScrollPos.ClientID %>');
if (panel && hidden && hidden.value !== '') {
// UpdatePanel 更新後需延遲執行,確保 DOM 已重建
setTimeout(function() { panel.scrollTop = parseInt(hidden.value, 10); }, 0);
}
}
// 針對 UpdatePanel 使用 pageLoad (ASP.NET AJAX 生命週期)
function pageLoad(sender, args) {
if (!args.get_isPartialLoad()) return; // 僅部分更新時
restorePanelScroll();
}
| 方案 | 優點 | 缺點 | 適用情境 |
|---|---|---|---|
| body onclick + onload (本文) | 簡單、無相依、涵蓋所有 PostBack | 每次點擊都儲存、頁面級全域 | 傳統 WebForms、單一/少數 Panel |
| MaintainScrollPositionOnPostBack | 內建、零程式碼 | 僅支援頁面整體捲軸 | 頁面主要捲軸需求 |
| UpdatePanel + pageLoad | 非同步回傳也支援 | 需 ScriptManager、較複雜 | 大量 AJAX 互動頁面 |
| jQuery 捲軸事件 + 本地儲存 | 跨頁面記住位置 | 較重、需序列化 | 複雜單頁應用 |
透過 `body onclick` 攔截所有點擊儲存 `scrollTop`,配合 `window.onload` 還原,是解決 Panel 捲軸位置重置最輕量、最相容的方案。對於現代專案,建議逐步遷移至前端框架 (Vue/React) 管理狀態,徹底避開 PostBack 機制帶來的此類問題。