天高地厚

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

【转载】ASP.NET Page 那点事

Posted on 2013-05-25 09:06  天高地厚-GNU  阅读(147)  评论(0编辑  收藏  举报

Page,我想每个ASP.NET开发人员对它应该都是比较熟悉的。
这次的博客我就打算专门谈谈它。不过呢,我不打算说 在Page中使用控件的一些话题,也不会说Page的生命周期的相关话题,因为我认为这些话题被人谈论的次数实在是太多了,尤其是市面上的ASP.NET的书籍,都会比较喜欢这些话题。

我不喜欢重复,因此今天我只想谈些人家不谈的那点事,但我认为它们仍然很重要。

一些重要的Page指令

虽然Page公开了很多属性,让我们可以在运行时调整它的状态与行为,但是,还有些重要的参数却是以“指令”方式提供的,需要在设计时就指定。
下面是我整理的一些我认为 比较重要并且经常需要使用 的指令:

@ Page 指令
Async 指示页面要不要以异步页的方式运行。默认值为 false。
注意:如果需要开发一个异步页,必须要设置这个指令属性,以便在编译页面时实现IHttpAsyncHandler接口。
AsyncTimeOut 定义在处理异步任务时使用的超时时间间隔(以秒为单位)。默认值为 45 秒。 该值必须是整数。
此属性仅对Page.RegisterAsyncTask()方法有效。
AutoEventWireup 指示页的事件是否自动绑定。如果启用了事件自动绑定,则为 true;否则为 false。默认值为 true。
所谓的事件自动绑定就是识别Page_Load这些类型的页面事件处理程序。
事件自动绑定发生在页面第一次请求时,在后续请求中会使用缓存的委托调用列表,因此对性能的轻微影响也仅发生在第一次请求时。
EnableSessionState 定义页的会话状态要求。如果启用了会话状态,则为 true;如果可以读取会话状态但不能进行更改,则为 ReadOnly;否则为 false。默认值为 true。
建议不要保留默认值:
1. 如果只是部分页面需要使用Session,那么不需要使用Session的页面请显式设置为关闭状态,
2. 对于不需要修改Session的页面请设置为ReadOnly。
页面编译时就是根据这个指令来决定生成对IRequiresSessionState,IReadOnlySessionState接口的实现关系。
EnableViewState 指示是否在页请求之间保持视图状态。如果要保持视图状态,则为 true;否则为 false。默认值为 true。
强烈建议不要使用视图状态,因为它解决的问题比引用的问题更多。
MasterPageFile 设置内容页的母版页或嵌套母版页的路径。支持相对路径和绝对路径。
可以在页面的PreInit事件中动态设置同名的属性实现动态切换母版页的功能。
Trace 指示是否启用跟踪。如果启用了跟踪,则为 true;否则为 false。默认值为 false。
启用跟踪对页面调试非常有用,我们可以调用Page.Trace对象的方法输出一些有价值的诊断信息。
Page.Trace.Write()采用默认字体输出,Page.Trace.Warn()采用红色字体输出。
如果你总是记不住页面生命周期,启用跟踪后,一切就摆在你眼前。
ValidateRequest 指示是否应发生请求验证。如果为true,请求验证将根据具有潜在危险的值的硬编码列表检查所有输入数据。默认值为 true。
如果希望用户输入HTML代码,请设置为false 。
注意:在ASP.NET 2.0中不验证ashx请求,但是ASP.NET 4.0默认会验证,
如果希望在4.0中兼容2.0的行为,请在web.config中配置<httpRuntime requestValidationMode="2.0"/>
其它指令
@ Import 导入允许您指定要在代码中引用的命名空间。
@ OutputCache 设置页面或者用户控件的输出缓存。
@ Register 声明控件的标记前缀和控件程序集的位置。如果要向页面添加用户控件或自定义 ASP.NET 控件,可以使用此指令。

web.config的全局设置

前面我介绍了一些常用的Page指令,考虑到方便性,ASP.NET还允许我们在web.config中为一些常用的指令配置默认值。下面我就一些常用的场景来说明这些全局配置的方便性。

1. 通常,我在创建一个网站项目时,肯定会决定不使用ViewState和Session的。那么如果为每个页面设置EnableViewState,EnableSessionState指令属性,那就显得太麻烦了,而且还容易遗漏。此时,我们可以直接在web.config中为这些参数指定一个全局的默认值:

<pages enableViewState="false" enableSessionState="false"></pages>

补充说明一下:全局禁用Session的彻底方法是把Session对应的HttpModule从httpModules列表中移除。

web.config允许我们设置Page默认参数的具体配置节如下:

<pages     
   buffer="[True|False]"
   enableEventValidation="[True|False]"
   enableSessionState="[True|False|ReadOnly]"
   enableViewState="[True|False]"
   enableViewStateMac="[True|False]"
   smartNavigation="[True|False]"
   autoEventWireup="[True|False]"
   pageBaseType="typename, assembly"
   userControlBaseType="typename"
   validateRequest="[True|False]"
   masterPageFile="file path" 
   theme="string"
   styleSheetTheme="string"
   maxPageStateFieldLength="number" 
   compilationMode="[Always|Auto|Never]" 
   pageParserFilterType="string" 
   viewStateEncryptionMode="[Always|Auto|Never]" 
   maintainScrollPositionOnPostBack="[True|False]" 
   asyncTimeout="number"
>
   <controls>...</controls>
   <namespaces>...</namespaces>
   <tagMapping>...</tagMapping>
</pages>

2. 为了代码重用,设计用户控件也是很常用的方法。
我们可以使用 @ Register指令 在页面注册需要使用的UserControl或者WebControl。然而,有些控件比较通用,许多页面都会使用它,那么就不要再使用 @ Register指令了,可以在web.config中统一注册。例如:

<pages>
    <controls>
        <add tagPrefix="fish" tagName="MainMenu" src="~/Controls/MainMenu.ascx" />
        <add tagPrefix="fish" tagName="PageHeader" src="~/Controls/PageHeader.ascx" />
    </controls>
</pages>

有了这个定义后,我就可以在任何页面中直接使用:

<fish:PageHeader runat="server" ID="PageHeader1" />

3. 对于喜欢使用页面内联代码的人来说,可能经常需要使用自己定义的类型。如果这些类型定义在某个命名空间中,那么就需要在内联代码中采用完整命名空间的写法。虽然这样做没有什么问题,但就是麻烦,于是,我们可以在页面中使用 @ Import指令 来导入我们需要使用的命名空间,但是这个指令每次只能导入一个命名空间,而且每个页面还得重复导入,显然不够方便。

为了方便使用一些常用的命名空间,我们可以在web.config中统一指定,例如:

<pages>
    <namespaces>
        <add namespace="MyMVC" />
        <add namespace="WebSiteCommonLib" />
        <add namespace="WebSiteModel" />
    </namespaces>
</pages>

这样设置后,所有页面就可以直接使用这些命名空间下的类型了。

不知道有些人想过:为什么在页面中使用某些微软提供的类型就不需要导入命名空间?
答案是:其实ASP.NET已经将一些微软认为常用的命名空间在web.config中配置好了:

<pages>
    <namespaces>
        <add namespace="System"/>
        <add namespace="System.Collections"/>
        <add namespace="System.Collections.Specialized"/>
        <add namespace="System.Configuration"/>
        <add namespace="System.Text"/>
        <add namespace="System.Text.RegularExpressions"/>
        <add namespace="System.Web"/>
        <add namespace="System.Web.Caching"/>
        <add namespace="System.Web.SessionState"/>
        <add namespace="System.Web.Security"/>
        <add namespace="System.Web.Profile"/>
        <add namespace="System.Web.UI"/>
        <add namespace="System.Web.UI.WebControls"/>
        <add namespace="System.Web.UI.WebControls.WebParts"/>
        <add namespace="System.Web.UI.HtmlControls"/>
    </namespaces>
</pages>

4. 现在,有越来越多的人为了方便而使用扩展方法。使用扩展方法的好处是:可以让我们不去关心这些扩展方法定义在那个类中,只要在支持扩展方法的对象上调用就可以了,就像下面的代码这样:

当前用户已登录,登录名:<%= Context.User.Identity.Name.HtmlEncode() %>

然而,在页面中使用扩展方法时,也必须先导入扩展方法的定义类的命名空间。
因此,为了方便,我们可以在web.config中为我们定义的扩展方法导入相应的命名空间:

<pages>
    <namespaces>
        <add namespace="FishDemoCodeLib" />
    </namespaces>
</pages>

换个方式使用 Page

在传统的WEB开发模式下,我们通常会设计一些页面(Page)响应来自用户浏览器的请求,在这种模式下,Page会将最后生成的整页HTML代码直接发送给用户浏览器。然而,在某些时候,我们只需要生成一个HTML片段:
1. 在AJAX请求中,客户端为了局部刷新,只要求服务端返回一个HTML片段。
2. BigPipe方式下,为了能分块输出,每次也只需要输出一个HTML片段。

如果只是为了得到一段简单的HTML代码,可能有些人会选择采用代码来拼接,但是如果那段HTML还有些复杂呢?显然拼接方法肯定是不行的。

对于第一个问题,可能有人说:我可以创建一个页面,只放部分代码到页面上。
的确,这种方法可以勉强解决第一个问题,但是,很有可能那部分代码在整页输出时也会用到,怎么办?
做成UserControl,然后放在一个单独的页面中!
其实这种做法很无奈,因为那个容器页面的意义不大(仅仅是个容器),最后搞得项目中一大堆页面文件!
事实上,这种方法仅适用于使用简单服务端控件的场合,如果想使用一些高级的服务端控件,它根本就不行。

为了能实现前面说到的二个需求,我们就不能再按照传统的方式来使用Page了。因为我们希望能得到(返回)一段HTML。

有二种方法可以让我们继续使用页面模板代码的方式生成HTML代码:
1. Server.Execute()方法。
2. Page.RenderControl()方法。

下面这段代码来源于 MyMVC框架,这个方法可以根据指定的用户控件以及控件显示所需的数据得到控件的输出结果(一段HTML代码)。

/// <summary>
/// 用指定的用户控件以及视图数据呈现结果,最后返回生成的HTML代码。
/// 用户控件应从MyUserControlView<T>继承
/// </summary>
/// <param name="ucVirtualPath">用户控件的虚拟路径</param>
/// <param name="model">视图数据</param>
/// <returns>生成的HTML代码</returns>
public static string Render(string ucVirtualPath, object model)
{
    if( string.IsNullOrEmpty(ucVirtualPath) )
        throw new ArgumentNullException("ucVirtualPath");
    
    Page page = new Page();
    Control ctl = page.LoadControl(ucVirtualPath);
    if( ctl == null )
        throw new InvalidOperationException(
            string.Format("指定的用户控件 {0} 没有找到。", ucVirtualPath));

    if( model != null ) {
        MyBaseUserControl myctl = ctl as MyBaseUserControl;
        if( myctl != null )
            myctl.SetModel(model);
    }

    // 将用户控件放在Page容器中。
    page.Controls.Add(ctl);

    StringWriter output = new StringWriter();
    HtmlTextWriter write = new HtmlTextWriter(output, string.Empty);
    page.RenderControl(write);

    // 用下面的方法也可以的。
    //HttpContext.Current.Server.Execute(page, output, false);

    return output.ToString();
}

整段代码分为以下几个步骤(我已用空行分隔开了):
1. 检查参数。
2. 创建页面容器并加载用户控件。
3. 设置页面(视图)所需的显示数据。
4. 将用户控件添加到Page的Controls集合中。
5. 调用RenderControl或者Execute让Page输出HTML代码。
6. 返回结果。

这段代码很简单,唯独值得介绍的就是第5步,调用它们就可以得到控件输出的HTML代码。
RenderControl或者Execute的差别在于:
RenderControl不支持服务器控件,原因在于它利用了页面的一种独特编译方式,我已在以前的博客中分析过了。
Execute可以支持服务器控件,因为它会执行一次完整的页面生命周期。

注意:上面这段代码就算使用Execute,也只能支持部分简单的服务器控件,因为一些复杂的服务器控件需要在HtmlForm中才能运行。因此,如果需要支持所有的服务器控件,那么还必须创建HtmlForm对象,并调整包含关系,还有就是还需要去掉产生的多余HTML代码。

如果你需要生成整个页面生成的HTML代码,可以参考 MyMVC框架,那里有实现这个功能的完整代码。

重新认识Eval()方法

我想很多人都写过类似下面的代码:

<asp:Repeater ID="repeater1" runat="server">
    <HeaderTemplate><ul></HeaderTemplate>
    <FooterTemplate></ul></FooterTemplate>
    <ItemTemplate>
        <li><%# Eval("OrderID")%><%# Eval("OrderDate")%><%# Eval("SumMoney")%>
        </li>
    </ItemTemplate>
</asp:Repeater>

在这里我要说的是 Eval() 的调用,还不是Repeater控件。

Eval()不仅仅可以读取一个绑定数据项的属性,还可以去读取DataTable中的一个数据列。而且还能完成更复杂的绑定计算:

<li><%# Eval("OrderID")%><%# Eval("OrderDate")%><%# Eval("SumMoney")%>
    ,订单中的第一个商品:<%# Eval("Detail[0].ProductName") %>
</li>

当然了,对于页面上的数据绑定,用Eval()的确不是性能最好的方法,建议还是使用强类型转换的方法。

有时候,尤其是在写反射应用时,时常会有从字符串解析并实现求值计算的需求。那么,前面这个示例中,Eval()的功能是不是值得挖掘呢?我认为答案是肯定的。

通过分析ASP.NET的绑定代码,我发现Eval在内部会调用DataBinder.Eval这个静态方法,这个方法的签名如下:

//     在运行时计算数据绑定表达式。
//
// 参数:
//   container:
//     表达式根据其进行计算的对象引用。此标识符必须是以页的指定语言表示的有效对象标识符。
//
//   expression:
//     从 container 到要放置在绑定控件属性中的公共属性值的导航路径。
//     此路径必须是以点分隔的属性或字段名称字符串,如 C# 中的 "Tables[0].DefaultView.[0].Price"
//     或 Visual Basic 中的 "Tables(0).DefaultView.(0).Price"。
//
// 返回结果:
//     System.Object,它是数据绑定表达式的计算结果。
public static object Eval(object container, string expression);

通过这个签名的注释,我们可以很容易地看出它的用法。
下面我来举个例子把它应用在非绑定的应用中:

我有一个类:

public class TestEvalClass
{
    public List<Order> Orders { get; set; }

    // Order以及OrderDetail的定义就省略了,我想大家能想像得出来。
}

那么下面的代码是可以运行的:

static void Main()
{
    TestEvalClass testObject = GetTestEvalClassInstance();

    string productName = (string)System.Web.UI.DataBinder.Eval(testObject, "Orders[0].Detail[0].ProductName");
    Console.WriteLine(productName);
}

对于这个示例,我想输出什么结果,并不重要。
我只想说:如果让你去解析那个表达式,会不会比较麻烦,现在有现成的,用起来是不是很方便?

不用基类也能扩展

在一个ASP.NET网站中,如果想为所有的页面添加某个功能,我们通常会想到使用基类的方式去实现。这的确是一种很有效的方法,但不并唯一的方法,还有一种方法也能容易实现这个需求,那就是使用PageAdapter的方式。

在我写博客的过程中,我写了很多示例页面,页面中包含一些提交按钮是少不了的事情,然而,为了能让示例代码看起来比较原始(简单),我尽量不使用服务器控件,因此就要面临提交按钮的事件处理问题。在博客【细说 ASP.NET Cache 及其高级用法】的示例代码中,我开始采用PageAdapter这种方法,它可以让代码很简单,而且以后也方便以后重用(只需要复制几个文件即可)。

或许有些人认为:扩展所有页面的功能,还是使用基类比较好。
对于这个观点,我完全不反对。
但是,PageAdapter的好处在于它的可插拔性(类似HttpModule的优点)。
不过,我当时设计这种扩展方式只是想再换个方法尝试一下而已。

其实微软设计PageAdapter的本意是为了处理各种浏览器的兼容问题,但是我把这个功能用到扩展Page的功能上去了。 HttpModule可以进入到ASP.NET请求管线的任何阶段,但它就是进入不了页面的生命周期中,有了这个方法,我们就可以采用HttpModule这种【外挂】式的方法进入到页面生命周期中,我认为是很有意义的。

方法多了,我想不是件坏事。每种方法都有适合它们的应用场合,了解更多的方法,以后就能做出更优秀的设计。

这次想到这个话题是因为前面的博客【细说ASP.NET Forms 身份认证】中的示例代码。有些人看到那些代码,发现代码的运行方式比较特别,所以,今天我就打算着重介绍这种方法。

我们再来回顾一下以前博客中的示例代码,首先从页面代码开始:

<fieldset><legend>普通登录</legend><form action="<%= Request.RawUrl %>" method="post">
    登录名:<input type="text" name="loginName" style="width: 200px" value="Fish" />
    <input type="submit" name="NormalLogin" value="登录" />
</form></fieldset>
 
<fieldset><legend>包含【用户信息】的自定义登录</legend>    <form action="<%= Request.RawUrl %>" method="post">
    <table border="0">
    <tr><td>登录名:</td>
        <td><input type="text" name="loginName" style="width: 200px" value="Fish" /></td></tr>
    <tr><td>UserId:</td>
        <td><input type="text" name="UserId" style="width: 200px" value="78" /></td></tr>
    <tr><td>GroupId:</td>
        <td><input type="text" name="GroupId" style="width: 200px" />
        1表示管理员用户
        </td></tr>
    <tr><td>用户全名:</td>
        <td><input type="text" name="UserName" style="width: 200px" value="Fish Li" /></td></tr>
    </table>    
    <input type="submit" name="CustomizeLogin" value="登录" />
</form></fieldset>

在这段页面代码中,我定义了二个表单,它们包含各自的提交按钮(其实这也只是部分代码)。
再来看后台处理代码是如何响应提交请求的:

public partial class _Default : System.Web.UI.Page 
{
    [SubmitMethod(AutoRedirect = true)]
    public void NormalLogin()
    {
        // 省略登录处理代码。
        // 如果需要知道这段代码可以浏览下面的网址:
        // http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html 
    }

    [SubmitMethod(AutoRedirect = true)]
    public void CustomizeLogin()
    {
        // 省略登录处理代码。
        // 如果需要知道这段代码可以浏览下面的网址:
        // http://www.cnblogs.com/fish-li/archive/2012/04/15/2450571.html 
    }

注意观察,这二个C#方法的名称与页面二个submit按钮的name属性相同,因此可以猜测到这二个C#方法可以处理那二个submit按钮的提交请求。那么这二段代码是如何运行起来的呢?有些人或许看到了[SubmitMethod]的使用,认为与它们有关。其实这种说法并不正确,我也可以完全不使用它们。请记住:Attribute永远只是一个标记,它不可能让代码自动运行起来。

前面的代码能运行起来,与App_Browsers目录下的Page.browser文件有关,此文件的代码如下:

<browsers>
    <browser refID="Default">
        <controlAdapters>
            <adapter controlType="System.Web.UI.Page"
                     adapterType="FishDemoCodeLib.MyPageAdapter, FishDemoCodeLib" />
        </controlAdapters>
    </browser>
</browsers>

这里定义了一个MyPageAdapter,它用于Page控件的请求过程。 refID="Default" 表示是对ASP.NET定义的Default.browser文件补充一些配置,它将能匹配来自所有浏览器的请求。
我再来看一下MyPageAdapter的代码:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SubmitMethodAttribute : Attribute
{
    public bool AutoRedirect { get; set; }
}

internal sealed class MethodInvokeInfo
{
    public MethodInfo MethodInfo;
    public SubmitMethodAttribute MethodAttribute;
}

public class MyPageAdapter : System.Web.UI.Adapters.PageAdapter
{
    private static readonly Hashtable s_table = Hashtable.Synchronized(new Hashtable());

    private static MethodInvokeInfo[] GetMethodInfo(Type type)
    {
        MethodInvokeInfo[] array = s_table[type.AssemblyQualifiedName] as MethodInvokeInfo[];
        if( array == null ) {
            array = (from m in type.GetMethods(BindingFlags.Instance | BindingFlags.Public)
                     let a = m.GetCustomAttributes(
                            typeof(SubmitMethodAttribute), false) as SubmitMethodAttribute[]
                     where a.Length > 0
                     select new MethodInvokeInfo { 
                            MethodInfo = m, MethodAttribute = a[0] }).ToArray();

            s_table[type.AssemblyQualifiedName] = array;
        }
        return array;
    }


    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        if( Page.Request.Form.AllKeys.Length == 0 )
            return;    // 没有提交表单

        MethodInvokeInfo[] array = GetMethodInfo(Page.GetType().BaseType);
        if( array.Length == 0 )
            return;

        foreach( MethodInvokeInfo m in array ) {
            if( string.IsNullOrEmpty(Page.Request.Form[m.MethodInfo.Name]) == false ) {
                m.MethodInfo.Invoke(Page, null);

                if( m.MethodAttribute.AutoRedirect 
                                && Page.Response.IsRequestBeingRedirected == false )
                    Page.Response.Redirect(Page.Request.RawUrl);

                return;
            }
        }
    }
}

这段代码并不长,核心代码更是比较少。
代码中,最重要的一块是MyPageAdapter的实现,它继承了System.Web.UI.Adapters.PageAdapter,并重写了OnLoad方法(相当是在重写Page的OnLoad方法),也正是由于这个重写,代码才有机会在页面的生命周期中被执行,这一点是HttpModule做不到的。
在OnLoad方法中做了以下事情:
1. 检查是不是发生了表单提交的操作。
2. 获取当前页面类型的所有[SubmitMethod]修饰过的方法。
3. 检查提交的表单数据中,是否存在与name对应的C#方法名。
4. 如果找到一个匹配的方法名,则调用。
5. 如果在[SubmitMethod]中设置了AutoRedirect=true,则引发重定向。

注意:如果不调用base.OnLoad(e); 那么页面的Load事件根本不会发生。
也就是说:PageAdapter.OnLoad的调用时间要早于Page.Onload方法。

由于这段代码仅供我写示例代码时使用,因此并没有检查要调用的方法的参数是否满足条件,也没有优化刻意去优化它的性能。在我的设计中,被调用的方法应该是无参的,因此是容易判断的,而且可以使用一个固定签名的委托去优化它的,这些细节留着以后再去完善它吧!