前些日子看了園友Jeffrey Zhao的關于User Control生成HTML的兩篇文章. 因為我不喜歡看到我們的工程中有比較多的ashx檔案(同時對于IHttpHandler接口,我的意見是盡量嘗試不用IHttpHandler),就琢磨了一下如何不用這個ashx和IHttpHandler也能做到同樣的功能,有點發現.在這裡提出另外一個方法,和大家分享.核心就是覆寫Page類的Render方法.
先來看我的一個用來做皮膚的HTML檔案HTMLPage1.htm:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<TITLE></TITLE>
<META NAME="GENERATOR" Content="Microsoft Visual Studio 7.0">
</HEAD>
<BODY>
<P><FONT face="宋體">test</FONT></P>
<P><FONT face="宋體">testee</FONT></P>
<!--$MyUserControl1$-->
<P><FONT face="宋體">teest</FONT></P>
<P><FONT face="宋體">Tttttest</FONT></P>
<!--$MyUserControl2$-->
<P><FONT face="宋體">Ttttessssttt</FONT></P>
<P>&nbsp;</P>
</BODY>
</HTML>
這個HTML檔案十分簡單.當然實際中你可以做得很複雜,做試驗就簡單點.隻要原理通就可以了. 其中有兩個特别得地方:<!--$MyUserControl1$-->是用來标記在此處要嵌入一個叫MyUserControl1的UserControl, <!--$MyUserControl2$-->也類似.就是說這個HTML檔案有兩塊地方的内容要由兩個UserControl來提供.這是一個特殊的HTML檔案,需要一個特殊的呈現器來将其解釋并呈現出來.我們來看這個特殊的呈現器:一個覆寫了Render方法的aspx頁面.
WebForm1.aspx:
<%@ Page language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="True" EnableEventValidation="false" Inherits="WebApplication1.WebForm1" validaterequest="false" %>
<title>WebForm1</title>
<meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">
<meta name="CODE_LANGUAGE" Content="C#">
<meta name="vs_defaultClientScript" content="JavaScript">
<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
<body>
<form id="Form1" method="post" runat="server">
</form>
</body>
其實就是一個空的aspx頁面,有一個伺服器端的Form而已. 稍有不同的是要加上: EnableEventValidation="false"和validaterequest="false",原因稍後再說.
再來看這個頁面的後端代碼WebForm1.aspx.cs:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace WebApplication1
{
/// <summary>
/// Summary description for WebForm1.
/// </summary>
public partial class WebForm1 : System.Web.UI.Page
{
protected void Page_Load(object sender, System.EventArgs e)
{
MyUserControl1 ctl1;
MyUserControl2 ctl2;
ctl1 = (MyUserControl1)this.Page.LoadControl("MyUserControl1.ascx");
ctl2 = (MyUserControl2)this.Page.LoadControl("MyUserControl2.ascx");
this.Page.Controls.Add(ctl1);
this.Page.Controls.Add(ctl2);
}
public override void VerifyRenderingInServerForm(Control control)
return;
protected override void Render(HtmlTextWriter writer)
StringBuilder strBuilder;
System.IO.StringWriter strWriter;
System.Web.UI.HtmlTextWriter htmlWriter;
string strResponse;
string strControl;
System.IO.FileStream fileStream = System.IO.File.OpenRead(Server.MapPath("HTMLPage1.htm"));
System.IO.TextReader rd = new System.IO.StreamReader(fileStream);
strResponse = rd.ReadToEnd();
strBuilder = new StringBuilder();
strWriter = new System.IO.StringWriter(strBuilder);
htmlWriter = new System.Web.UI.HtmlTextWriter(strWriter);
this.Controls[1].RenderControl(htmlWriter);
strControl = strBuilder.ToString();
strControl = strControl.Replace("</form>", string.Empty);
strResponse = strResponse.Replace("<BODY>", "<BODY>" + strControl);
strResponse = strResponse.Replace("</BODY>", "</FORM></BODY>");
this.Controls[3].RenderControl(htmlWriter);
strResponse = strResponse.Replace("<!--$MyUserControl1$-->", strControl);
this.Controls[4].RenderControl(htmlWriter);
strResponse = strResponse.Replace("<!--$MyUserControl2$-->", strControl);
writer.Write(strResponse);
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
InitializeComponent();
base.OnInit(e);
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
#endregion
}
}
解釋:
1. Page_Load中須将要呈現的兩個UserControl裝入,并且不管是Get還是Post都要執行.隻有這樣才能讓這兩個UserControl經曆Page請求的生命周期,同時讓asp.net維持這兩個UserControl的ViewState和用戶端的事件響應代碼.
2.覆寫VerifyRenderingInServerForm方法是為了讓asp.net引擎不産生一些諸如UserControl中的伺服器端TextBox控件需要在一個伺服器端的Form中運作的錯誤資訊.這樣在UserControl中放置任何一個伺服器端控件都可以了.
3.覆寫預設的Render方法. 這個自定義的Render方法先讀入我的Html皮膚檔案. 然後構造了htmlwriter, 用于儲存form的html輸出. 我在WebForm1.aspx裡面定義的一個空的伺服器端Form, 用RenderControl方法來取得其HTML輸出.注意,因為這個Form經曆了Page請求的生命周期,是以這個Form的HTML輸出都帶有ViewState和客戶段PostBack響應的javascript代碼. 請特别注意這幾行代碼:
strControl = strControl.Replace("</form>", string.Empty); //去掉form的HTML内容最後的</form>,隻留下前面的<form>....
strResponse = strResponse.Replace("<BODY>", "<BODY>" + strControl); //asp.net form和HTML皮膚檔案進行結合
strResponse = strResponse.Replace("</BODY>", "</FORM></BODY>"); //asp.net form和HTML皮膚檔案進行結合
我們已經得到Form的HTML輸出,其内容是這樣的: <form>..........</form>. <form>标簽在兩頭. 因為要把這個Form的内容和HTML皮膚的内容相結合.必須在皮膚HTML中的<BODY>後面加上<form>的内容, 同時在皮膚HTML中的</BODY>之前加上</form>. 這樣就成了一個asp.net的form. 有讀者可能會問,能不能在此頁面中做多個伺服器段的Form, 回答是不行. 原因是asp.net的引擎從1.0開始就不支援一個aspx頁面中有多一個的伺服器段form.據我所知這一點一直沒有改變.
Form的HTML内容已經處理了, 兩個UserControl也類似, 用RenderControl方法取到其HTML内容, 再用String的Replace方法将我們的特殊标記替換成UserControl的HTML内容. 注意,因為這兩個UserControl經曆了Page請求的生命周期, 是以其HTML就帶有相應的用戶端javascipt PostBack方法, 能響應如Click等等事件.
4. 關于WebForm1.aspx裡面的EnableEventValidation="false"和validaterequest="false", 因為asp.net伺服器在處理Post過來的請求時要驗證其請求是否原始的Page産生,如果我們不加兩個false, 當我們點選UserControl上的按鈕進行PostBack時, asp.net伺服器會保錯. 因為這個改造的Page已經不是我們原始的那個Page.
關于通用化的考慮:
1.大凡采用皮膚的asp.net應用一般不會隻有一套皮膚,這些皮膚HTML可以存在資料庫中. 在用的時候不必象這裡采用的方式從一個HTML檔案中取出來,而可以采用從資料庫中取出.
2.針對不同頁面内容,HTML皮膚可以引入不同的UserControl. 這就要求特殊的呈現器在Page_load中解析HTML皮膚,根據HTML皮膚的指定,來裝入相應的UserControl.
3. 如何向UserControl傳遞參數? 因為有些UserControl需要一些參數才能顯示相應的内容, 我想在UserControl的代碼中完全可以通路Request.Form或者Request.QueryString來取得所需要的參數.
關于記憶體開銷,性能的問題
目前這樣的樣例代碼還沒有考慮太多這方面的問題. 我認為可能的問題包括:
如果一個HTML皮膚引入較多的UserControl時,可能會有性能上的問題.
在這裡的String.Replace比較多.記憶體開銷可能會比較大.
水準有限,歡迎指正.
另注: 此方法在asp.net 1.1/2.0/3.0下都可行
對部落格園的話:不知道怎麼回事,居然點一下發出兩篇Post了.有意思.