MasterPage是Asp.net2.0引入的一个非常实用的特性,怎么用,我想不用我说,基本上大家都会,这里要讲的是,它是如何实现的。
在深入源代码去探索MasterPage之前,我以为MasterPage的实现应该是比较复杂的,也一直纳闷为什么MasterPage类会继承于UserControl类,感觉这两者好像差得很远。昨天晚上,我专门抽出时间,阅读了部分与MasterPage有关的源代码,终于明白了是怎么回事,在那突然明白的那一刻,真有如醍醐灌顶,拍案叫绝,不得不佩服微软的那些guys。
下面就是我的探索之旅的过程(大家也可以跳过该部分,直接看后面的真相大白部分):
1、我首先查看的是Page.ProcessRequestMain方法,我们知道,Page类大部分特性,包括LifeCycle、PostBack、ProcessPostData等都是在该方法中实现,所以,我想,MasterPage的实现肯定在该方法中有不少的体现。然而,令我惊讶的是,我居然没有在该方法中找到任何有关MasterPage的线索,而仅仅在this.PerformPreInit()中,找到唯一一个ApplyMasterPage()方法。而该方法也出奇的简单,感觉仅仅是将递归的各级MasterPage的._masterPageApplied字段设为true而已。当时,我忽略了一个重要的东西,就是代码中对this.Master这个属性的访问,实际上,奥秘就在对这个属性的访问上(下文将叙述)。
private void PerformPreInit()
{
this.OnPreInit(EventArgs.Empty);
this.InitializeThemes();
this.ApplyMasterPage();
this._preInitWorkComplete = true;
}
Page.ApplyMasterPage()
private void ApplyMasterPage()
{
if (this.Master != null)
{
ArrayList appliedMasterFilePaths = new ArrayList();
appliedMasterFilePaths.Add(this._masterPageFile.VirtualPathString.ToLower(CultureInfo.InvariantCulture));
MasterPage.ApplyMasterRecursive(this.Master, appliedMasterFilePaths);
}
}
2、我查看了MasterPage的源代码,出奇的是,竟也如此简单,以至于我也没有从该源代码中找到多少有价值的信息。
MasterPage
[ControlBuilder(typeof(MasterPageControlBuilder)), Designer("Microsoft.VisualStudio.Web.WebForms.MasterPageWebFormDesigner, Microsoft.VisualStudio.Web, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(IRootDesigner)), ParseChildren(false), AspNetHostingPermission(SecurityAction.LinkDemand, Level=AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal)]
public class MasterPage : UserControl
{
private IList _contentPlaceHolders;
private IDictionary _contentTemplateCollection;
private IDictionary _contentTemplates;
private MasterPage _master;
private bool _masterPageApplied;
private VirtualPath _masterPageFile;
internal TemplateControl _ownerControl;
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected internal void AddContentTemplate(string templateName, ITemplate template)
{
if (this._contentTemplateCollection == null)
{
this._contentTemplateCollection = new Hashtable(10, StringComparer.OrdinalIgnoreCase);
}
try
{
this._contentTemplateCollection.Add(templateName, template);
}
catch (ArgumentException)
{
throw new HttpException(SR.GetString("MasterPage_Multiple_content", new object[] { templateName }));
}
}
internal static void ApplyMasterRecursive(MasterPage master, IList appliedMasterFilePaths)
{
if (master.Master != null)
{
string str = master._masterPageFile.VirtualPathString.ToLower(CultureInfo.InvariantCulture);
if (appliedMasterFilePaths.Contains(str))
{
throw new InvalidOperationException(SR.GetString("MasterPage_Circular_Master_Not_Allowed", new object[] { master._masterPageFile }));
}
appliedMasterFilePaths.Add(str);
ApplyMasterRecursive(master.Master, appliedMasterFilePaths);
}
master._masterPageApplied = true;
}
internal static MasterPage CreateMaster(TemplateControl owner, HttpContext context, VirtualPath masterPageFile, IDictionary contentTemplateCollection)
{
MasterPage child = null;
if (masterPageFile == null)
{
if ((contentTemplateCollection != null) && (contentTemplateCollection.Count > 0))
{
throw new HttpException(SR.GetString("Content_only_allowed_in_content_page"));
}
return null;
}
VirtualPath virtualPath = VirtualPathProvider.CombineVirtualPathsInternal(owner.TemplateControlVirtualPath, masterPageFile);
ITypedWebObjectFactory vPathBuildResult = (ITypedWebObjectFactory) BuildManager.GetVPathBuildResult(context, virtualPath);
if (!typeof(MasterPage).IsAssignableFrom(vPathBuildResult.InstantiatedType))
{
throw new HttpException(SR.GetString("Invalid_master_base", new object[] { masterPageFile }));
}
child = (MasterPage) vPathBuildResult.CreateInstance();
child.TemplateControlVirtualPath = virtualPath;
if (owner.HasControls())
{
foreach (Control control in owner.Controls)
{
LiteralControl control2 = control as LiteralControl;
if ((control2 == null) || (Util.FirstNonWhiteSpaceIndex(control2.Text) >= 0))
{
throw new HttpException(SR.GetString("Content_allowed_in_top_level_only"));
}
}
owner.Controls.Clear();
}
if (owner.Controls.IsReadOnly)
{
throw new HttpException(SR.GetString("MasterPage_Cannot_ApplyTo_ReadOnly_Collection"));
}
if (contentTemplateCollection != null)
{
foreach (string str in contentTemplateCollection.Keys)
{
if (!child.ContentPlaceHolders.Contains(str.ToLower(CultureInfo.InvariantCulture)))
{
throw new HttpException(SR.GetString("MasterPage_doesnt_have_contentplaceholder", new object[] { str, masterPageFile }));
}
}
child._contentTemplates = contentTemplateCollection;
}
child._ownerControl = owner;
child.InitializeAsUserControl(owner.Page);
owner.Controls.Add(child);
return child;
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)]
protected internal IList ContentPlaceHolders
{
get
{
if (this._contentPlaceHolders == null)
{
this._contentPlaceHolders = new ArrayList();
}
return this._contentPlaceHolders;
}
}
[Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)]
protected internal IDictionary ContentTemplates
{
get
{
return this._contentTemplates;
}
}
[WebSysDescription("MasterPage_MasterPage"), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), Browsable(false)]
public MasterPage Master
{
get
{
if ((this._master == null) && !this._masterPageApplied)
{
this._master = CreateMaster(this, this.Context, this._masterPageFile, this._contentTemplateCollection);
}
return this._master;
}
}
[WebSysDescription("MasterPage_MasterPageFile"), WebCategory("Behavior"), DefaultValue("")]
public string MasterPageFile
{
get
{
return VirtualPath.GetVirtualPathString(this._masterPageFile);
}
set
{
if (this._masterPageApplied)
{
throw new InvalidOperationException(SR.GetString("PropertySetBeforePageEvent", new object[] { "MasterPageFile", "Page_PreInit" }));
}
if (value != VirtualPath.GetVirtualPathString(this._masterPageFile))
{
this._masterPageFile = VirtualPath.CreateAllowNull(value);
if ((this._master != null) && this.Controls.Contains(this._master))
{
this.Controls.Remove(this._master);
}
this._master = null;
}
}
}
}
3、我又查看了ContentPlaceHolder类、Content类,心想,难道奥秘在这两个控件上?打开源代码一看,彻底晕倒,这两个类简单得简直不能让人相信,ContentPlaceHolder居然是一个空类,仅仅起到一个标识的作用,而Content居然也仅仅只有ContentPlaceHolderID唯一一个string属性。
public class ContentPlaceHolder : Control, INonBindingContainer, INamingContainer
{
}
Content
public class Content : Control, INonBindingContainer, INamingContainer
{
private string _contentPlaceHolderID;
public string ContentPlaceHolderID
{
get
{
if (this._contentPlaceHolderID == null)
{
return string.Empty;
}
return this._contentPlaceHolderID;
}
set
{
if (!base.DesignMode)
{
throw new NotSupportedException(SR.GetString("Property_Set_Not_Supported", new object[] { "ContentPlaceHolderID", base.GetType().ToString() }));
}
this._contentPlaceHolderID = value;
}
}
}
4、此时,我几乎已经没法再从Asp.net源代码中找到其他有关MasterPage的有价值的信息了。于是,我决定写一个简单的Web应用程序,该应用程序仅仅只有一个MasterPage页与Default页,并将其编译,查看编译后的代码,看看是否能找到有价值的信息。
源代码如下:
MasterPage.master
<%@ Master Language="C#" AutoEventWireup="true" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:ContentPlaceHolder id="ContentPlaceHolder" runat="server">
<asp:Button runat="server" ID="master" Text="master"/>
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
Default.aspx
<%@ Page Title="" Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true" %>
<%@ MasterType VirtualPath="~/MasterPage.master" %>
<asp:Content ID="Content" ContentPlaceHolderID="ContentPlaceHolder" Runat="Server">
<asp:Button runat="server" ID="default" Text="default" />
</asp:Content>
编译后代码如下:
Code
[CompilerGlobalScope]
public class masterpage_master : MasterPage
{
private static bool __initialized;
private ITemplate __Template_ContentPlaceHolder;
protected ContentPlaceHolder ContentPlaceHolder;
protected HtmlForm form1;
protected Button master;
[DebuggerNonUserCode]
public masterpage_master()
{
base.AppRelativeVirtualPath = "~/MasterPage.master";
if (!__initialized)
{
__initialized = true;
}
base.ContentPlaceHolders.Add("contentplaceholder");
}
[DebuggerNonUserCode]
private ContentPlaceHolder __BuildControlContentPlaceHolder()
{
ContentPlaceHolder container = new ContentPlaceHolder();
this.ContentPlaceHolder = container;
container.ID = "ContentPlaceHolder";
if (base.ContentTemplates != null)
{
this.__Template_ContentPlaceHolder = (ITemplate) base.ContentTemplates["ContentPlaceHolder"];
}
if (this.__Template_ContentPlaceHolder != null)
{
this.__Template_ContentPlaceHolder.InstantiateIn(container);
return container;
}
IParserAccessor accessor = container;
accessor.AddParsedSubObject(new LiteralControl("\r\n "));
Button button = this.__BuildControlmaster();
accessor.AddParsedSubObject(button);
accessor.AddParsedSubObject(new LiteralControl("\r\n "));
return container;
}
[DebuggerNonUserCode]
private HtmlForm __BuildControlform1()
{
HtmlForm form = new HtmlForm();
this.form1 = form;
form.ID = "form1";
IParserAccessor accessor = form;
accessor.AddParsedSubObject(new LiteralControl("\r\n <div>\r\n "));
ContentPlaceHolder holder = this.__BuildControlContentPlaceHolder();
accessor.AddParsedSubObject(holder);
accessor.AddParsedSubObject(new LiteralControl("\r\n </div>\r\n "));
return form;
}
[DebuggerNonUserCode]
private Button __BuildControlmaster()
{
Button button = new Button();
this.master = button;
button.ApplyStyleSheetSkin(this.Page);
button.ID = "master";
button.Text = "master";
return button;
}
[DebuggerNonUserCode]
private void __BuildControlTree(masterpage_master __ctrl)
{
IParserAccessor accessor = __ctrl;
accessor.AddParsedSubObject(new LiteralControl("\r\n\r\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n <title></title>\r\n</head>\r\n<body>\r\n "));
HtmlForm form = this.__BuildControlform1();
accessor.AddParsedSubObject(form);
accessor.AddParsedSubObject(new LiteralControl("\r\n</body>\r\n</html>\r\n"));
}
[DebuggerNonUserCode]
protected override void FrameworkInitialize()
{
base.FrameworkInitialize();
this.__BuildControlTree(this);
}
protected HttpApplication ApplicationInstance
{
get
{
return this.Context.ApplicationInstance;
}
}
protected DefaultProfile Profile
{
get
{
return (DefaultProfile) this.Context.Profile;
}
}
protected override bool SupportAutoEvents
{
get
{
return false;
}
}
[TemplateContainer(typeof(MasterPage)), TemplateInstance(TemplateInstance.Single)]
public virtual ITemplate Template_ContentPlaceHolder
{
get
{
return this.__Template_ContentPlaceHolder;
}
set
{
this.__Template_ContentPlaceHolder = value;
}
}
}
Code
[CompilerGlobalScope]
public class default_aspx : Page, IRequiresSessionState, IHttpHandler
{
private static object __fileDependencies;
private static bool __initialized;
protected Button @default;
[DebuggerNonUserCode]
public default_aspx()
{
base.AppRelativeVirtualPath = "~/Default.aspx";
if (!__initialized)
{
string[] virtualFileDependencies = new string[] { "~/Default.aspx", "~/MasterPage.master" };
__fileDependencies = base.GetWrappedFileDependencies(virtualFileDependencies);
__initialized = true;
}
base.Server.ScriptTimeout = 0x1c9c380;
}
[DebuggerNonUserCode]
private void __BuildControlContent(Control __ctrl)
{
IParserAccessor accessor = __ctrl;
accessor.AddParsedSubObject(new LiteralControl("\r\n "));
Button button = this.__BuildControldefault();
accessor.AddParsedSubObject(button);
accessor.AddParsedSubObject(new LiteralControl("\r\n"));
}
[DebuggerNonUserCode]
private Button __BuildControldefault()
{
Button button = new Button();
this.@default = button;
button.TemplateControl = this;
button.ApplyStyleSheetSkin(this);
button.ID = "default";
button.Text = "default";
return button;
}
[DebuggerNonUserCode]
private void __BuildControlTree(default_aspx __ctrl)
{
__ctrl.Title = "";
__ctrl.MasterPageFile = "~/MasterPage.master";
this.InitializeCulture();
base.AddContentTemplate("ContentPlaceHolder", new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControlContent)));
IParserAccessor accessor = __ctrl;
accessor.AddParsedSubObject(new LiteralControl("\r\n\r\n"));
}
[DebuggerNonUserCode]
protected override void FrameworkInitialize()
{
base.FrameworkInitialize();
this.__BuildControlTree(this);
base.AddWrappedFileDependencies(__fileDependencies);
base.Request.ValidateInput();
}
[DebuggerNonUserCode]
public override int GetTypeHashCode()
{
return -2002306427;
}
[DebuggerNonUserCode]
public override void ProcessRequest(HttpContext context)
{
base.ProcessRequest(context);
}
protected HttpApplication ApplicationInstance
{
get
{
return this.Context.ApplicationInstance;
}
}
public masterpage_master Master
{
get
{
return (masterpage_master) base.Master;
}
}
protected DefaultProfile Profile
{
get
{
return (DefaultProfile) this.Context.Profile;
}
}
protected override bool SupportAutoEvents
{
get
{
return false;
}
}
}
我们首先观察default_aspx的BuildControlTree方法,该方法调用了base.AddContentTemplate("ContentPlaceHolder", new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControlContent)));而该方法实质上只有一行代码,即this._contentTemplateCollection.Add(templateName, template);其中,_contentTemplateCollection是IDictionary类型。因此,base.AddContentTemplate只有一个功能,即在Page的_contentTemplateCollection中添加一个CompliedTemplateBuilder类型的对象。那么该对象有啥作用呢?为了不岔开话题,这里不对该对象做详细描述,只给出结论:该对象实现ITemplate接口,其接口方法InstantiateIn(Control container)具体实现为为container添加BuildTemplateMethod委托所创建的控件。这话有点拗口,简单地就刚才那个例子来说,就是如果你调用该对象的InstantiateIn(Control container)方法,就为该container添加this.__BuildControlContent()方法所创建的控件做为子控件。
5、我们继续来看看masterpage_master的__BuildControlContentPlaceHolder()方法,我们发现,该方法即调用了刚才讨论的那个InstantiateIn方法。哈,原来在这里,终于明白了,原来Page里Content控件中的所有内容最终都将变成其对应的MasterPage中的ContentPlaceHolder的子控件。等一下,这个结论下得有点早,难道这里的base.ContentTemplates属性就等于Page的_contentTemplateCollection字段吗?如果是,那么上面的结论就是正确的。
6、我们重新回到1中的代码,查看Page.Master属性的实现,我们发现,它调用了MasterPage的CreateMaster静态方法,而该方法传送的其中一个参数就是._contentTemplateCollection字段。
Page.Master
public MasterPage Master
{
get
{
if ((this._master == null) && !this._preInitWorkComplete)
{
this._master = MasterPage.CreateMaster(this, this.Context, this._masterPageFile, this._contentTemplateCollection);
}
return this._master;
}
}
我们再回头来看看MasterPage.CreateMaster方法,在倒数第6行,我们发现,该方法果然将contentTemplateCollection赋给了MasterPage实例(child)的_contentTemplates字段。再往下看,我们还看到了owner.Controls.Add(child),什么意思呢?意思就是将该MasterPage实例作为普通控件加入到owner实例的子控件集合中。而往上,我们又可以找到owner.Controls.Clear()语句,因此,该MasterPage将作为owner的唯一子控件而存在。而该owner,常常就是Page实例。(在存在嵌套MasterPage的时候,该owner还可能是下一层次的MasterPage实例)。
至此,真相大白,原来,MasterPage最终将作为Page的唯一子控件而存在,难怪它要继承自 UserControl,而Page中Content控件定义的各个子控件,又将作为该MasterPage的ContentPlaceHolder的子控件而存在,难怪ContentPlaceHolder无需实现任何代码,因为它仅仅是一个容器。正因为MasterPage最终成为Page的唯一子控件,那么后来的处理就与普通的控件没什么两样了,难怪ProcessRequestMain方法里无需为MasterPage单独编码,哈哈,一切都真相大白了。这里,我们还发现一个比较有趣的现象,即Content控件本身却消失不见了,这应该是Asp.net解析器所做的优化,因为ContentPlaceHolder完全没必要先装上Content控件,然后再装上Content中的那些控件。
另外,从Page.Master属性与Page.MasterPageFile属性的实现上,我们也不难明白为什么MasterPageFile属性只能在 PreInit 事件中进行设置的原因。
如何证明以上所说都是正确的呢?呵呵,其实很简单,我们可以观察最终页面的控件树,就可证明上面分析是正确的。(写这篇blog时才想起看控件树,要是早想起,就能让我少走不少歪路了,唉,幸好打开控件树发现结果与预期完全一致。)
那么嵌套MasterPage是如何实现的呢?呵呵,其实也一样,即Top1MasterPage成为Top2MasterPage的唯一子控件,Top2MasterPage成为Top3MasterPage的唯一子控件,……,直到TopNMasterPage成为Page的唯一子控件。
最后我用两幅图来做总结。
下图为初始的控件树结构:
下图为最终的控件树结构: