天天看點

直接通過User Control生成HTML-asp.net頁面的換皮膚方案

前些日子看了園友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> </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了.有意思.