Trong chủ đề mình muốn đề cập cho các bạn về việc ViewState sẽ không còn là cơn ác mộng đối với chúng ta và các lập trình viên asp.net web forms. Như chúng ta đã biết, ViewState là một tính năng cực kỳ quan trọng trong nền tảng phát triển web asp.net web forms. Vậy tại sao nó lại quan trọng như vậy? Để đi sâu về phần định nghĩa ViewState chi tiết, mình muốn nói lại khái niệm asp.net web forms một chút. Asp.Net web forms được ra đời vào tháng một năm 2002 cùng với việc phiên bản đầu tiên của .NET Framwork ra đời. Nó đánh dấu việc phát triển web theo một hướng mới và theo một framework chung .NET Framework. Hướng mới ở đây là phát triển web theo hướng sự kiện giống như lập trình theo kiểu Visual Basic 6.0 và Windows Forms. Điều này có thể giúp lập trình viên phát triển web rất nhanh và mang tính mở rộng cao ví dụ là nếu bạn muốn lập trình trên Button bạn chỉ cần tạo một trình xử lý sự kiện sau đó viết code trong đó để xử lý gì đó.
Tại sao Microsoft lại ra mô hình phát triển web theo hướng sự kiện vào lúc đó? Theo mình nghĩ Microsoft đã đi nước cờ thông minh là trước đây người dùng đã từng quen với việc phát triển Windows Forms trên Visual Basic 6.0 và Windows Forms trên nền tảng .NET. Điều này giúp cho các lập trình viên Windows tiếp cận nhanh đối với việc phát triển web. Chính vì ra đời mô hình phát triển theo hướng sự kiện này vì thế mô hình các asp.net server control mới ra đời. Asp.net server controls tập hợp các controls được chạy trên server đã được đóng gói một khối lượng chức năng phong phú trong đó. Thực chất các asp.net server controls chính là các đoạn mã html được đóng gói và lập trình sẵn trong đó và sẽ được chuyển đổi sang mã html sau khi trở về phía người dùng(client-side) sau một vòng đời xử lý trang của asp.net web forms. Mình sẽ không nói rõ về vòng đời xử lý trang ở đây bạn cứ hiểu là tập hợp các dòng sự kiện và quy trình để xử lý một trang trong asp.net web forms. Chính vì asp.net server controls ra đời cũng kéo theo ViewState ra đời. Mặc định khi trả về đoạn mã html ở phía client, khi chúng ta thay đổi trạng thái của các html phía client(ví dụ như chúng ta muốn bắt sự kiện change của select tag), phía server không biết trạng thái của các server controls đã thay đổi hay chưa nếu không có tính năng ViewState. ViewState là tính năng để lưu trữ và giữ trạng thái của các server controls để asp.net web forms biết để xử lý trên server. Nếu không có tính năng này thì khi người dùng postback dữ liệu đến máy chủ web, thì asp.net web forms không biết trạng thái của các server controls đã thay đổi hay chưa. Tại sao asp.net có thể giữ được trạng thái của các server controls? Mặc định tất cả các dữ liệu ViewState của các server controls sẽ được lưu trữ ở phía client trong một hidden field với đoạn mã dữ liệu đã được mã hóa theo Base64. Và sẽ được asp.net web forms giải mã lại và cập nhật lại trạng thái đã được thay đổi bởi người dùng khi họ postback dữ liệu về phía máy chủ web.
Mặc dù ViewState là đặc tính tuyệt vời của asp.net web forms nhưng song song với đó nó tồn tại một số vấn đề performance của asp.net web forms. Việc lạm dụng quá nhiều asp.net server controls có thể làm tăng kích cỡ của ViewState nhất là các server controls nặng ViewState như GridView, DataList, v.v... Việc kích cỡ ViewState càng ngày càng tăng sẽ làm ảnh hưởng nhiều đến performance của asp.net web forms và mang một số điểm bất lợi sau đây:
- Tăng chi phí băng thông.
=> Tốn nhiều chi phí cho băng thông.
- Rủi ro về bảo mật dữ liệu ViewState.
=> Bởi vì mặc định dữ liệu ViewState mã hóa theo chuẩn Base64. Theo chuẩn này có thể giải mã một cách dễ dàng.
- Trang web chậm hơn.
=> Bởi vì dữ liệu ViewState nặng có thể làm trình duyệt tải trang web chậm hơn.
Ở đây chúng ta chỉ có thể khắc phục điểm bất lợi về bảo mật dữ liệu ViewState, chúng ta có thể mã hóa ViewState.bằng directive EnableViewStateMac của asp.net web forms để dữ liệu ViewState khó được giải mã. Mặc dù vậy chúng ta cũng không chắc về đoạn dữ liệu ViewState được lưu ở phía client có được bảo mật hoàn toàn hay không.
Để minh chứng cho điều này mình lấy một ví dụ nho nhỏ. Giả sử mình có một trang Default.aspx. Trong trang này một server control GridView. Control này sẽ binding đến danh sách dữ liệu Persons.
Default.aspx:
<form id="frm" runat="server"> <div> <asp:GridView ID="gv1" runat="server" AutoGenerateColumns="true"></asp:GridView> </div> </form>
|
Person.cs:
using System; public class Person { public string FirstName { get; set; } public string LastName { get; set; } }
|
Default.aspx.cs:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; public partial class Default : System.Web.UI.Page { private List<Person> GetPersons() { var personList = new List<Person>(); personList.Add(new Person { FirstName = "Phat", LastName = "Ly" }); personList.Add(new Person { FirstName = "Lam", LastName = "Ly" }); personList.Add(new Person { FirstName = "Tam", LastName = "Chieu" }); personList.Add(new Person { FirstName = "Phuc", LastName = "To" }); personList.Add(new Person { FirstName = "Phuong", LastName = "Tran" }); personList.Add(new Person { FirstName = "Phuong", LastName = "Pham" }); personList.Add(new Person { FirstName = "Phuong", LastName = "Nguyen" }); personList.Add(new Person { FirstName = "Khanh", LastName = "Pham" }); personList.Add(new Person { FirstName = "Viet", LastName = "Pham" }); personList.Add(new Person { FirstName = "Minh", LastName = "Ly" }); return personList; } protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { gv1.DataSource = GetPersons(); gv1.DataBind(); } } }
|
Bây giờ bạn thử chạy trên trình duyệt bạn sẽ thấy đoạn mã ViewState ở phía client như hình dưới đây:

Với đoạn mã này bạn có thể giải mã chúng bằng việc viết code hay sử dụng công dụ ViewState decoder online. Ở đây mình sử dụng công cụ ViewState decoder online ở trang http://ignatu.co.uk/ViewStateDecoder.aspx để giải mã lại đoạn ViewState của chúng ta:

Như chúng ta thấy trong hình trên đoạn dữ liệu ViewState của chúng ta đã được giải mã điều này sẽ tiềm ẩn rủi ro về bảo mật nếu dữ liệu nhạy cảm hay quan trọng nằm trong ViewState chưa được mã hóa hoàn toàn. Để đoạn dữ liệu ViewState của chúng ta bảo mật hơn chúng ta có thể sử dụng directive EnableViewStateMac của asp.net web forms và thiết lập trong web.config.
Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="ViewStateWebApplication.Default" EnableViewStateMac="true" %>
|
Web.config:
<configuration> <system.web> <pages viewStateEncryptionMode="Always"/> </system.web> </configuration>
|
Bạn thử chạy trên trình duyệt lần nữa bạn thấy dữ liệu ViewState đã được mã hóa và bạn không có thể giải mã theo Base64 như trườc

Như hình phía trên chúng ta đã bảo mật được dữ liệu ViewState. Tuy nhiên điều này cũng không chắc dữ liệu của chúng ta bảo mật một cách hoàn toàn. Theo kinh nghiệm của mình, chúng ta nên tránh lưu bất cứ dữ liệu nhạy cảm trong ViewState. Điều này giúp chúng ta hạn chế rủi ro khá nhiều trong ứng dụng web.
Mặc dù dữ liệu ViewState được bảo mật chúng ta vẫn còn gặp 2 điểm bất lợi khác như đã đề cập ở phía trên. Để khắc phục hoàn toàn, chúng ta có thể lưu trữ và tải dữ liệu ViewState trực tiếp trên server thay vì phía client. Hiện tại có vài giải pháp thực tế sau đây để thực hiện điều này:
- Lưu trữ và tải dữ liệu ViewState trong Session trên server. Giải pháp này chỉ dành cho dữ liệu ViewState vừa và nhỏ.
- Lưu trữ và tải dữ liệu ViewState trong tập tin trên server.
- Lưu trữ và tải dữ liệu ViewState trong asp.net cache trên server.
- Lưu trữ và tải dữ liệu ViewState trong distributed caching trên server như memcached, redis...
Do mình không có nhiều thời gian, vì thế tạm thời mình thiết kế một thư viện chỉ giúp giải quyết việc lưu trữ và tải dữ liệu ViewState trong tập tin trên server cho các bạn. Mình sẽ nâng cấp dần dần và hỗ trợ tất cả các giải pháp tốt nhất hiện có. Sau đây là sơ đồ thiết kế của mình:

- Inteface IViewStateOptimizer: interface này định nghĩa 2 phương thức Load và Save dữ liệu ViewState.
- Abstract class BaseViewStateOptimizer: lớp abstract này thực hiện interface IViewStateOptimizer để thực hiện tính trừu tượng chung cho việc Load và Save dữ liệu ViewState. Trong lớp này có 2 phương thức abstract phải thực hiện khi thừa kế đó là LoadViewStateContents và SaveViewStateContents. Đối với LoadViewStateContents, hàm này sẽ tải nội dụng ViewState vào trang và các server controls. Còn đối với SaveViewStateContents, hàm này sẽ lưu dữ liệu ViewState đến chỗ nào đó. Bạn có thể kế thừa từ lớp này và thực hiện 2 phương thức này để mở rộng thư viện này.
- Class FileViewStateOptimizer: lớp này kế thừa từ lớp trừu tượng BaseViewStateOptimizer để thực hiện việc lưu trữ và tải dữ liệu ViewState trong tập tin trên server.
- Abstract Class BaseFileViewStateOptimizerHttpApplication: lớp trừu tượng này thực hiện việc xóa các tập tin đã lưu khi session đã hết hạn và kết thúc. Bạn phải kế thừa lớp này trong tập tin Global.
- Class ViewStateOptimizerSecurity: lớp static này chứa đựng các phương thức bảo mật cho thư viện ViewStateOptimizer.
- Class FileViewStateOptimizerOptions: lớp này chứa đựng phần cấu hình cho việc lưu trữ và tải dữ liệu ViewState trên server.
- Class FileViewStateOptimizerPageAdapter: lớp này cấu hình toàn cục cho việc thay đổi phương thức lưu trữ và tải dữ liệu ViewState đến tập tin trên server.
- Class SessionViewStateOptimizerPageAdapter: lớp này cấu hình toàn cục cho việc thay đổi phương thức lưu trữ và tải dữ liệu ViewState đến Session trên server.
Mã nguồn:
IViewStateOptimizer.cs:
namespace ViewStateOptimizer { public interface IViewStateOptimizer { void Load(); void Save(); } }
|
BaseViewStateOptimizer.cs:
namespace ViewStateOptimizer { using System; using System.Web.UI; public abstract class BaseViewStateOptimizer : PageStatePersister, IViewStateOptimizer { protected BaseViewStateOptimizer(Page page) : base(page) { } public abstract string LoadViewStateContents(); public abstract bool SaveViewStateContents(string viewStateContents); public override void Load() { IStateFormatter formatter = this.StateFormatter; var viewStateContents = LoadViewStateContents(); var statePair = (Pair)formatter.Deserialize(viewStateContents); if (Page.Session != null) { if (statePair != null) { this.ViewState = statePair.First; this.ControlState = statePair.Second; } } else { throw new InvalidOperationException("The Session is required for ViewStateOptimizer."); } } public override void Save() { if (ViewState != null || ControlState != null) { if (Page.Session != null) { var formatter = this.StateFormatter; var statePair = new Pair(ViewState, ControlState); var serializedState = formatter.Serialize(statePair); if (!SaveViewStateContents(serializedState)) { throw new InvalidOperationException("Can't save the ViewState contents to the specified location. Please ensure all configurations are correct."); } } else { throw new InvalidOperationException("The Session is required for ViewStateOptimizer."); } } } } }
|
FileViewStateOptimizer.cs:
namespace ViewStateOptimizer.FileStorage { using System; using System.IO; using System.Security.Cryptography; using System.Text; using System.Web.UI; public class FileViewStateOptimizer : BaseViewStateOptimizer { public FileViewStateOptimizer(Page page) : base(page) { } public override string LoadViewStateContents() { if (!Page.IsPostBack) return null; string vsHashedKey = Page.Request.Form[FileViewStateOptimizerOptions.ViewStateKey]; if (String.IsNullOrEmpty(vsHashedKey) || !vsHashedKey.StartsWith(FileViewStateOptimizerOptions.ViewStatePrefixValue)) { throw new ViewStateException(); } IStateFormatter frmt = StateFormatter; var salt = Page.Session[vsHashedKey].ToString(); var vsFile = Page.Session[ViewStateOptimizerSecurity.GenerateHashStringBySalt(vsHashedKey, salt)].ToString(); if (!String.IsNullOrEmpty(vsFile)) { if (File.Exists(vsFile)) { return File.ReadAllText(vsFile); } } return null; } public override bool SaveViewStateContents(string viewStateContents) { try { if (ViewState != null || ControlState != null) { if (Page.Session == null) { throw new InvalidOperationException("Session is required for FilePageStatePersister."); } string vsFile, vsHashedKey, salt; if (!Page.IsPostBack) { var sessionId = Page.Session.SessionID; var pageUrl = Page.Request.Path; var vsKey = string.Format("{0}_{1}_{2}_{3}", Guid.NewGuid(), pageUrl, sessionId, DateTime.Now.Ticks); vsKey = vsKey.Replace("/", String.Empty); var vsFileName = vsKey ".vso"; var vsPath = Page.MapPath(FileViewStateOptimizerOptions.ViewStateStorageRelativeFolder); if (!Directory.Exists(vsPath)) { Directory.CreateDirectory(vsPath); } vsFile = Path.Combine(vsPath, vsFileName); vsHashedKey = FileViewStateOptimizerOptions.ViewStatePrefixValue "_" Convert.ToBase64String(ViewStateOptimizerSecurity.GenerateHash(vsKey)); salt = ViewStateOptimizerSecurity.GenerateSaltString(50); Page.Session[vsHashedKey] = salt; Page.Session[ViewStateOptimizerSecurity.GenerateHashStringBySalt(vsHashedKey, salt)] = vsFile; } else { vsHashedKey = Page.Request.Form[FileViewStateOptimizerOptions.ViewStateKey]; if (String.IsNullOrEmpty(vsHashedKey)) { throw new ViewStateException(); } salt = Page.Session[vsHashedKey].ToString(); vsFile = Page.Session[ViewStateOptimizerSecurity.GenerateHashStringBySalt(vsHashedKey, salt)].ToString(); if (string.IsNullOrEmpty(vsFile)) { throw new ViewStateException(); } } File.WriteAllText(vsFile, viewStateContents); Page.ClientScript.RegisterHiddenField(FileViewStateOptimizerOptions.ViewStateKey, vsHashedKey); return true; } return false; } catch { return false; } } } }
|
BaseFileViewStateOptimizerHttpApplication.cs:
namespace ViewStateOptimizer.FileStorage { using System; using System.IO; using System.Web; public abstract class BaseFileViewStateOptimizerHttpApplication : HttpApplication { protected virtual void Session_End(object sender, EventArgs e) { foreach (string key in Session.Keys) { if(key.StartsWith(FileViewStateOptimizerOptions.ViewStatePrefixValue)) { var salt = Session[key].ToString(); var vsFile = Session[ViewStateOptimizerSecurity.GenerateHashStringBySalt(key, salt)].ToString(); if(File.Exists(vsFile)) { File.Delete(vsFile); } } } } } }
|
ViewStateOptimizerSecurity.cs:
namespace ViewStateOptimizer { using System; using System.Security.Cryptography; using System.Text; public static class ViewStateOptimizerSecurity { public static string StandardizeToUtf8String(byte[] bytes) { return Encoding.UTF8.GetString(bytes); } public static byte[] StandardizeToUtf8Bytes(string value) { return Encoding.UTF8.GetBytes(value); } public static byte[] GenerateSalt(int saltSize) { HashAlgorithm algorithm = new SHA512Managed(); byte[] random = new Byte[saltSize]; RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider(); rng.GetBytes(random); return algorithm.ComputeHash(random); } public static string GenerateSaltString(int saltSize) { return StandardizeToUtf8String(GenerateSalt(saltSize)); } public static byte[] GenerateHash(string value) { HashAlgorithm algorithm = new SHA512Managed(); return algorithm.ComputeHash(Encoding.Default.GetBytes(value)); } public static byte[] GenerateHashBySalt(string value, string salt) { var hmacSHA512 = new HMACSHA512(StandardizeToUtf8Bytes(salt)); return hmacSHA512.ComputeHash(StandardizeToUtf8Bytes(value)); } public static string GenerateHashStringBySalt(string value, string salt) { return StandardizeToUtf8String(GenerateHashBySalt(value, salt)); } } }
|
FileViewStateOptimizerPageAdapter.cs:
namespace ViewStateOptimizer.FileStorage { using System.Web.UI; using System.Web.UI.Adapters; public class FileViewStateOptimizerPageAdapter : PageAdapter { public override PageStatePersister GetStatePersister() { return (new FileViewStateOptimizer(this.Page)); } } }
|
SessionViewStateOptimizerPageAdapter.cs:
namespace ViewStateOptimizer { using System.Web.UI; using System.Web.UI.Adapters; public class SessionViewStateOptimizerPageAdapter : PageAdapter { public override PageStatePersister GetStatePersister() { return (new SessionPageStatePersister(this.Page)); } } }
|
Thư viện này mình viết trên nền .NET Framework 2.0 và có thể giải quyết các bất lợi trên. Vì thế ứng dụng asp.net web forms của bạn phải >= .Net Framework 2.0.
Kết luận
Mình hy vọng qua chủ đề này ViewState sẽ không còn là cơn ác mộng đối với nhiều lập trình viên asp.net web forms. Chúng ta có thể lưu trữ và tải dữ liệu ViewState trên server với nhiều giải pháp khác nhau. Các bạn có thể tải source code thư viện ở dưới đây
Download source code thư viện ViewStateOptimizer trên Github
Download source code thư viện ViewStateOptimizer trên Codeplex
Nếu có gì sai sót, mong các bạn góp ý để hoàn thiện hơn.
|
(Trích http://congdongdotnet.com)