ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ASP.NET Web程序设计课件:请求管线、ViewState与数据访问实验

ASP.NET Web程序设计课件:请求管线、ViewState与数据访问实验 简介这份课件面向高校计算机相关专业学生与ASP.NET初学者对应《ASP.NET Web程序设计》第二版全套教学讲义可用于课堂同步学习、期末复习与自学入门。压缩包内仅1个PPT文件约4.2MB为整本书配套的完整版电子课件以幻灯片形式分章组织图文并茂便于直接投屏讲解或打印成讲义。内容从Web基础知识讲起梳理HTTP请求与响应流程、客户端-服务器体系结构、B/S三层数据库应用、静态网页与动态网页的差异再进入ASP.NET工作原理对比ASP与ASP.NET在效率、可重用性、代码量上的区别介绍.NET框架的组成、MS中间语言、CLR公共语言运行库与.NET类库、常见.NET语言并归纳ASP.NET设计目标、程序基本结构及设计、实现、测试、维护等开发步骤。目前已有181人学习适合希望系统掌握ASP.NET入门知识、建立完整知识框架的读者。1. 拿到《ASP.NET Web 程序设计》第二版课件先别急着翻 PPT一套完整的《ASP.NET Web 程序设计》第二版电子课件通常按 Web 基础、HTML 与 CSS、服务器控件、状态管理、数据访问、母版页、配置与部署这样排下来PPT 十几章配套讲义也齐。但把整套课件从头翻到尾的人很多还是写不出一个像样的页面表单提交后控件值丢了GridView 绑不上数据登录态刷新一次就没了调试半天只能靠 Response.Write。问题不在课件厚度而在于每一章 PPT 都在讲一个独立知识点缺一条把请求管线、页面生命周期、状态管理、数据访问串起来的执行主线。下面按这套课件的章节顺序把知识点落成本机能跑起来、能打断点、能看 trace 的最小实验用 IIS Express 起站点看一次回发到底走了哪些事件ViewState 里究竟存了什么参数化查询为什么不能拼字符串GridView 零代码绑定的分页为什么在大表上会拖垮进程。适合两类人跟着课件学第一遍、想补上动手环节的人和要把这套课件改成实验课的教师。2. ASP.NET Web 程序设计讲义里的请求管线一次 .aspx 回发经历了什么2.1 从 IIS 到 .aspx 的请求处理链路课件讲 IIS 那一章通常一笔带过但这条链路决定了后面所有实验能不能解释清楚。浏览器发一个 GET /Default.aspxIIS 在集成模式下把请求交给 ASP.NET 运行时HttpRuntime 从应用程序池里取一个 HttpApplication 实例按顺序触发管线事件到 MapRequestHandler 阶段根据 URL 扩展名映射到 PageHandlerFactory工厂生成 Default_aspx 实例也就是我们写的那个页面类页面跑完生命周期、Render 出 HTML 后响应回到 IIS 再回浏览器。HttpModule 可以挂在任意管线事件上Global.asax 本质就是 HttpApplication 事件的一份集中挂接点——这两者在课件里常被当成配置文件知识点讲其实它们是观察整条链路最直接的窗口。不想每次都开 Visual Studio 的话命令行起站点更省事# 用 IIS Express 直接跑一个 Web Forms 站点路径换成你自己的项目根目录 C:\Program Files\IIS Express\iisexpress.exe /path:C:\WebApp /port:8080 /clr:v4.0 /systray:false/path是站点物理根目录必须含 web.config/port是监听端口被占用就换一个/clr:v4.0指定 CLR 版本Web Forms 项目基本都跑在 .NET Framework 4.x 上/systray:false只是不弹托盘图标方便在脚本里跑。再看一眼编译产物课件里aspx 会被编译这句话才站得住# 预编译整站然后去输出目录的 bin 里找 App_Web_*.dll aspnet_compiler -v / -p C:\WebApp -f C:\WebApp_Precompiled-v是虚拟路径-p是物理路径-f指定输出目录并允许覆盖。预编译完用 ILSpy 打开生成的程序集能直接看到Default_aspx继承自Page页面上每个runatserver的标签都变成了类里的一个字段。这一步做完后面讲控件树和 ViewState 就不会悬空。2.2 Page 生命周期各阶段与事件触发顺序课件里那张生命周期图信息量最大但学生最容易记混的是哪个阶段能改什么。按实际执行顺序拆开看阶段页面事件这个阶段适合做的事PreInitPage_PreInit动态设置 MasterPageFile、Theme创建动态控件InitPage_Init控件树已建好读控件属性、挂事件处理InitCompletePage_InitComplete视图状态还没加载做一次性初始化PreLoadPage_PreLoad回发前的最后准备LoadPage_Load用 IsPostBack 分支做首次绑定控件回发事件Button_Click 等真正处理业务PreRenderPage_PreRender最后一次修改控件属性改动会被保存SaveStateComplete无视图状态已经写完再改控件不会生效Render无递归输出 HTMLUnloadPage_Unload释放非托管资源不能在这里改响应两个反直觉点值得在讲义里加粗一是 Page_Load 比 Button_Click 先执行所以在 Load 里读按钮点击后的结果一定是空的二是动态控件必须在 PreInit 或 Init 阶段创建等到 Load 才Controls.Add控件的视图状态和回发数据都对不上页面上会看到值莫名其妙变成空。这两条几乎决定了后面所有实验能不能复现。2.3 用 HttpModule 与 Global.asax 把管线打成 trace光看图不如让程序自己说话。写一个最小的 Module把管线关键节点写进 ASP.NET 的 trace// LifecycleTraceModule.cs —— 挂在管线事件上配合 trace.axd 观察执行顺序 using System.Web; namespace WebApp { public class LifecycleTraceModule : IHttpModule { public void Init(HttpApplication app) { // 请求刚进来此时还没确定由哪个 Handler 处理 app.BeginRequest (s, e) HttpContext.Current.Trace.Write(Pipeline, BeginRequest: HttpContext.Current.Request.RawUrl); // Page 即将执行生命周期马上开始 app.PreRequestHandlerExecute (s, e) HttpContext.Current.Trace.Write(Pipeline, PreRequestHandlerExecute); // 响应即将结束页面的 Render 已经完成 app.EndRequest (s, e) HttpContext.Current.Trace.Write(Pipeline, EndRequest状态码 HttpContext.Current.Response.StatusCode); } public void Dispose() { } } }在 web.config 里注册并打开 traceconfiguration system.web !-- pageOutputfalse 表示不把 trace 追加到页面底部改到 /trace.axd 看 -- trace enabledtrue pageOutputfalse requestLimit50 localOnlytrue / /system.web system.webServer modules !-- type 格式是完整类名, 程序集名preCondition 限定只对托管 Handler 生效 -- add nameLifecycleTrace typeWebApp.LifecycleTraceModule, WebApp preConditionmanagedHandler / /modules /system.webServer /configurationrequestLimit是保留的请求条数调大占内存localOnlytrue只允许本机访问 trace.axd这一项千万别改成 false否则等于把每个请求的细节挂在公网上。Global.asax 里也能做同样的事而且顺便暴露一个常见误区// Global.asax.cs protected void Application_BeginRequest(object sender, EventArgs e) { // 这里读 Context.Session 会直接抛异常Session 还没建立 Context.Trace.Write(Global, BeginRequest 到了); } protected void Application_AcquireRequestState(object sender, EventArgs e) { // Session 在这个阶段才可用要读 SessionID 就写在这里 if (Context.Handler is System.Web.UI.Page Context.Session ! null) Context.Trace.Write(Global, SessionID Context.Session.SessionID); }跑一次页面访问打开/trace.axd能看到 BeginRequest → PreRequestHandlerExecute → AcquireRequestState → EndRequest 的真实顺序比背生命周期表有效得多。2.4 课件最容易讲错的两个点IsPostBack 与控件重建IsPostBack判断的是本次请求是不是回发首次 GET 为 false按钮提交后为 true。它常被写成判断页面是否第一次加载严格说不准确——刷新页面也是回发但控件值可能已经被浏览器重放。更稳的写法是同时看 IsPostBack 和具体业务状态。动态控件的坑更硬protected void Page_PreInit(object sender, EventArgs e) { // 动态控件唯一可靠的创建时机PreInit。 // 放到 Page_Load 里创建控件会错过 Init视图状态和回发数据全部对不上。 var lbl new Label { ID lblDynamic, Text 动态创建 }; form1.Controls.Add(lbl); }ID 必须稳定。如果每次请求用 Guid 当控件 ID回发数据用的是上一次的键新版控件树里找不到它值就丢了。课件里讲控件 ID 要唯一真正要强调的是唯一且可复现。3. 课件里的服务器控件与 ViewStateASP.NET Web 程序设计的状态管理实验3.1 runatserver 之后页面变成了一棵控件树runatserver不是简单加个标记。aspx 被编译成Page的派生类每个服务器控件在类里对应一个字段页面初始化时按声明顺序实例化再挂到父控件的Controls集合里形成一棵树。Render 阶段就是从根节点递归遍历每个控件把自己的 HTML 吐出来。理解这一点GridView 的FindControl、母版页里找子页控件、跨页传值这些课件里分散在各章的操作就统一了本质都是在控件树上按 ID 走。同时也解释了为什么控件 ID 变了值就丢——回发数据PostData和视图状态都是按控件 ID 生成键的键对不上恢复自然失败。3.2 ViewState 的序列化格式与 MAC 校验参数__VIEWSTATE隐藏字段里的内容是LosFormatter序列化后再 Base64 的结果最常见的外层类型是System.Web.UI.Pair里面装着控件状态和视图状态两棵树。序列化内容默认只做完整性校验不做加密如果要防止内容被看到得显式开加密。system.web !-- 4.5.2 之后 enableViewStateMac 强制为 true写 false 也会被忽略 -- pages enableViewStatetrue enableViewStateMactrue viewStateEncryptionModeAlways / !-- 集群或负载均衡部署必须显式指定autoGenerate 每次重启都会换密钥 -- machineKey validationHMACSHA256 decryptionAES validationKey按生成工具产出后填入 decryptionKey按生成工具产出后填入 / /system.web几个参数直接影响线上表现enableViewStateMac负责校验关掉它页面会正常跑但 ViewState 就从状态载体变成了外部可改的输入viewStateEncryptionMode设成Always会增加每次请求的加解密开销页面 ViewState 很大时能明显感觉到machineKey最容易被忽略多台服务器各自 autoGenerate 时A 机发出的 ViewState 到 B 机校验失败浏览器上看到的是验证视图状态 MAC 失败本地单机永远复现不出来。注意ViewState 存在客户端用户随时可以改。保持 MAC 和加密开启、machineKey 定期轮换且不写进代码仓库是应有的底线。不要在产品代码里去反序列化未经校验的 ViewState 字符串。本地调试想看看里面存了什么可以只读地解一下// 仅用于本地排查把页面源码里 __VIEWSTATE 的值复制进来 string vs 粘贴 __VIEWSTATE 的值; var formatter new System.Web.UI.LosFormatter(); using (var sr new System.IO.StringReader(vs)) { object state formatter.Deserialize(sr); // 常见结果是 System.Web.UI.Pair里面再拆才是各控件的状态 Console.WriteLine(state.GetType().FullName); }解出来能看到哪些控件的哪些属性被存了进去。ViewState 体积过大的页面把只读控件的EnableViewStatefalse关掉页面大小往往能降一半以上。3.3 Session、Cookie、Application、Cache 怎么选课件通常把这四种列成一张对比表但不说何时用哪个机制存储位置生命周期典型用途关键限制ViewState页面隐藏字段单页回发控件状态恢复体积计入响应ControlState隐藏字段单页回发分页控件等关键状态无法关闭Session服务端会话期登录态、购物车InProc 模式进程回收即丢Cookie客户端可设过期时间免登录标识单域名约 4KBApplication服务端应用生命周期全局计数、配置缓存写入需加锁Cache服务端可设过期策略查询结果缓存内存压力下会被回收改 Session 模式只需要动一行配置system.web !-- InProc 最快但会丢StateServer 需要先起 ASP.NET State Service -- sessionState modeStateServer stateConnectionStringtcpip127.0.0.1:42424 cookielessUseCookies timeout20 / /system.webmode可选 InProc、StateServer、SQLServer、Offtimeout单位是分钟默认 20cookielessUseCookies用 Cookie 传 SessionID如果改成UseUriSessionID 会拼进 URL课件里出现过这个写法但地址栏一复制就泄漏会话线上不要用。3.4 一个跨页传值的完整最小示例课件里跨页传值常列五六种方式实际讲课留两种就够。服务端跳转// A 页面把条件放进 Context.Items再交给 B 页面 protected void btnSearch_Click(object sender, EventArgs e) { Context.Items[keyword] txtKeyword.Text.Trim(); Server.Transfer(Result.aspx); // 服务端跳转浏览器地址栏不变 }// B 页面读 Context.Items读不到再回来源页找控件 protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { string kw Context.Items[keyword] as string; if (PreviousPage ! null string.IsNullOrEmpty(kw)) kw ((TextBox)PreviousPage.FindControl(txtKeyword))?.Text; lblResult.Text string.IsNullOrEmpty(kw) ? 没有关键字 : 关键字 kw; } }Server.Transfer在一次请求内完成Context.Items和PreviousPage都有效换成Response.Redirect会发 302浏览器重新请求Context.Items清空只能靠 QueryString 或 Session。B 页面要用PreviousPage得声明% PreviousPageType VirtualPath~/A.aspx %否则PreviousPage是 null这点课件里经常漏写。4. 从 ADO.NET 到 GridViewASP.NET Web 程序设计数据访问章节的落地4.1 Connection、Command、DataReader、DataAdapter 怎么取舍数据访问这章的核心不是记住四个类名而是分清连接式和断开式对象是否保持连接适用场景注意点SqlConnection是所有操作的入口必须 using 或 try/finally 释放SqlCommand是单条 SQL 或存储过程一律用参数不拼字符串SqlDataReader是只读、单向、大数据量遍历离开连接即失效SqlDataAdapter DataSet否需要离线绑定、批量更新数据一次性读进内存一个可以运行的查询实验// 连接式读取只把需要的行取出来适合列表页 const string sql SELECT Id, Title, Author, Price FROM Books WHERE Category Category AND Price MaxPrice ORDER BY Title; using (var conn new SqlConnection( ConfigurationManager.ConnectionStrings[BookDb].ConnectionString)) using (var cmd new SqlCommand(sql, conn)) { // 显式指定类型和长度让 SQL Server 复用执行计划避免隐式转换 cmd.Parameters.Add(Category, SqlDbType.NVarChar, 20).Value category; cmd.Parameters.Add(MaxPrice, SqlDbType.Decimal).Value maxPrice; conn.Open(); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { // 用序号取值比用列名快列名在循环里每次都要查元数据 Console.WriteLine(reader.GetString(1) / reader.GetDecimal(3)); } } }参数说明SqlDbType.NVarChar, 20里的 20 是长度和表定义对齐能避免计划缓存里出现一堆长度不同的变体AddWithValue写起来省事但它是根据 .NET 值反推 SQL 类型字符串容易按 nvarchar(4000) 发过去导致索引扫描和计划膨胀参数一多就该换成显式 Add。4.2 为什么不能拼字符串实验一眼就能看出来把上面的参数化写法换成拼接再跑一次带引号的输入就能看到注入发生。防护手段其实只有一条值一律走参数标识符表名、列名、排序字段走白名单映射。// 反例看起来能跑输入一个单引号就出事 // var sql SELECT * FROM Books WHERE Title LIKE % keyword %; // 正例LIKE 也能参数化通配符拼在参数值里而不是 SQL 文本里 cmd.CommandText SELECT Id, Title FROM Books WHERE Title LIKE kw; cmd.Parameters.Add(kw, SqlDbType.NVarChar, 100).Value % keyword %;排序字段不能用参数只能白名单// 排序方向和白名单映射避免把用户输入直接拼进 ORDER BY string[] allowed { Title, Author, Price }; string sort allowed.Contains(Request.QueryString[sort]) ? Request.QueryString[sort] : Title; cmd.CommandText $SELECT Id, Title, Author, Price FROM Books ORDER BY {sort};参数说明allowed.Contains保证了拼进 SQL 的只有代码里写死的三个列名之一用户输入只用来做选择不进入 SQL 文本。4.3 GridView 加 SqlDataSource 的零代码绑定课件演示数据绑定时最常用这套组合确实可以一行 C# 都不写asp:GridView IDgvBooks runatserver DataSourceIDdsBooks AutoGenerateColumnsfalse AllowPagingtrue PageSize20 AllowSortingtrue EmptyDataText没有数据 Columns asp:BoundField DataFieldTitle HeaderText书名 SortExpressionTitle / asp:BoundField DataFieldAuthor HeaderText作者 SortExpressionAuthor / asp:BoundField DataFieldPrice HeaderText价格 DataFormatString{0:C2} / /Columns /asp:GridView asp:SqlDataSource IDdsBooks runatserver ConnectionString%$ ConnectionStrings:BookDb % SelectCommandSELECT Id, Title, Author, Price FROM Books WHERE Category Category ORDER BY Title DataSourceModeDataSet CacheDuration60 CacheExpirationPolicyAbsolute SelectParameters asp:QueryStringParameter NameCategory QueryStringFieldcat DefaultValue计算机 / /SelectParameters /asp:SqlDataSource参数说明ConnectionString用表达式绑定到 web.config 的 connectionStrings 节避免连接串散落在页面里DataSourceMode必须是DataSetAllowPaging和AllowSorting才生效CacheDuration60配合Absolute表示 60 秒绝对过期同一查询直接命中缓存QueryStringParameter从 URL 取cat缺省值是计算机比在代码里写 if 判断干净。4.4 分页、排序和缓存的真实代价DataSourceModeDataSet的代价是把符合条件的全部行读进内存再分页。几万行看不出来几百万行时 w3wp 内存会直接顶上去而且每次翻页都要重新查一遍全表。数据量上来后必须换自定义分页只取当前页那 20 行-- 只返回当前页的数据配合 ObjectDataSource 的 SelectCountMethod 算总页数 SELECT Id, Title, Author, Price FROM ( SELECT Id, Title, Author, Price, ROW_NUMBER() OVER (ORDER BY Title) AS RowNum FROM Books WHERE Category Category ) AS T WHERE T.RowNum BETWEEN StartRow AND EndRow;参数说明StartRow是(页码-1)*每页条数1EndRow是页码*每页条数ROW_NUMBER()的 ORDER BY 必须和展示顺序一致否则翻页会看到重复或漏行。缓存参数也要跟着调带用户维度的查询把CacheDuration设短或干脆关掉公共列表页才适合缓存几十秒。5. 用 ASP.NET Core 反向验证《ASP.NET Web 程序设计》课件里的关键机制5.1 概念对照Web Forms 与 ASP.NET Core 差在哪课件里的很多机制在 Core 里被换掉了做一张对照表学生反而更容易理解 Web Forms 为什么这么设计Web FormsASP.NET Core差异的原因aspx code-behindRazor Pages 的 .cshtml PageModel页面与逻辑仍然分文件但不再有页面类继承服务器控件Tag Helper 原生 HTML控件树换成了服务端渲染模板ViewState无状态靠 POST 表单和 Model Binding 显式传递GridView SqlDataSourceEF Core 查询 foreach 渲染数据访问和渲染解耦web.configappsettings.json 环境变量配置来源更多元Global.asax中间件管道管线从事件模型变成显式委托链对照之后会发现ViewState 承担的回发后恢复控件值在 Core 里是靠表单字段绑定到 PageModel 属性实现的机制不同但解决的问题一样。5.2 用 dotnet 命令跑一个对照实验# 建一个 MVC 或 Razor Pages 项目用来对照课件的数据绑定章节 dotnet new webapp -o BookStore cd BookStore dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet watch rundotnet new webapp生成 Razor Pages 骨架dotnet watch run会在源码改动后自动重编译并刷新浏览器比 Web Forms 改一次等一次编译快得多适合做课堂演示。同一个按分类查书的需求在 Core 里长这样* Pages/Books/Index.cshtml —— 对应课件里 GridView 那一页 * model IEnumerableBookStore.Models.Book table theadtrth书名/thth作者/thth价格/th/tr/thead tbody foreach (var b in Model) { trtdb.Title/tdtdb.Author/tdtdb.Price.ToString(C2)/td/tr } /tbody /table// 分页不再依赖控件的 AllowPaging而是查询时直接限制返回行数 int page Math.Max(1, pageIndex); var books await _db.Books .Where(b b.Category cat) .OrderBy(b b.Title) .Skip((page - 1) * 20) // 跳过前面几页 .Take(20) // 只取当前页 .AsNoTracking() // 只读场景关掉变更跟踪省内存 .ToListAsync();参数说明Skip和Take会翻译成 SQL 的 OFFSET/FETCH只查当前页AsNoTracking()在只读列表页能明显降低内存和 CPU分页总数另外用CountAsync()查一次不要为了拿总数把明细全读出来。5.3 每章课件配套实验的验收点课件改成实验课每个知识点留一个能自己验证的动作就够了课件章节实验动作验收标准请求管线打开 /trace.axd 观察一次回发能看到 BeginRequest、PreRequestHandlerExecute、EndRequest 的顺序页面生命周期在 Page_PreInit 和 Page_Load 各创建一个动态控件Load 里创建的控件回发后值丢失PreInit 里的保留ViewState把某控件 EnableViewState 设为 false 后回发页面源码里 __VIEWSTATE 变短该控件值不再恢复状态管理把 sessionState 的 mode 从 InProc 改成 StateServer重启 w3wp 后登录态仍在改回 InProc 则立刻失效数据访问把参数化查询临时改成字符串拼接输入带单引号的关键字在 SQL Server Profiler 里能看到语句结构被改写数据绑定用 10 万行数据开启 AllowPaging 翻页观察 w3wp 内存曲线换成 ROW_NUMBER 分页后回落本文还有配套的精品资源点击获取
返回列表