Tác giả: Bertrand Le Roy
Link: http://msdn.microsoft.com/en-us/magazine/cc972638.aspx
Đây là một bài dịch kỹ thuật do trung tâm Microsoft Innovation Center thực hiện. Rất mong nhận được phản hồi từ các bạn độc giả để ban dịch thuật hiệu đính cho tốt hơn
Mã nguồn có thể download tại: MSDN Code Gallery (188 KB)
Tài liệu này thảo luận những vấn đề sau:
- Thao tác với dữ liệu phía máy chủ.
- UpdatePanel và máy khách.
- Giảm thiểu postbacks và payloads (giảm thiểu việc chờ đáp ứng từ phía máy chủ và giảm thời gian đợi để tải trang).
- Rendering template phía máy khách.
Nội dung
- A Postback-Based Master-Details Page
- Phiên bản UpdatePanel
- Phiên bản thuần AJAX
- Các mẫu phiên bản AJAX
- Bong bóng sự kiện
- Quản lý nút back
- Sản phẩm hoàn thiện
AJAX là một nền tảng thực sự ấn tượng. Sử dụng AJAX, nhiều công việc thông thường được thực thi trên máy chủ sẽ được chuyển về xử lý trong trình duyệt, điều này sẽ giúp tiết kiệm được băng thông, nhanh hơn và đáp ứng được nhiều hơn trong việc thao tác với giao diện Web. Trong khi những kết quả trả về được giảm tải là cách thức làm việc tốt cho máy khách thì trình duyệt vẫn không phải là môi trường lựa chọn cho nhiều nhà phát triển, những người luôn muốn các ứng dụng máy chủ có đầy đủ sức mạnh và độ mềm dẻo theo cách mà họ bố trí.
Các giải pháp làm việc cho đến nay đã có liên quan đến việc kiểm soát các điều khiển UpdatePanel, chúng cho phép nhà phát triển xây dựng các ứng dụng AJAX trong khi vẫn giữ lại toàn bộ các công cụ của máy chủ. Nhưng UpdatePanel vẫn mang tính nặng nề từ mô hình postback truyền thống - một yêu cầu UpdatePanel vẫn là một qui trình postback toàn diện. Trong thực tế, sử dụng UpdatePanel, toàn bộ form (bao gồm cả trạng thái ViewState) được gửi tới server, hầu hết vòng đời của toàn bộ trang được thực thi ở đó, và thời gian đáp ứng vẫn xử lý trên máy chủ. Rõ ràng, cách thức này đã loại bỏ một trong những lý do chính để chuyển sang AJAX. Thứ thực sự được tiết kiệm ở đây là đối tượng XMLHttpRequest được sử dụng thường xuyên thay vì một HTTP POST yêu cầu và chỉ cập nhật các phần của trang cùng các ViewState được gửi lại cho máy khách. Vì vậy, đáp ứng (response) từ phía máy chủ là nhỏ hơn nhiều, nhưng yêu cầu (require) từ phía máy khách thì không.
Cách tiếp cận AJAX thuần hầu như luôn thực thi tốt hơn cách tiếp cận UpdatePanel. Trong một giải pháp hoàn toàn thuần AJAX, việc tạo lại các thành phần sẽ thực hiện trên máy khách, máy chủ chỉ gửi lại dữ liệu, mà thường sẽ nhỏ hơn nhiều so với mã HTML. Cách tiếp cận này cũng có thể làm giảm đáng kể số lượng các mạng lưới yêu cầu: có dữ liệu trên máy khách cho phép nhiều ứng dụng giao diện người dùng có thể chạy trong trình duyệt.
Tuy nhiên, vấn đề chính với cách tiếp cận AJAX thuần là trình duyệt thiếu những công cụ để chuyển dữ liệu vào các mã HTML. Chỉ có hai cách khá thô sơ để thực hiện việc này: dùng innerHTML, thuộc tính này thay thế toàn bộ nội dung của một phần tử HTML với chuỗi bạn cung cấp, và ít dùng hơn là một số hàm API của đối tượng Document Object Model (DOM) hoạt động trên các thẻ và các thuộc tính (tương tự trong thuật ngữ về mức độ trừu tượng với HtmlTextWriter).
Trong bài viết này, tôi sẽ trình bày ba iterations (iterations là một sự thực thi đơn của một tập hợp các lệnh được lặp lại) của trang được viết với quy trình postback cổ điển, sau đó với UpdatePanel, và sau cùng là sử dụng AJAX thuần để minh họa kỹ thuật làm thế nào để công việc thực hiện trên server có thể giúp cho việc thực thi tốt hơn trên máy khách. Hai ví dụ đầu tiên có thể được xây dựng ngay với ASP.NET 3.5 SP1 đã được triển khai, trong khi phiên bản thứ ba sẽ sử dụng một vài đặc điểm mới phía máy khách dùng công nghệ ASP.NET 4.0.
A Postback-Based Master-Details Page
Trang tôi xây dựng sẽ hiển thị một danh sách các sản phẩm, khi lựa chọn, sẽ hiển thị chi tiết mô tả của sản phẩm trong một bảng điều khiển ở bên phải của danh sách. Tôi sẽ dùng cơ sở dữ liệu mẫu AdventureWorks mà bạn có thể tải về từ liên kết sau: go.microsoft.com/fwlink/?LinkId=124953. Tôi sẽ tạo ra chỉ một lớp dữ liệu thô sơ sử dụng LINQ bởi vì các lớp dữ liệu không phải là trọng tâm của bài viết này.
Tôi bắt đầu bằng cách thêm tập tin AdventureWorks .mdf vào thư mục App_Data của ứng dụng. Sau đó, tôi chỉ cần thêm mới tập tin “Các lớp LINQ tới SQL” .dbml và thả các bảng Product, ProductPhoto, ProductProductPhoto cùng với khung nhìn (view) vProductModelCatalogDescription từ server explorer lên trên bề mặt thiết kế. Các lớp dữ liệu kết quả có thể được nhìn thấy trong hình 1.
.gif)
Hình 1: Bảng chân quạ
Trang web sẽ bao gồm hai khung nhìn, một cho danh sách các sản phẩm và khung còn lại cho chi tiết sản phẩm. Hình 2 cho thấy trang đã được hiển thị xong. Ngay yêu cầu đầu tiên cho trang web, danh sách sản phẩm được liên kết với dữ liệu sử dụng mã sau:
private void BindProductList() {
ProductList.DataSource = from p in AdventureWorksContext.Products
where p.ProductSubcategoryID == 1
//Mountain bikes
orderby p.Name
select p;
ProductList.DataBind();
}
.gif)
Hình 2: Danh sách xe đạp và chi tiết
Danh sách này và mẫu của nó có thể thấy trong hình 3. Mã lệnh truy vấn dữ liệu có thể được tìm thấy trong các dự án và tải xuống khá đơn giản; nó chỉ cần truy vấn dữ liệu cho các sản phẩm trong danh mục Mountain Bike và liên kết điều khiển ListView tới nó. Tất nhiên, tôi có thể đã sử dụng một điều khiển nguồn dữ liệu (data source control) để làm cùng một công việc nhưng tôi tìm thấy mã lệnh tiếp cận với nhiều linh hoạt và chính xác hơn. Các kết quả của bạn có thể thay đổi nếu bạn có nhiều hơn một người thiết kế.
Công việc thực tế của việc tạo ra HTML từ dataset được kiểm soát bởi điều khiển ListView, rất thuận tiện và hiệu quả. Tất cả tôi phải làm là cung cấp các mẫu cho mã HTML trong các thuộc tính LayoutTemplate và ItemTemplate (nhìn hình 3). Mẫu này sẽ hiển thị danh sách các sản phẩm như là các liên kết bên trong một danh sách không có thứ tự (các thẻ UL và LI).
<asp:ListView ID="ProductList" runat="server"
DataKeyNames="ProductId"
OnSelectedIndexChanging="ProductList_SelectedIndexChanging"
OnSelectedIndexChanged="ProductList_SelectedIndexChanged">
<LayoutTemplate>
<ul ID="itemPlaceholderContainer" runat="server">
<asp:PlaceHolder ID="itemPlaceholder" runat="server" />
</ul>
</LayoutTemplate>
<ItemTemplate>
<li><asp:LinkButton runat="server" ID="Select" CommandName="Select" Text='<%# Eval("Name") %>' /></li>
</ItemTemplate>
</asp:ListView>
Chý ý rằng bản thân các liên kết không phải là các liên kết thông thường, thay vào đó là các điều khiển LinkButton, điều này có nghĩa là chúng sẽ post thông tin trở lại trang thay vì chuyển hướng tới một trang khác. Chúng là các liên kết hữu dụng và vẫn mang đầy đủ đặc tính của nút. Ngoài cách sử dụng LinkButton, có thể cải thiện khả năng truy cập trang một cách dễ dàng với việc sử dụng JavaScript cùng với các điều khiển Button đã được định kiểu một cách thích hợp.
Đặc điểm chính của LinkButtons mà tôi dùng ở đây là thay vì gắn bộ xử lý sự kiện cho mỗi nút, tôi thiết lập thuộc tính CommandName là “Select”. Kết quả nhận được là khi click, nút sẽ xuất hiện một lệnh trên cây điều khiển tới khi nó được xử lý bởi một điều khiển hiểu nó. Đây là một đặc điểm rất mạnh trong các phần tử tùy biến giao diện người dùng để gửi các lệnh tới các điều khiển cha của nó mà không cần biết nhiều hơn những lệnh và tham số nó cần. Đó là những gì mạnh mẽ cho phép kiểm soát dữ liệu như ListView để vẫn cho phép người phát triển hoàn toàn kiểm soát việc đánh dấu (markup). Bạn sẽ thấy cách này tương tự với các khái niệm xử lý trên trình duyệt khi tôi xây dựng một phiên bản thuần AJAX của trang này.
Ở giai đoạn này, tôi có một danh sách sản phẩm hỗ trợ việc lựa chọn mà không phải viết bất cứ một đoạn mã nào. Ta có thể tiếp tục công việc mà không cần viết code bằng việc sử dụng các điều khiển DataSource và ControlParameters để liên kết dữ liệu được chọn trong danh sách với các dữ liệu quan trọng của phần thông tin chi tiết, nhưng tôi đã chọn để làm điều đó với mã lệnh. Tiếp theo, tôi sẽ xử lý sự kiện SelectedIndexChanged của danh sách và gọi phương thức BindProductDetails với mã sản phẩm tương ứng:
protected void ProductList_SelectedIndexChanged(object sender, EventArgs e)
{
var productId = (int)ProductList.SelectedDataKey.Value;
BindProductDetails(productId);
}
Phương thức BindProductDetails truy vấn dữ liệu để lấy ra thông tin sản phẩm và hình ảnh sau đó gắn những giá trị này vào các điều khiển tương ứng trong khung xem chi tiết.
Những hình ảnh đang được triệu gọi bởi một bộ xử lý đơn giản là các truy vấn dữ liệu cho các ảnh kiểu bytes và sao chép chúng lên một dòng nhị phân phản hồi (response’s binary stream), xem đoạn code dưới. Xử lý này sẽ được dùng trong cả ba phiên bản của trang. Hiện tại tôi có một khung định hướng dữ liệu chi tiết về sản phẩm của mình, được viết hoàn toàn bằng sự pha trộn giữa mã lệnh bắt buộc và mã lệnh tự khai báo phía máy chủ, nhưng có một số thứ có thể được cải thiện.
public void ProcessRequest (HttpContext context) {
int id;
if (int.TryParse(context.Request.QueryString["id"], out id)) {
context.Response.ContentType = "image/gif";
AdventureWorksDataContext dc = new AdventureWorksDataContext();
var bytes = dc.ProductPhotos
.Where(p => p.ProductPhotoID == id)
.Single().LargePhoto;
context.Response.OutputStream.Write(bytes.ToArray(), 0, bytes.Length);
}
else {
throw new HttpException(404, "Image not found");
}
}
Đây là một hình thức rất điển hình của trang web trong đó web hoạt động theo kiểu lưu trạng thái (stateful). Một cách nhanh chóng kiểm tra mã nguồn đã được hiển thị trên của trình duyệt trong việc kiểm tra 4KB của đối tượng ViewState do thực tế là khác nhau ở mỗi điểm nhìn, đối tượng này ghi nhớ tất cả các trạng thái bên trong và mang nó theo cùng với mỗi postback.
Đồng thời, trang web hiện tại không biết về trạng thái hiện thời của nó; không có vấn đề gì bạn làm trên trang web, một URL trong thanh chuyển hướng của trình duyệt vẫn còn “1_WebForm.aspx”. Nếu một người dùng đánh dấu trang, người đó sẽ luôn luôn xem trang ban đầu mà không hiển thị chi tiết bất kỳ sản phẩm nào.
Điều này có thể được sửa lại trong ví dụ này bằng cách thay đổi các liên kết trong danh sách sản phẩm, chúng sẽ được thường xuyên liên kết thay vì dùng các điều khiển LinkButton và thay thế việc lựa chọn postbacks với sự chuyển hướng rõ ràng tới trang chi tiết (tôi thậm chí có thể bật tắt ViewState và tiết kiệm được khoảng 4KB hai lần cho mỗi vòng). Nếu bạn để ý những bước tiến gần đây của thư viện ASP.NET Model View Controller (MVC) (xem http://msdn.microsoft.com/magazine/cc337884), bạn đã có thể đoán rằng đây là một trường hợp điển hình, nơi tạo ra một phương pháp tiếp cận MVC với nhiều ý nghĩa.
Phương pháp tiếp cận dựa trên việc điều hướng cũng có nhiều cải thiện khả năng tìm kiếm trang web (đây có thể là cả một chủ đề cho một tài liệu khác). Điều đó cho biết, các ứng dụng hướng dữ liệu điển hình sẽ có nhiều phức tạp hơn ví dụ đơn giản này và cách điều hướng thông thường không phải là cách thức đúng để xây dựng giao diện người dùng. Vì vậy, ngay cả với ứng dụng đơn giản này, tôi sẽ dành thời gian để minh họa qui trình postback và các khái niệm Web Forms được cải thiện như thế nào với AJAX.
Cả qui trình postback và chuyển hướng liên kết đều có thể cải thiện hơn cho người dùng kinh nghiệm như bạn muốn: trong khi cả postback và chuyển hướng, giao diện người dùng được “đóng băng” và không có giao dịch của người sử dụng nào khác có thể cho tới khi máy chủ đáp ứng với các nội dung mới, mà sau đó cần phải được tải lại để thay thế toàn bộ tài liệu, đôi khi mất một phần trạng thái, chẳng hạn như vị trí di chuyển.
Một vấn đề mà người sử dụng đặt rất nhiều kỳ vọng là cách hoạt động của nút quay lại (Back) và lưu lịch sử truy cập Web (History). Thật không may, trong mô hình postback, bạn có ít hoặc không kiểm soát được những gì lưu trong lịch sử hoặc những gì sẽ xảy ra khi người dùng ấn nút Quay lại (Back), chuyển tiếp (Forward), hoặc làm mới lại trang (Refresh). Một cách lý thưởng, những gì tạo ra một thay đổi của trạng thái và tạo ra một điểm lưu lịch sử nên được đặt vào tay các nhà phát triển, nhưng trong một ứng dụng postback, hầu như bất kỳ người sử dụng tương tác sẽ tạo ra một mục mới trong lịch sử của trình duyệt.
Phiên bản UpdatePanel (The UpdatePanel Version)
Cách dễ dàng nhất để cải thiện trang này là sử dụng UpdatePanel. Một UpdatePanel sẽ cho phép bạn phân định các phần của trang có thay đổi khi các hành động của người sử dụng thông thường sẽ gây ra một postback. Trong trường hợp rất đơn giản này, khu vực của trang web mà tôi sẽ muốn cập nhật là khung chi tiết. Để kích hoạt một UpdatePanel cụ thế, tôi chỉ cần thêm một điều khiển ScriptManager tới trang web, ngay sau thẻ form, giống như sau:
<asp:ScriptManager ID="ScriptManager1" runat="server"/>
Tôi cũng phải thêm vào UpdatePanel bao quanh khung nhìn chi tiết:
<asp:UpdatePanel ID="UpdatePanel1" runat="server" RenderMode="Inline">
<Triggers>
<asp:AsyncPostBackTrigger ControlID="ProductList"
EventName="SelectedIndexChanged" />
</Triggers>
<ContentTemplate>
<div class="float" id="Div1">
<fieldset>
...
</fieldset>
</div>
</ContentTemplate>
</asp:UpdatePanel>
Chú ý rằng UpdatePanel này có một bộ kích hoạt (trigger) theo dõi sự kiện SelectedIndexChanged của danh sách sản phẩm. Điều này là không cần thiết khi tất cả các điều khiển mà có thể kích hoạt một phần cập nhật bên trong UpdatePanel, nhưng ở đây danh sách sản phẩm nên vẫn còn bên ngoài UpdatePanel bởi vì quá trình rendering của nó không cần được cập nhật khi sự kiện SelectedIndexChanged xảy ra. Nếu tôi không cung cấp bộ kích hoạt, một postback sẽ xảy ra thường xuyên thay vì cập nhật một phần. Ngoài ra, khi sử dụng UpdatePanel cần luôn luôn nhớ đến tất cả các điều khiển postback có thể kích hoạt việc cập nhật một phần. Quên làm điều này có thể dẫn tới việc trang web thường xuyên postback không có lí do.
Đó là tất cả vấn đề để chuyển một qui trình postback cổ điển mà có sự xuất hiện của AJAX. Nhưng nó không có vẻ như tôi thực sự có tất cả tính năng mà tôi muốn. Một trong những vấn đề mà tôi đánh mất chỉ là tôi sẽ có ít sự hỗ trợ đã có cho nút Quay lại (Back). Bây giờ nếu người dùng quay trở lại sau khi đã duyệt qua nửa tá xe đạp, người đó sẽ trở lại bất cứ trang nào trước khi người đó truy cập. Ấn nút chuyển tiếp (Forward) khi người dùng nhận ra thao tác này sẽ đưa ta trở về trạng thái mặc định của ứng dụng (trong trường hợp này là một danh sách các sản phẩm mà không được chọn).
May mắn thay, ASP.NET 3.5 SP1 cung cấp một cách đơn giản để trở về với nút Quay lại hỗ trợ cho trang web. ScriptManager bây giờ rất thuận lợi với thuộc tính EnableHistory, phương thức AddHistoryPoint, và một sự kiện Navigate tương tác với nhau cho phép các nhà phát triển ứng dụng kiểm soát lịch sử của trình duyệt vượt xa bất cứ điều gì postback cho phép. Tính năng này không chỉ mang lại những gì đã mất khi sử dụng UpdatePanel mà còn được sử dụng trong một hình thức mạnh mẽ hơn nhiều.
Sự khác biệt lớn từ postbacks thông thường là bây giờ tôi có thể quyết định chính xác những gì tạo ra thay đổi trong trong trạng thái của ứng dụng và lọc ra các tương tác người dùng nào mà tôi cho rằng không quan trọng. Tôi cũng có thể đạt được “khả năng đánh dấu” (bookmarkability) và đảm bảo các mục có ý nghĩa trong lịch sử của trình duyệt.
Để thêm vào quản lý lịch sử của trang, tôi cần phải xác định xem những thông tin nào mong muốn sẽ được lưu giữ khi sử dụng một mục đánh dấu trang (bookmark). Ở đây, chỉ có một mẩu thông tin liên quan mà nên nhập thông tin trạng thái: mã sản phẩm hiện thời đã được chọn.
Tôi sẽ cần ngăn chặn bất kỳ sự việc nào mà thay đổi trạng thái đó. Trên thực tế, chỉ có những sự kiện tôi cần xử lý là một trong những bộ kích hoạt (trigger) đã được sử dụng trước đó: sự kiện SelectedItemChanged trên danh sách sản phẩm. Tôi đã từng xử lý để ràng buộc lại khung nhìn chi tiết, vì vậy tôi sẽ viết một đoạn mã để tạo ra một điểm lịch sử mới mỗi khi sự kiện được gọi:
protected voidProductList_SelectedIndexChanged(objectsender,EventArgs e) {
var productId = (int)ProductList.SelectedDataKey.Value;
var product = BindProductDetails(productId);
if(ScriptManager1.IsInAsyncPostBack
&& !ScriptManager1.IsNavigating) {
ScriptManager1.AddHistoryPoint("product",
productId.ToString(), "AdventureWorks - "+ product.Name);
}
}
Để đảm bảo rằng sự kiện này đã không được gọi như là kết quả của việc ngưởi dùng chuyển hướng trở về trạng thái trước của ứng dụng, mã lệnh kiểm tra yêu cầu (request) là một phần của một postback không đồng bộ và rằng nó không phải là một phần của lệnh chuyển hướng. Nếu tôi không thực thi việc kiểm tra này, tôi sẽ tạo ra một điểm lưu lịch sử mới có thể sẽ đè lên bất kỳ chuyển tiếp lịch sử nào có thể tồn tại trên trình duyệt.
Sau khi việc kiểm tra này đã được thực hiện, ta có thể gọi phương thức AddHistoryPoint, truyền vào trạng thái tôi muốn, mã sản phẩm, với tên tham số là “product”. Tên gọi này là những gì sẽ được sử dụng trong việc thay đổi URL, như bạn sẽ nhìn thấy. Giá trị của chính nó cần được chuyển đổi thành 1 chuỗi kí tự. Hãy suy nghĩ về trạng thái của lịch sử như là một biểu mẫu của chuỗi truy vấn. Mẩu thông tin cuối cùng tôi đưa ra trong phương thức là tiêu đề tài liệu. Đây là một cơ hội tuyệt vời để làm người dùng có kinh nghiệm tốt hơn như việc có thể sẽ nhìn thấy thông tin lịch sử web trong menu thả xuống có nhiều ý nghĩa hơn và sẽ giúp điều hướng ứng dụng (xem Hình 5).
.gif)
Hình 5: Menu thả xuống của History
Cách để trạng thái này được lưu lại lâu dài là dùng hàm băng của URL trên trình duyệt (thành phần sau dấu # đã được thiết kế để cho phép chuyển hướng bên trong tài liệu). Lý do cho việc sử dụng cách này như phương tiện lưu trữ là bởi vì nó chỉ cho phép thêm một mục lịch sử mà không thực sự điều hướng ra khỏi trang web và trạng thái Javascript và DOM (bởi vì trình duyệt sẽ chỉ cho phép thêm một mục lịch sử nữa nếu liên kết URL thay đổi). Đi kèm với các ràng buộc mà bạn lưu giữ trạng thái trong URL là không gian đã được giới hạn. Một vài trình duyệt không cho phép nhận các URL lớn hơn 1KB. Nếu bạn cần nhiều hơn thế, nghĩa là bạn đã không chọn các thông tin liên quan tới trạng thái và bạn sẽ cần “sản sinh lại” (refactoring).
Hãy để ý cách tôi dùng mã của sản phẩm, là một mảnh nhỏ của dữ liệu, và không phải tên đầy đủ của nó, có thể sẽ nhiều thân thiện hơn nhưng thường sẽ được lớn hơn. Việc thiếu khoảng trống trong URL cũng cho thấy rằng các Web Forms và ViewState thông thường có lẽ thích hợp hơn cho các thiết kế của bạn, hơn là dùng AJAX và lưu lịch sử.
Thứ hai là trạng thái tôi đã lưu cần được khôi phục khi lịch sử được chuyển hướng. Tôi sẽ làm điều này bởi việc xử lý sự kiện Navigate trên ScriptManager (xem Hình 6). Trong đoạn mã này, trước tiên tôi xử lý trường hợp khi không có trạng thái. Trường hợp này sẽ trả về trạng thái mặc định của trang web khi tất cả các thao tác trở về GET request. Điều này có vẻ không bình thường một chút nếu bạn không biết rằng trạng thái thực sự đã được khôi phục bằng cách thực hiện một postback mới. Trong trường hợp này, trạng thái “trước” của postback, việc khôi phục được thực hiện một cách tự động, là trạng thái “sau” trong lịch sử của trình duyệt, vì vậy tôi phải xóa trạng thái đã được khôi phục và thay thế nó với trạng thái mặc định.
protected void ScriptManager_Navigate(object sender, HistoryEventArgs e) {
var productIdString = e.State["product"];
if (productIdString == null) {
ProductList.SelectedIndex = -1;
ProductDetails.DataSource = null;
ProductDetails.DataBind();
ProductModelDetails.DataSource = null;
ProductModelDetails.DataBind();
ProductPhotoList.DataSource = null;
ProductPhotoList.DataBind();
Page.Title = "AdventureWorks";
}
else {
var productId = int.Parse(productIdString);
var product = BindProductDetails(productId);
ProductList.SelectedIndex = (
from p in AdventureWorksContext.Products
where p.ProductSubcategoryID == 1 // Mountain bikes
orderby p.Name
select p).ToList().IndexOf(product);
BindProductList();
Page.Title = "AdventureWorks - " + product.Name;
}
}
Vấn đề thứ hai, bản thân trạng thái nên được xem xét và cần phải được xác nhận tính hợp lý, điều này tôi đang làm bởi việc ép nó tới kiểu số nguyên (integer). Và cuối cùng, tôi đang khôi phục lại tiêu đề của trang khi khôi phục lại trạng thái, ngoài việc cái đặt lại danh sách và trạng thái chi tiết.
Nếu bạn sử dụng trang web sau những thay đổi này, địa chỉ URL trong trình duyệt sẽ thay đổi mỗi khi một sản phẩm được chọn và sẽ xem xét một vài điều như thế này:
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&5YLQHC81D2 OEdJU/9ZBdHUip1qx3ooPKDhCLgKogupQ=
Bạn có thấy liên kết trên là rất khó đọc không? Điều này là bởi framework mặc định coi dữ liệu người dùng cung cấp là nguy hiểm, vì vậy nó băm trạng thái nhằm ngăn chặn sự xáo trộn. Tuy nhiên, trong nhiều trường hợp, người phát triển sẽ thích những thứ tương đối dễ đọc, ít khi là những liên kết khiến người nhìn phải “sợ hãi”, ngay cả khi đó có nghĩa là xác nhận trạng thái từ mã nguồn và cho phép người dùng thay đổi nó. Những liên kết này được coi là cộng thêm trong một vài ngữ cảnh (thư viện MSDN là một ví dụ điển hình; nó cho phép người dùng xây dựng những URL của riêng họ, chẳng hạn như msdn.microsoft.com/library/system.web.ui.scriptmanager.aspx, từ một sự sắp xếp chính xác bởi vì nó làm cho việc chuyển hướng dễ dàng hơn nhiều).
Để kích hoạt tính năng này, ScriptManager thiết lập thuộc tính EnableSecureHistoryState kiểu Boolean. Với việc thiết lập thuộc tính này tới giá trị false, URL của tôi trở nên thân thiện hơn, giống như sau:
http://MyServer/MSDNAjax/2_UpdatePanel.aspx#&&product=776
Kết quả là một trang web mềm dẻo hơn nhiều trang mà không phải là chỉ cần thực hiện giống phiên bản AJAX của Web Form, nhưng một trong số đó có rất nhiều các tính năng tiện dụng được bổ sung như khả năng đánh dấu trang (bookmark) và cải thiện khả năng xử lý của nút Back. Và tất cả những điều này đã được viết xong mà không có một dòng mã Javascript nào.
Phiên bản thuần AJAX
Trong khi trang web có nhiều điểm tương đồng với phiên bản UpdatePanel, tôi vẫn dùng ý tưởng của đối tượng ViewState. Để thu gọn nó, tôi cần thực hiện nhiều logic hơn tới khách hàng. Để làm như vậy, tôi cần viết một chút mã Javascript.
Bạn có thể viết rất tốt phiên bản thuần AJAX với ASP.NET AJAX 3.5 SP1, nhưng nó sẽ khá nhàm chán trong cách lấy dữ liệu và định dạng nó với HTML. Có hai cách cơ bản để chuyển dữ liệu vào các mã HTML.
Cách đầu tiên hầu hết máy khách thường sử dụng là thay thế những mẫu nội dung tĩnh sang dữ liệu có nội dung động. Điều này nghe có vẻ tương đối đơn giản và nhanh chóng vì nó sử dụng innerHTML như là một phương pháp duy nhất để tương tác với DOM. Mặc dù nó có một vài vấn đề.
Một điều nó không làm tốt là bảo vệ từ các cuộc tấn công kiểu injection (injection thông thường là cách tấn công vào hệ thống sử dụng lỗi về các chuỗi ký tự nhập vào xâu truy vấn dữ liệu). Nếu bạn săp tạo ra mã HTML bởi việc nối các xâu ký tự, bạn sẽ cần phải mã hóa tất cả dữ liệu trước khi sử dụng nó. Nếu không, việc sử dụng một dấu ghi chú (quote) trong một thuộc tính hay một thẻ script vào trong một text node, tình cờ hoặc cố ý sẽ là kết quả của việc mã lệnh tự động thực thi (điều này thật sự rất tệ). Mã hóa có vẻ khó hơn bởi vì bạn có lẽ cần các thuật toán mã hóa khác nhau tùy theo việc bạn dùng thuộc tính text, thuộc tính URL hay một text node.
Một cơ chế mẫu cũng cần một ngôn ngữ mở rộng; trong khi nó dễ để xử lý các trường dữ liệu mà không cần tới sự sửa đổi, điều này chỉ là một kịch bản đơn giản nhất. Thông thường, bạn sẽ cần phải áp dụng một định dạng chuỗi ký tự, kết hợp nhiều trường và nhiều thao tác thông thường với dữ liệu trước khi hiển thị nó. Điều này có thể được thực hiện bằng cách chuyển dữ liệu trước khi cho nó ăn vào bản mẫu, nhưng dễ dàng hơn và hiệu quả hơn nếu nó được xây dựng vào trong một cơ chế mẫu. Một khi bạn bắt đầu thêm các tính năng như định dạng, bạn rất nhanh chóng nhận ra rằng mình cần đầy đủ tính linh hoạt của một ngôn ngữ mở rộng.
Khả năng viết đan xen mã và đánh dấu (như bạn có thể làm với khối <% %> trong ASP) cho phép những kịch bản thú vị như lặp lại đánh dấu dùng một vòng lặp quanh một đoạn mã HTML hay những câu lệnh điều kiện đơn giản. Kịch bản này cần một ngôn ngữ đầy đủ để nó thực sự hữu ích.
Cuối cùng, mã HTML chỉ là một nửa của câu chuyện. Ứng dụng AJAX thực sự quan tâm về cách hoạt động của nội dung, không chỉ phỉa khách hàng cập nhật vào DOM. Sau khi bạn đã tạo ra mã HTML, bạn vẫn phải gắn các sự kiện vào phần tử và đính kèm các điều khiển cùng các hành vi (behaviors) của phần tử. Bạn có thể làm điều đó với các thể hệ HTML, nhưng tạo ra sự khó chịu giữa HTML, mà trở nên rất rõ ràng, và logic, trong đó trở nên khó khăn và đòi hỏi nhiều kiến thức hơn cấu trúc mẫu.
Nói cách khác, để đính kèm các hành vi, bạn cần phải biết nơi để làm việc này, mà lần lượt có nghĩa là bất kỳ thay đổi nào trong cấu trúc mã HTML mẫu sẽ đòi hỏi thay đổi trong code để kích hoạt nó. Có nhiều cách để thực hiện điều này nhưng ngay cả khi một giải pháp tốt hơn sẽ được thực hiện biến nội dung thành một thành phần của cơ chế mẫu.
Cách khác, bạn có thể tạo ra mã HTML từ dữ liệu bằng cách thao tác trực tiếp với các hàm API của DOM và tạo ra các phần tử, thuộc tính, và text node từ mã. Lúc đầu, điều này giống như một lựa chọn tồi bởi một vài lý do. Chác chắn, nó có giá trị, nhưng nó chậm hơn innerHTML. Tuy nhiên lý do chính cho việc không thường sử dụng các hàm API của DOM là vì các hàm này thiếu sự diễn đạt rõ ràng, và mã lệnh kết quả khó đọc và càng khó hơn trong việc bảo trì mà không có sự giúp đỡ nào. Hiện nay có những bộ công cụ giống như Jquery cung cấp các cách thức trừu tượng hóa đa dạng và làm toàn bộ qui trình trở nên thú vị hơn, nhưng ngay cả khi dùng các công cụ, nó cũng khó hơn mức cần thiết (đây là lý do tại sao Jquery có một vài mẫu plug-ins).
Một số bạn có lẽ đã biết rằng Microsoft đã từng có một cơ chế mẫu trong ASP.NET AJAX tương lai, nhưng nó đã quá chậm và phức tạp trong thiết kế và chúng tôi muốn làm nhiều thứ tốt hơn nhiều. Điều tốt đẹp là chúng tôi đã thu được nhiều kinh nghiệm và chúng tôi không muốn phiên bản mới lặp lại các khiếm khuyết cũ (sự chậm chạp và tính phức tạp).
Đội ngũ phát triển có nhiều thiết kế khác nhau cho một cơ chế mẫu mới của ASP.NET AJAX, từ việc nối kết các chuỗi để thao tác hoàn toàn với DOM, và chúng tôi đánh giá chúng trên kết quả thực thi, tính đơn giản và tính linh hoạt. Chúng tôi cũng so sánh chúng trong các thuật ngữ về những kịch bản họ ngăn chặn xảy ra. Không có giải pháp lý tưởng, nhưng chúng tôi đã lựa chọn một trong những công việc mà có vẻ tốt hơn như đã hứa.
Nguyên tắc của cơ chế mẫu mới khá đơn giản: chúng tôi lấy mẫu mã lệnh của bạn, trong đó chứa mã HTML, trường dữ liệu, các biểu thức, các khai báo thành phần và các mã lệnh bắt buộc, và chúng tôi đã tự động nhúng thêm mã Javascript , tạo ra các mã HTML tương đương. Điều này trông đơn giản và no slaf tốt cho tới khi trình duyệt xuất hiện). Chúng tôi phải trả một mức hình phạt đối với hiệu xuất sử dụng DOM API, nhưng nếu chúng tôi đang xây dựng thận trọng các yếu tố bên ngoài DOM, và thêm một vài thứ có thể, hiệu quả hoạt động không phải là quá lớn và tạo ra sự linh hoạt tuyệt vời, nhưng nó tạo ra một lợi thế để thương mại. Tất cả các vấn đề trong việc tiếp cận ghép nối xâu chuỗi có vẻ tự dưng biến mất.
Tấn công kiểu injection quan tâm tới bản thân chúng. Như tôi đang sử dụng mã để tạo ra text nodes và thiết lập giá trị thuộc tính, không cần thiết phải mã hóa bởi vì các hàm API tôi dùng đã được an toàn. Điều này được sử dụng tương tự như các tham số SQL so với việc xây dựng SQL bởi việc ghép nối thay thế các xâu ký tự. Tại sao bạn vẫn muốn những nguy cơ tương tự ở đây?
Bây giờ ai cần có một ngôn ngữ biểu thức mới? Chúng ta đã từng có một: Javascript. Khi nào bạn chuyển biểu mẫu đánh dấu vào mã Javascript, cái gì có thể trở nên dễ dàng hơn việc đưa mã Javascipt vào trong mã bạn tạo ra?
Để kích hoạt việc phát triển các mẫu phổ biến nhất, một lần, trích trường dữ liệu một chiều (trên máy chủ được thể hiện bởi “<%=biểu thức%>” và trong hệ thống của chúng tôi là “{{ biểu thức }}”), tôi dùng một đặc tính của Javascript mà thường bị xem nhẹ - từ khóa “with”. Nó giúp tôi không phải sử dụng biểu thức như “{{ dataItem.myField }}” để đưa một trường dữ liệu kết hợp với mẫu thể hiện. Nhờ có từ khóa “with”, bạn có thể bao quanh mã được tạo ra cho mẫu (template) giống như “with(dataItem) {…}” để bất kỳ thành viên nào của đơn vị dữ liệu được đẩy lên phạm vi trên cùng của hàm mẫu, làm cho biểu thức đưa lên đơn giản như “{{ myField }}”.
Bạn có thể đưa hành vi vào mẫu (template) theo 2 cách. Thứ nhất, bạn có thể viết $attachEvent và $create từ sự kiện itemCreated, hay viết ngay trong template sử dụng một biến đặc biệt $element mà có sẵn từ bên trong template và tham chiếu tới các phần tử mới nhất đã được tạo. Hoặc bạn có thể dùng cú pháp khai báo đã được chúng tôi cung cấp. Ví dụ, nếu bạn muốn thêm một thuộc tính autocomplete và một hành vi watermark vào một thẻ input, bạn sẽ viết như sau:
<body xmlns:sys="BLOCKED SCRIPTSys"
xmlns:autocomplete="BLOCKED SCRIPTAjaxControlToolkit.AutoCompleteBehavior"
xmlns:watermark="BLOCKED SCRIPTAjaxControlToolkit.extBoxWatermarkBehavior">
...
<input id="search" sys:attach="autocomplete,watermark"
autocomplete:servicepath="SearchAutoComplete.asmx"
watermark:watermarktext="Type your search terms here" />
Ở đây tôi đăng ký tiền tố cho mỗi hành vi được khai báo trên HTML hoặc thẻ body (hay trên thẻ cha cho mẫu) bằng cách sử dụng khai báo namespace xmlns XHTML. Điều này cho phép tôi mở rộng đánh dấu XHTML trong một cách chuẩn, và nó tương tự chỉ thị @Register cho mã máy chủ. Thành phần sau “xmlns:” là tiền tố có nghĩa là sẽ được kết hợp với mỗi hành vi hay điều khiển. Liên két cho namspace để ánh xạ tiền tố tới một loại Javascript cụ thể. Namespace “sys” là một hệ thống namespace đặc biệt mà cần được ánh xạ tới namespace Sys, là namespace gốc trong AJAX.
Các cài đặt chính được thực hiện thông qua thuộc tính đặc biệt sys:attach, giá trị của nó là một dấu phẩy phân cách danh sách các tiền tốt của các hành vi hay điều khiển để cài đặt và đính kèm tới phần tử. Sau đó, tôi có thể thiết lập thuộc tính cho tất cả những hành vi mà không xung đột với các thuộc tính HTML hay các hành vi khác trên cùng phần tử, bởi vì chúng được tạo ra sự khác biệt bằng namespace.
Một trong những tính năng tao nhã của cơ chế này là việc biên soạn các mẫu vào Javascript thực sự tương tự với việc biên soạn thực tế. Điều này có nghĩa là nó chỉ có thể xảy ra một lần cho mỗi mẫu và cung cấp các cơ hội để thực hiện một số nhiệm vụ trước mắt về thời gian thay vì thực hiện mỗi lần mẫu được thiết lập. Nhưng lý thuyết đó là đủ cho hiện tại. Làm thế nào để áp dụng điều này tới trang master/details?
Các mẫu phiên bản AJAX
Template cho danh sách của sản phẩm là hoàn toàn dễ hiểu :
<ul id="productListTemplate" class="sys-template">
<li>
<a href="{{ String.format('3_Client.aspx?product={0}',
ProductID) }}">{{ Name }}</a>
</li>
</ul>
Các mẫu Item (item template) là một danh sách item với link đơn giản trong nó. Dòng text của link chỉ là tên của sản phẩm ("{{ Name }}") và thuộc tính href là chuỗi định dạng xây dựng từ id của product sử dụng Javascript rõ ràng:
"{{ String.format('5_Client.aspx?product={0}', ProductID) }}"
Lớp "sys-template" được định nghĩa trong CSS cho trang để ẩn đi các template từ việc trả lại ban đầu của trang. Những dòng code được biên dịch cho những template ví dụ được đưa ra ở Figure 7. Nội dung chính là một chút phức tạp và thực chất chứa một vài dòng code ở trong (Figure 8). Tôi có thể đã sử dụng nested template để trả về danh sách của photo, nhưng nó là một đơn giản nhỏ để sử dụng vòng lặp đều đặn trên markup cho một photo. Khối template lồng nên hợp lý nếu tôi đã phân phát với dữ liệu biến đổi động và việc ánh xạ của những thay đổi trong markup (là hoàn cảnh đã được hỗ trợ, nhưng bên ngoài của vùng của article, nhưng bởi vì tôi đang phân phát với một cách, 1 lần bind ở đây, dòng code ở trong là rất tốt.
Hình 7: Code mẫu đã được biên dịch
function(__containerElement, $dataItem, $parentContext, __instanceId) {
var __context = {}, $component, __app = Sys.Application,
__creatingComponents = __app.get_isCreatingComponents(),
__components = [], __componentIndex, __e, __f, __topElements = [],
__p = [__containerElement], $index = __instanceId,
$id = Sys.Preview.UI.Template._getIdFunction(__instanceId),
$element = __containerElement;
Sys.Preview.UI.Template._contexts.push(__topElements);
with(__context) { with($dataItem || {}) {
$element=__p[1]=document.createElement('LI');
__topElements.push($element);
$element=__p[2]=document.createElement('A');
$component = $element;
__e = document.createAttribute('href');
__e.nodeValue = String.format('5_Client.aspx?product={0}',
ProductID);
$element.setAttributeNode(__e);
__p[1].appendChild($element);
__p[2].appendChild(document.createTextNode(Name));
$element=__p[2];
__p[1].appendChild(document.createTextNode(" "));
$element=__p[1];
}
}
for (var __i = 0, __l = __topElements.length; __i < __l; __i++) {
__containerElement.appendChild(__topElements[__i]);
}
Sys.Preview.UI.Template._contexts.pop();
return new Sys.Preview.UI.TemplateResult(this, __containerElement, __topElements, __components);
}
Hình 8: Hiển thị chi tiết
<div class="sys-template" id="Div2">
<fieldset>
<legend>{{ Name }} ({{ ProductNumber }})
{{ String.format("{0:C}", ListPrice) }}</legend>
<ul class="photoList">
<!--* for (var i = 0; i < Photos.length; i++) { *-->
<li>
<img src="{{String.format('productphoto.ashx?id={0}', Photos[i]) }}" />
</li>
<!--* } *-->
</ul>
<table>
<tr><td class="label">Summary:</td><td>
{{ Summary }}
</td></tr>
<tr><td class="label">Experience:</td>
<td>{{ RiderExperience }}</td></tr>
...
<tr><td class="label">Style:</td>
<td>{{ Style }</td></tr>
<tr><td class="label">Wheel:</td>
<td>{{ Wheel }}</td></tr>
<tr><td class="label">Maintenance:</td>
<td>{{ MaintenanceDescription }}</td></tr>
</table>
</fieldset>
</div>
Templates biên dịch chậm ở lần đầu chúng ta đã thuyết minh nhưng chúng ta đã chuẩn bị bằng cách tạo ra "new Sys.Preview.UI.Template" sử dụng element cha của template markup như tham số của constructor. Các Templates tự chúng được thuyết minh từ hàm gọi từ mạng gọi mang trả dữ liệu từ Web services trên server.
AdventureWorks.GetProducts(1 /* Mountain bikes */,
function(productArray) {
renderProductList(productArray, productListTemplate);
selectProduct(initialProductID, true);
});
function renderProductList(productArray) {
var target = $get("productList");
target.innerHTML = "";
for (var i = 0, l = productArray.length; i < l; i++) {
productListTemplate.createInstance(target, productArray[i]);
}
}
Sẽ không cần thiết trong việc chuyển phiên bản của ASP.NET 4.0; Đó là DataView component sẽ quan tâm đến sự phân tích template, biên dịch và thuyết minh. Hầu hết code trong ứng dụng cuối cùng sẽ đi ra, nhưng nó có tác dụng chỉ ra các thứ xảy ra dưới hood. Nó đồng thời chỉ ra người lập trình thành phần là người muốn bao gồm template trả về có thể sử dụng các đặc điểm.
Event Bubbling
Chúng ta đưa code vào chỗ nào thì hiển thị thông tin chính xác khi mà người dùng click vào một trong các sản phẩm? Code, giống như là tương đương phía máy chủ, sử dụng bong bóng sự kiện, vậy tôi có thể viết một sự kiện click cho tất cả các link trong một danh sách (vậy tôi có thể thêm hoặc bỏ link từ list nếu tôi muốn mà ko cần lo lắng về việc tạo ra các sự kiện mới hoặc xóa sạch một cái gì cũ). Những dòng code theo chỉ ra cách điều khiển. Tất cả các sự kiện click cho link trong list sẽ nổi lên đến danh sách và có thể điều khiển. "e.target" là tham chiếu đến các phần tử để nhận được việc đã click; mặt khác, đó là link, đó là khả năng tôi trả về id của sản phẩm từ thuộc tính href và chọn sản phẩm liên quan:
$addHandler($get("productList"), "click", function(e) { varhref = e.target.href;
selectProduct(parseInt(href.substring(href.indexOf('=') + 1), 10));
e.preventDefault();
e.stopPropagation();
});
Chỉ một lần là xong, sự kiện mặc định (the link navigation) là bị xóa bỏ và sự kiện ngăn cản từ việc nổi lên xa hơn. Nó được hoàn thành bởi gọi W3C hàm tiêu chuẩn stopPropagation và preventDefault trên đối tượng sự kiện, đó là framework tạo ra để dùng được trên trình duyệt, bao gồm cả IE.
Quản lý nút quay lại
Đặc điểm duy nhất còn lại để tái sản xuất từ phiên bản phía máy chủ là history. History có khả năng trên ScriptManager điều kiển đồng thời hiển thị ở phía máy khách. API chính xác ra là tương tự API ở server-side. Tôi đã sử dụng trước và có thể sử dụng cùng một lúc (cho phép trộn lẫn sự quản lý trạng thái giữa máy khách và máy chủ).
Tạo ra history point hoàn thành bởi gọi Application.addHistoryPoint từ các sự kiện đó là phù hợp đến thay đổi trạng thái, trong trường hợp này, đã click vào sản phẩm trong danh sách :
Sys.Application.addHistoryPoint({product:productDetails.ProductID}, "AdventureWorks - " + productDetails.Name);
Một cách tương ứng, trạng thái được khôi phục từ sự kiện “điều hướng” trên Sys.Application. Tham số HistoryEventArgs mà bộ xử lý sự kiện nhận một thuộc tính trạng thái mà cho phép bạn lấy lại sản phẩm để khôi phục:
Sys.Application.add_navigate(function(sender, e) {
var ProductID = parseInt(e.get_state()["product"], 10);
selectProduct(ProductID, true);
});
Trang kết quả chạy giống phiên bản UpdatePanel rất nhiều, nhưng đó ko phải là sự so sánh trong giới hạn của mạng. Khi mà một sản phẩm được chọn, UpdatePanel gửi hơn 4KB dữ liệu đến server và nhận về 8KB. Phiên bản thuần AJAX, nói cách khác, chỉ gửi "{"productId":771}" cộng với phần header của chuẩn HTTP và nhận về 2KB của dữ liệu thuần JavaScript Object Notation (JSON). Đó là khoảng 10KB được lưu lại trong tất cả những lần người dùng click vào sản phẩm.
Đây là một đặc điểm thú vị được đưa ra cho ASP.NET 4.0. Chia sẻ suy nghĩ của bạn tại go.microsoft.com/fwlink/?LinkId=126987
Tiến sỹ Bertrand Le Roy trưởng dự án AJAX tại Microsoft với 5 năm kinh nghiệm chuyên sâu về AJAX. Ông đại diện cho Microsoft tại liên minh OpenAjax.=