在Asp.net 1.1下实现MasterPage

     Asp.net 2.0,提供了一个MasterPage的功能,它可以让我们很方便的完成页面的整体结构相同的网站,而且后期修改界面的时候只要修改一下MasterPage即可,无需一个个界面进行修改,这样就大大的方便了开发人员。
    可惜的是,在Asp.net 2.0以前的版本中,并不包含MasterPage的特性。虽然现在使用Asp.net 2.0或以上版本的开发者越来越多,但是常常由于项目周期长等原因,还有很大数量的开发人员使用Asp.net 1.1进行开发(比如我自己)。所以,虽然Asp.net 2.0发布这么长时间了,我们这些可怜的人还是无法应用。这不,.net 3.5都出来了,呵呵,感觉自己越来越落后了。
   网上看到过别人在.net 1.1下实现类似MasterPage的功能,但感觉都不是很直观,而且用法和.net 2.0下的MasterPage相差比较大。前天翻开Spring.net来看,发现它的Web框架实现了.net 1.1下的MasterPage,而且他的用法和.net 2.0下的用法一样。自己也照着葫芦画了个瓢,不敢私藏,特拿出来分享。
1 主要原理

根据Asp.net 2.0的MasterPage应用,我们知道:
1. MasterPage里面包含ContentPlaceHolder控件用做为具体内容的容器
2. 使用母版页的页面包含Content控件提供具体内容
3. 页面展现时,会将MasterPage的内容展现出来,并把Content控件下的内容填充到ContentPlaceHolder中。

仿照Asp.net 2.0,对应到Asp.net 1.1下面应该是:
1. 做两个自定义控件,分别叫做ContentPlaceHolder和Content
2. MasterPage是一个用户控件(ascx),里面包含ContentPlaceHolder控件做为具体内容的容器
3. 使用母版页的页面包含Content控件提供具体内容
4. 页面展现时,会将MasterPage的内容展现出来,并把Content控件下的内容填充到ContentPlaceHolder中。

是啊,就这么简单,没啥特别的东西,但没看到Spring.net之前,怎么就没想过去实现这么个MasterPage呢?

2 ContentPlaceHolder和Content控件

前面说了,ContentPlaceHolder是内容的容器,Content控件是具体的内容,他们的代码如下:

Content.cs:

 1using System.Web.UI;
 2using System.Web.UI.WebControls;
 3namespace ZSoft.Web.UI.WebControls
 4{
 5    /// <summary>
 6    /// 内容控件
 7    /// </summary>

 8    [PersistChildren(true)]
 9    [ParseChildren(false)]
10    public class Content : Control
11    {
12        private string contentPlaceHolderID;
13
14        public string ContentPlaceHolderID
15        {
16            get return contentPlaceHolderID;}
17            set { contentPlaceHolderID = value;}
18        }

19    }

20}

21

 

可以看到,Content控件包含一个属性ContentPlaceHolderID,用以指向母版页的ContentPlaceHolder控件。

ContentPlaceHolder.cs:

 1using System.Web.UI;
 2using System.Web.UI.WebControls;
 3
 4namespace ZSoft.Web.UI.WebControls
 5{
 6    /// <summary>
 7    /// 内容容器
 8    /// </summary>

 9    [PersistChildren(true)]
10    [ParseChildren(false)]
11    public class ContentPlaceHolder : WebControl
12    {
13        private Content content;
14
15        public Content Content
16        {
17            get return content;}
18            set { content = value;}
19        }

20
21        protected override void Render(HtmlTextWriter writer)
22        {
23            //如果Content控件不为null,则展现Content控件的内容,否则,展现自己的内容
24            if(content != null)
25            {
26                content.RenderControl(writer);                
27            }

28            else
29            {
30                base.Render (writer);
31            }

32        }

33    }

34}

35

 

ContentPlaceHolder控件拥有一个属性Content,指向一个Content控件实例,在下面的代码中你会看到它是何时被赋值的。同时,ContentPlaceHolder重写了Control控件的Render方法,当它拥有一个Content控件的实例的时候,展现Content控件的内容,否则,展现自己的内容(用于展现默认内容)。
    注意PersistChildren(true)ParseChildren(false),这两句指定了这两个控件是可以包含子控件的,这非常重要,因为不管是ContentPlaceHolder,还是Content控件,都需要拥有子控件(ContentPlaceHolder用子控件来表示默认的内容,Content用子控件表示具体的要替换的内容)。
    上面说到,ContentPlaceHolder控件拥有一个属性Content,指向一个Content控件实例,那么,这个实例是什么时候被赋值的呢?
    我们知道,MasterPage应该是一个用户控件,并且ContentPlaceHolder控件是包含在MasterPage控件里的,所以,我们应该在MasterPage里去初始化ContentPlaceHolderContent属性。在页面初始化时,根据页面的MasterPageFile属性,加载MasterPage控件,然后初始化该控件里的ContentPlaceHolder。这样,我们就需要另外两个类,MasterPage基类和Page基类,分别对应母版控件和使用母版的页面。
3 Page和MasterPage

Page.cs:

 1using System;
 2using System.Web.UI;
 3
 4namespace ZSoft.Web.UI
 5{
 6    /// <summary>
 7    /// 页面基类
 8    /// </summary>

 9    public class Page : System.Web.UI.Page
10    {
11        private string masterPageFile;
12        private MasterPage master;
13        
14        /// <summary>
15        /// 母版的路径
16        /// </summary>

17        public string MasterPageFile
18        {
19            get return masterPageFile;}
20            set { masterPageFile = value;}
21        }

22
23        /// <summary>
24        /// 是否有母版
25        /// </summary>

26        public bool HasMaster
27        {
28            get return this.MasterPageFile != null || this.master != null;}
29        }

30        
31        protected override void OnInit(EventArgs e)
32        {
33            if(HasMaster)
34            {
35                //加载母版并初始化母版
36                master = (MasterPage)LoadControl(MasterPageFile);
37                master.Initialize(this);
38            }

39            base.OnInit (e);
40        }

41
42        protected override void Render(HtmlTextWriter writer)
43        {
44            //有母版的时候,展现母版的内容
45            if(HasMaster && master != null)
46            {
47                master.RenderControl(writer);
48            }

49            else
50            {
51                base.Render (writer);
52            }

53        }

54    }

55}

56

Page类重写了OnInit方法,并在OnInit时,调用MasterPage类的Initialize方法初始化母版。另外,它重写了Render方法,当母版存在的时候,展现母版的内容。

MasterPage.cs:

 1using System;
 2using System.Web.UI;
 3using ZSoft.Web.UI.WebControls;
 4
 5namespace ZSoft.Web.UI
 6{
 7    /// <summary>
 8    /// 母版(for asp.net 1.1 only)
 9    /// </summary>

10    public class MasterPage : UserControl
11    {
12        /// <summary>
13        /// 初始化母版
14        /// </summary>
15        /// <param name="childPage"></param>

16        public void Initialize(Page childPage)
17        {            
18            this.ID = "MasterPage";
19            for(int i = 0; i < childPage.Controls.Count; i++)
20            {
21                if(childPage.Controls[i] is Content)
22                {
23                    Content content = childPage.Controls[i] as Content;
24
25                    ContentPlaceHolder holder = (ContentPlaceHolder)this.FindControl(content.ContentPlaceHolderID);
26
27                    if(holder == null)
28                    {
29                        throw new ArgumentException("在母版页中未找到Content PlaceHolder " + content.ContentPlaceHolderID);
30                    }

31
32                    holder.Content = content;
33                }

34            }

35
36            childPage.Controls.AddAt(0,this);
37        }

38    }

39}

40

 

在母版的初始化方法里,它遍历了子页面的第一层控件来寻找Content控件,然后根据Content控件实例的ContentPlaceHolderID属性,从自身找到相对应的ContentPlaceHolder控件,然后把Content控件的实例赋值给ContentPlaceHolder控件,从而达到初始化的目的,最后,母版把自己做为一个控件,加到子控件里(childPage.Controls.AddAt(0,this),这句话非常重要,少了这句会带来PostBack时的异常。
    注意,上面的初始化方法,只是遍历了子页面的第一层控件来寻找Content控件,这就要求我们的子页面(即使用母版的页面)的Content控件不能放在runat=serverForm内了,因为如果控件位于runat=serverform内,页面的第一层控件里就遍历不到Content控件了,因为他们属于HtmlForm控件的子控件。当然,如果您非要在子控件的Content控件外层放置一个runat=serverform的话,那就要修改一下上面的这段代码了。
    到这里为止,这个MasterPage的功能就被我们实现了,代码比较简单,下面简单介绍一下如何使用。
4 如何使用

    它的使用方法和Asp.net 2.0下的MasterPage使用方法一样。
    首先我们定义一个母版页,后台代码继承与上面定义的基类MasterPage:

 1<%@ Control Language="c#" AutoEventWireup="false" Codebehind="Master.ascx.cs" Inherits="TaskManager.TestMaster.Master"
 2    TargetSchema="http://schemas.microsoft.com/intellisense/ie5" 
%>
 3<%@ Register Assembly="ZSoft.Web" TagPrefix="zsoft" Namespace="ZSoft.Web.UI.WebControls" %>
 4<html>
 5<head>
 6    <title>
 7        <zsoft:ContentPlaceHolder ID="Title" runat="server" />
 8    </title>
 9</head>
10<body>
11    <form id="Form1" runat="server">
12        <table width="100%" border="1">
13            <tr>
14                <td>
15                    <h1>
16                        <zsoft:ContentPlaceHolder ID="Head" runat="server">
17                            Default Head
18                        </zsoft:ContentPlaceHolder>
19                    </h1>
20                </td>
21            </tr>
22            <tr>
23                <td>
24                    <zsoft:ContentPlaceHolder ID="Content" runat="server" />
25                </td>
26            </tr>
27            <tr>
28                <td align="center">
29                    the footer
30                </td>
31            </tr>
32        </table>
33    </form>
34</body>
35</html>
36

 

    在这个母版里,定义了三个ContentPlaceHolder,分别表示页面的Title,Head和Content。

    然后定义一个使用该模板的子页面(后台代码继承与上面定义的基类Page):

 1<%@ Page Language="c#" Codebehind="WebForm1.aspx.cs" AutoEventWireup="false" Inherits="TaskManager.TestMaster.WebForm1" %>
 2
 3<%@ Register Assembly="ZSoft.Web" TagPrefix="zsoft" Namespace="ZSoft.Web.UI.WebControls" %>
 4<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
 5<html>
 6<head>
 7    <title>WebForm1</title>
 8    <meta name="GENERATOR" content="Microsoft Visual Studio .NET 7.1">
 9    <meta name="CODE_LANGUAGE" content="C#">
10    <meta name="vs_defaultClientScript" content="JavaScript">
11    <meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
12</head>
13<body ms_positioning="GridLayout">
14    <zsoft:Content ID="Content1" ContentPlaceHolderID="Title" runat="server">
15        Hello
16    </zsoft:Content>
17    <zsoft:Content ID="Content2" ContentPlaceHolderID="Head" runat="server">
18        Asp.net 1.1中的母版页
19    </zsoft:Content>
20    <zsoft:Content ID="Content3" ContentPlaceHolderID="Content" runat="server">
21        <asp:TextBox ID="TxtContent" runat="server" zvalidate="notnull(sdfsdf)" zbind="Content"></asp:TextBox>
22        <asp:Button ID="BtnSave" runat="server" OnClick="SaveClick" Text="submit" zCausesValidation="true" />
23    </zsoft:Content>
24</body>
25</html>
26

    这个页面定义了三个Content控件,分别对应与母版的三个ContentPlaceHolder控件。在该页面的后台代码类的OnInit方法里,加入this.MasterPageFile = "Master.ascx";用以指定母版文件,如下:

 1        override protected void OnInit(EventArgs e)
 2        {
 3            this.MasterPageFile = "Master.ascx";
 4
 5            //
 6            // CODEGEN: 该调用是 ASP.NET Web 窗体设计器所必需的。
 7            //
 8            InitializeComponent();
 9            base.OnInit(e);
10        }

11        
 

关于这个MasterPageFile属性值的指定,这里是在OnInit方法里硬编码赋值的,您也可以通过额外的方式(如配置文件,Spring的依赖注入等)来实现以提高灵活性,当然这些不属于我们讨论的内容。

我还尝试着扩展Page指令,使MasterPageFile属性可以像Asp.net 2.0那样,通过Page指令来设置,如:

1<%@ Page Language="c#" MasterPageFile="Master.ascx" Inherits="TaskManager.TestMaster.WebForm1" %>

    但不幸的是,.net 1.1并不能像2.0那样可以在Page指令里指定Page中属性的值,最终放弃了这个想法。如果哪位朋友知道如何扩展.net 1.1下的Page指令,希望能告诉我,不胜感激。
    当然,如何能像VS 2005的窗体设计器那样支持MasterPage,有兴趣的朋友可以做更深一步的研究。

posted on 2008-04-16 15:25  向心力  阅读(2361)  评论(12编辑  收藏  举报