在Asp.net 1.1下实现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呢?
前面说了,ContentPlaceHolder是内容的容器,Content控件是具体的内容,他们的代码如下:
Content.cs:
using System.Web.UI;2
using System.Web.UI.WebControls;3
namespace ZSoft.Web.UI.WebControls4
{5
/// <summary>6
/// 内容控件7
/// </summary>8
[PersistChildren(true)]9
[ParseChildren(false)]10
public class Content : Control11
{12
private string contentPlaceHolderID;13

14
public string ContentPlaceHolderID15
{16
get { return contentPlaceHolderID;}17
set { contentPlaceHolderID = value;}18
}19
}20
}21

可以看到,Content控件包含一个属性ContentPlaceHolderID,用以指向母版页的ContentPlaceHolder控件。
ContentPlaceHolder.cs:
using System.Web.UI;2
using System.Web.UI.WebControls;3

4
namespace ZSoft.Web.UI.WebControls5
{6
/// <summary>7
/// 内容容器8
/// </summary>9
[PersistChildren(true)]10
[ParseChildren(false)]11
public class ContentPlaceHolder : WebControl12
{13
private Content content;14

15
public Content Content16
{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
else29
{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里去初始化ContentPlaceHolder的Content属性。在页面初始化时,根据页面的MasterPageFile属性,加载MasterPage控件,然后初始化该控件里的ContentPlaceHolder。这样,我们就需要另外两个类,MasterPage基类和Page基类,分别对应母版控件和使用母版的页面。
3 Page和MasterPage
Page.cs:
using System;2
using System.Web.UI;3

4
namespace ZSoft.Web.UI5
{6
/// <summary>7
/// 页面基类8
/// </summary>9
public class Page : System.Web.UI.Page10
{11
private string masterPageFile;12
private MasterPage master;13
14
/// <summary>15
/// 母版的路径16
/// </summary>17
public string MasterPageFile18
{19
get { return masterPageFile;}20
set { masterPageFile = value;}21
}22

23
/// <summary>24
/// 是否有母版25
/// </summary>26
public bool HasMaster27
{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
else50
{51
base.Render (writer);52
}53
}54
}55
}56

Page类重写了OnInit方法,并在OnInit时,调用MasterPage类的Initialize方法初始化母版。另外,它重写了Render方法,当母版存在的时候,展现母版的内容。
MasterPage.cs:
using System;2
using System.Web.UI;3
using ZSoft.Web.UI.WebControls;4

5
namespace ZSoft.Web.UI6
{7
/// <summary>8
/// 母版(for asp.net 1.1 only)9
/// </summary>10
public class MasterPage : UserControl11
{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=server的Form内了,因为如果控件位于runat=server的form内,页面的第一层控件里就遍历不到Content控件了,因为他们属于HtmlForm控件的子控件。当然,如果您非要在子控件的Content控件外层放置一个runat=server的form的话,那就要修改一下上面的这段代码了。
到这里为止,这个MasterPage的功能就被我们实现了,代码比较简单,下面简单介绍一下如何使用。
4 如何使用
它的使用方法和Asp.net 2.0下的MasterPage使用方法一样。
首先我们定义一个母版页,后台代码继承与上面定义的基类MasterPage:
<%@ 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 Head18
</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 footer30
</td>31
</tr>32
</table>33
</form>34
</body>35
</html>36

在这个母版里,定义了三个ContentPlaceHolder,分别表示页面的Title,Head和Content。
然后定义一个使用该模板的子页面(后台代码继承与上面定义的基类Page):
<%@ 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
Hello16
</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";用以指定母版文件,如下:
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指令来设置,如:
<%@ Page Language="c#" MasterPageFile="Master.ascx" Inherits="TaskManager.TestMaster.WebForm1" %> 但不幸的是,.net 1.1并不能像2.0那样可以在Page指令里指定Page中属性的值,最终放弃了这个想法。如果哪位朋友知道如何扩展.net 1.1下的Page指令,希望能告诉我,不胜感激。
当然,如何能像VS 2005的窗体设计器那样支持MasterPage,有兴趣的朋友可以做更深一步的研究。


浙公网安备 33010602011771号