ASP.NET Page 那点事

一些重要的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默认参数的具体配置节如下:

  1. 为了代码重用,设计用户控件也是很常用的方法。

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

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

复制代码
<pages>
    <namespaces>
        <add namespace="MyMVC" />
        <add namespace="WebSiteCommonLib" />
        <add namespace="WebSiteModel" />
    </namespaces>
</pages>

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

不知道有些人想过:为什么在页面中使用某些微软提供的类型就不需要导入命名空间?

答案是:其实ASP.NET已经将一些微软认为常用的命名空间在web.config中配置好了:

  1. 现在,有越来越多的人为了方便而使用扩展方法。使用扩展方法的好处是:可以让我们不去关心这些扩展方法定义在那个类中, 只要在支持扩展方法的对象上调用就可以了,就像下面的代码这样:
复制代码
当前用户已登录,登录名:<%= 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
相关推荐
木西1 小时前
告别机械重复:使用 Node.js + Playwright 构建 24 小时全自动测试网领水脚本
前端·javascript·node.js
爱敲代码的小鱼1 小时前
springsecurity:
java·开发语言·数据库
豆沙小饼干1 小时前
MySQL 小白循序渐进实操路线(纯动手,跟着敲就能练,全程生活化案例:外卖平台)
java
西柚小萌新1 小时前
【论文写作】--LaTeX详细入门教程2
前端·html
qdkfz1 小时前
JavaScript 如何解析 Outlook .msg 文件 —— @kenjiuno/msgreader
前端·javascript
Wang's Blog1 小时前
Java框架快速入门: Spring Bean核心配置与实例化详解
java·开发语言·spring
LuckyDog阿祥2 小时前
卸载Microsoft Edge:EdgeRemover完整操作指南
前端·edge
zhangxingchao2 小时前
AI应用开发九:从 Hermes Agent 学长期记忆
前端·人工智能·后端
爱敲代码的小鱼2 小时前
springboot原理篇:
java·spring boot·spring