ASP.NET Web Forms 預設將 ViewState 序列化為 Base64 字串,置於隱藏欄位 __VIEWSTATE。當頁面包含大量資料繫結控制項(GridView、Repeater)或啟用大量控制項 ViewState 時,隱藏欄位動輒數十 KB 甚至百 KB,導致:
繼承 Page 並覆寫 LoadPageStateFromPersistenceMedium / SavePageStateToPersistenceMedium,配合 PageStatePersister 抽象類,將 ViewState/ControlState 序列化後存為伺服器端實體檔案,頁面僅保留檔案識別碼(GUID/Ticks+Hash)。
public class XVPage : Page {
// === 目錄管理 ===
static private DirectoryInfo _viewStateDir;
private DirectoryInfo ViewStateDir {
get {
if (_viewStateDir == null) {
var baseDir = new DirectoryInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data"));
if (!baseDir.Exists) baseDir.Create();
_viewStateDir = new DirectoryInfo(Path.Combine(baseDir.FullName, "ViewState"));
if (!_viewStateDir.Exists) _viewStateDir.Create();
}
return _viewStateDir;
}
}
// === 讀取 ===
protected override object LoadPageStateFromPersistenceMedium() {
var ps = this.PageStatePersister;
ps.Load();
if (ps.ControlState != null)
ps.ControlState = DeserializeFromStore((string)ps.ControlState);
if (ps.ViewState != null)
ps.ViewState = DeserializeFromStore((string)ps.ViewState);
return new Pair(ps.ControlState, ps.ViewState);
}
// === 寫入 ===
protected override void SavePageStateToPersistenceMedium(object state) {
var ps = this.PageStatePersister;
if (state is Pair pair) {
ps.ControlState = pair.First;
ps.ViewState = pair.Second;
} else {
ps.ViewState = state;
}
if (ps.ControlState != null)
ps.ControlState = SerializeToStore(ps.ControlState);
if (ps.ViewState != null)
ps.ViewState = SerializeToStore(ps.ViewState);
ps.Save();
}
// === 序列化 → 檔案 + Cache 雙層加速 ===
private string SerializeToStore(object obj) {
string serialized = new ObjectStateFormatter().Serialize(obj);
string fileId = (DateTime.Now.Ticks + (long)serialized.GetHashCode()).ToString(); // 離散 ID
string filePath = Path.Combine(ViewStateDir.FullName, fileId);
File.WriteAllText(filePath, serialized);
Cache.Insert(fileId, serialized); // 熱資料進 Cache,下次讀取命中記憶體
return fileId; // 僅回傳 ID 給 __VIEWSTATE 隱藏欄位
}
private object DeserializeFromStore(string fileId) {
// 先嘗試 Cache
string serialized = Cache[fileId] as string;
if (serialized == null) {
string filePath = Path.Combine(ViewStateDir.FullName, fileId);
if (File.Exists(filePath)) serialized = File.ReadAllText(filePath);
} else {
Cache.Remove(fileId); // 讀取後移除 Cache,避免殘留
}
return new ObjectStateFormatter().Deserialize(serialized);
}
// === 定期清理過期檔案(20 分鐘未存取)===
protected override void OnUnload(EventArgs e) {
base.OnUnload(e);
var cutoff = DateTime.Now.AddMinutes(-20);
foreach (var file in ViewStateDir.GetFiles()) {
try {
if (file.LastAccessTime < cutoff) file.Delete();
} catch { /* 忽略佔用/權限錯誤 */ }
}
}
}
// 原本:public partial class MyPage : Page
public partial class MyPage : XVPage { /* ... */ }
| 設計點 | 說明 |
|---|---|
| ObjectStateFormatter | ASP.NET 內建序列化器,支援 ViewState 所需的型別(Pair、Triplet、IndexedString 等),比 BinaryFormatter 安全、輕量。 |
| ID 生成策略 | Ticks + HashCode 極低衝突機率,無需 GUID 產生開銷。 |
| Cache + 檔案雙層 | 熱資料走記憶體(Cache),冷資料落盤;讀取後移除 Cache,配合 OnUnload 定期清理,防止檔案無限堆疊。 |
| ControlState 同步處理 | ControlState(如 GridView 排序/分頁資訊)同樣走此管道,確保控制項核心狀態不丟失。 |
| App_Data/ViewState 目錄 | 受 ASP.NET 保護(預設拒絕直接 HTTP 存取),安全性優於任意目錄。 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 伺服器端檔案 (本文) | 零架構變更、相容既有專案、無需額外基礎設施 | Web Farm 需共享儲存(NFS/DFS)、IO 壓力大時需 SSD | 中小型單機/少量負載平衡節點 |
| SessionPageStatePersister | 內建、程式碼極少 | Session 暴增、InProc 模式回收即丟失、StateServer/SQL Server 序列化開銷大 | 極簡專案、單伺服器、ViewState 极小 |
| 自訂 SQL/Redis PageStatePersister | Web Farm 原生支援、集中管理、可查詢審計 | 需額外基礎設施、延遲略高、實作複雜度高 | 大型分散式系統、高可用性要求 |
| 關閉 ViewState + 控制項層級啟用 | 根治體積、效能最佳 | 開發成本高、易遺漏導致 Bug、需深度理解控制項生命週期 | 新專案、團隊有經驗、極致效能要求 |
ViewStateDir 指向共享網路磁碟(檔案鎖定需測試)或改寫為 Redis/SQL 實作。machineKey 一致(ObjectStateFormatter 依賴 machineKey 簽章/加密)。