好了让我们进入今天的主题看看下面这个简单的HTML表单。form actionHandler1.ashx methodpost p客户名称: input typetext nameCustomerName stylewidth: 300px //p p客户电话: input typetext nameCustomerTel stylewidth: 300px //p pinput typesubmit value提交 //p /form在这个HTML表单中我定义了二个文本输入框一个提交按钮表单将提交到Handler1.ashx中处理且以POST的方式。注意哦如果我们想让纯静态页面也能向服务器提交数据就可以采用这样方式来处理将action属性指向一个服务器能处理的地址。说明当我们使用WebForms的服务器表单控件时一般都会提交到页面自身来处理(action属性指向当前页面) 这样可以方便地使用按钮事件以及从服务器控件访问从浏览器提交的控件输入结果。如果在URL重写时希望能在页面回传时保持URL不变即action为重写后的URL那么可以Page类中执行以下调用Form.Action Request.RawUrl; // 受以下版本支持3.5 SP1、3.0 SP1、2.0 SP1好了我们再回到前面那个HTML表单看一下如果用户点击了“提交”按钮浏览器是如何把表单的内容发出的。 在此我们需要Fiddler工具的协助请在提交表单前启动好Fiddler。我将这个表单的提交请求过程做了如下截图。上图是将要提交的表单的输入情况下图是用Fiddler看到的浏览器发出的请求内容。在这张图片中我们可以看到浏览器确实将请求发给了我前面在action中指定的地址且以POST形式发出的。 表单的二个控件的输入值放在请求体中且做了【编码】处理编码的方式用请求头【Content-Type】说明 这样当服务端收到请求后就知道该如何读取请求的内容了。 注意表单的数据是以name1value1name2value2 的形式提交的其中name,value分别对应了表单控件的相应属性。我们还可以在Fiddler中将视图切换到WebForms选项卡这样能更清楚地只查看浏览器提交的数据如下图。看了客户端的页面和请求的内容我们再来看看在服务端如何获取浏览器提交的表单的输入吧代码如下string name context.Request.Form[CustomerName]; string tel context.Request.Form[CustomerTel];代码很简单直接根据表单控件的name属性访问Request.Form就可以了。回到顶部表单提交成功控件我们再来看一下浏览器是如何提交表单的或者说浏览器在提交表单时要做哪些事情。浏览器并不是将所有的表单控件全部发送到服务器的而是会查找所有的【成功控件】只将这些成功控件的数据发送到服务端 什么是成功控件呢简单地来说成功控件就是每个表单中的控件都应该有一个name属性和”当前值“ 在提交时它们将以 namevalue 的形式做为提交数据的一部分。对于一些特殊情况成功控件还有以下规定1. 控件不能是【禁用】状态即指定【disableddisabled】。即禁用的控件将不是成功控件。2. 如果一个表单包含了多个提交按键那么仅当用户点击的那个提交按钮才算是成功控件。3. 对于checkbox控件来说只有被用户勾选的才算是成功控件。4. 对于radio button来说只有被用户勾选的才算是成功控件。5. 对于select控件来说所有被选择的选项都做为成功控件name由select控件提供。6. 对于file上传文件控件来说如果它包含了选择的文件那么它将是一个成功控件。此外浏览器不会考虑Reset按钮以及OBJECT元素。注意1. 对于checkbox, radio button来说如果它们被确认为成功控件但没有为控件指定value属性 那么在表单提交时将会以on做为它们的value2. 如果在服务端读不到某个表单控件的值请检查它是否满足以上规则。提交方式在前面的示例代码中我为form指定了methodpost这个提交方法就决定了浏览器在提交数据时通过什么方式来传递它们。如果是【post】那么表单数据将放在请求体中被发送出去。如果是【get】那么表单数据将会追加到查询字符串中以查询字符串的形式提交到服务端。建议表单通常还是以post方式提交比较好这样可以不破坏URL况且URL还有长度限制。数据的编码前面我将浏览器的请求细节用Fiddler做了个截图从这个图中我们可以看到控件输入的内容并不是直接发送的 而是经过一种编码规则来处理的。目前基本上只会只使用二种编码规则application/x-www-form-urlencoded 和 multipart/form-data 这二个规则的使用场景简单地说就是后者在上传文件时使用其它情形则使用前者(默认)。这个规则是在哪里指定的呢 其实form还有个enctype属性用它就可以指定编码规则当我在VS2008写代码时会有以下提示按照我前面说过的编码规则选择逻辑application/x-www-form-urlencoded做为默认值所以一般情况下我们并不用显式指定。 除非我们要上传文件了那么此时必须设置enctypemultipart/form-data好了说了这么一大堆理论我们再来看一下浏览是如何处理表单数据的。这个过程大致分为4个阶段1. 识别所有的成功控件。2. 为所有的成功控件创建一个数据集合它们包含 control-name/current-value 这样的值对。3. 按照form.enctype指定的编码规则对前面准备好的数据进行编码。编码规则将放在请求中用【Content-Type】指出。4. 提交编码后的数据。此时会区分post,get二种情况提交的地址由form.action属性指定的。回到顶部多提交按钮的表单用过Asp.net WebForms框架的人可能都写过这样的页面一个页面中包含多个服务端按钮。处理方式嘛 也很简单在每个按钮的事件处理器写上相应的代码就完事了根本不用我们想太多。不过对于不理解这背后处理过程的开发人员来说当他们转到MVC框架下可能会被卡住MVC框架中可没有按钮事件 即使用不用MVC框架用ashx通用处理器的方式也会遇到这种问题怎么办对于这个问题本文将站在HTML角度给出二个最根本的解决办法。方法1根据【成功控件】定义我们设置按钮的name在服务端用name来区分哪个按钮的提交HTML代码form actionHandler1.ashx methodpost p客户名称: input typetext nameCustomerName stylewidth: 300px //p p客户电话: input typetext nameCustomerTel stylewidth: 300px //p pinput typesubmit namebtnSave value保存 / input typesubmit namebtnQuery value查询 / /p /form服务端处理代码// 注意我们只要判断指定的name是否存在就可以了。 if( string.IsNullOrEmpty(context.Request.Form[btnSave]) false ) { // 保存的处理逻辑 } if( string.IsNullOrEmpty(context.Request.Form[btnQuery]) false ) { // 查询的处理逻辑 }方法2我将二个按钮的name设置为相同的值根据前面的成功控件规则只有被点击的按钮才会提交在服务端判断value示例代码如下form actionHandler1.ashx methodpost p客户名称: input typetext nameCustomerName stylewidth: 300px //p p客户电话: input typetext nameCustomerTel stylewidth: 300px //p pinput typesubmit namesubmit value保存 / input typesubmit namesubmit value查询 / /p /formstring action context.Request.Form[submit]; if( action 保存 ) { // 保存的处理逻辑 } if( action 查询 ) { // 查询的处理逻辑 }当然了解决这个问题的方法很多我们还可以在提交前修改form.action属性。 对于MVC来说可能有些人会选择使用Filter的方式来处理。最终选择哪种方法可根据各自喜好来选择。我可能更喜欢直接使用Ajax提交到一个具体的URL这样也很直观在服务端也就不用这些判断了。接着往下看吧。回到顶部上传文件的表单前面我说到“数据的编码提到了form.enctype这个属性正是上传表单与普通表单的区别请看以下示例代码form actionHandler2.ashx methodpost enctypemultipart/form-data pinput typetext namestr value一个字符串别管它 //p p要上传的文件input typefile namefile1//p p要上传的文件input typefile namefile2//p pinput typesubmit value提交 //p /form我将上传2个小文件我们再来看看当我点击提交按钮时浏览器发送的请求是个什么样子的注意我用红色边框框出来的部分以及请求体中的内容。此时请求头Content-Type的值发生了改变 而且还多了一个叫boundary的参数它将告诉服务端请求体的内容以这个标记来分开。并且请求体中每个分隔标记会单独占一行且具体内容为-- boundary 最后结束的分隔符的内容为-- boundary -- 也是独占一行。 从图片中我们还可以发现在请求体的每段数据前还有一块描述信息。具体这些内容是如何生成的可以参考本文后面的实现代码。再来看看在服务端如何读取上传的文件。HttpPostedFile file1 context.Request.Files[file1]; if( file1 ! null string.IsNullOrEmpty(file1.FileName) false ) file1.SaveAs(context.Server.MapPath(~/App_Data/) file1.FileName); HttpPostedFile file2 context.Request.Files[file2]; if( file2 ! null string.IsNullOrEmpty(file2.FileName) false ) file2.SaveAs(context.Server.MapPath(~/App_Data/) file2.FileName);或者HttpFileCollection files context.Request.Files; foreach( string key in files.AllKeys ) { HttpPostedFile file files[key]; if( string.IsNullOrEmpty(file.FileName) false ) file.SaveAs(context.Server.MapPath(~/App_Data/) file.FileName); }二种方法都行前者更能体现控件的name与服务端读取的关系后者在多文件上传时有更好的扩展性。安全问题注意上面示例代码中这样的写法是极不安全的。正确的做法应该是重新生成一个随机的文件名 而且最好能对文件内容检查例如如果是图片可以调用.net的一些图形类打开文件然后另存文件。 总之在安全问题面前只有一个原则不要相信用户的输入一定要检查或者转换。回到顶部MVC Controller中多个自定义类型的传入参数前面的所有示例代码中都有一个规律在服务端读取浏览器提交的数据时都会使用控件的name属性基本上在Asp.net中就是这样处理。 但是在MVC中MS为了简化读取表单数据的代码可以让我们直接在Controller的方法中直接以传入参数的形式指定 此时框架会自动根据方法的参数名查找对应的输入数据当然也不止表单数据了。下面举个简单的例子form action/Home/Submit methodpost p客户名称: input typetext nameName stylewidth: 300px //p p客户电话: input typetext nameTel stylewidth: 300px //p pinput typesubmit value提交 //p /formConntroller中的方法的签名public ActionResult Submit(Customer customer) { } public ActionResult Submit(string name, string tel) { }以上二种方法都是可以的当然了前者会比较好但需要事先定义一个Customer类代码如下public class Customer { public string Name { get; set; } public string Tel { get; set; } }如果表单简单或者业务逻辑简单我们或许一直也不会遇到什么麻烦以上代码能很好的工作。 但是如果哪天我们有了新的业务需要求需要在这个表单中同时加上一些其它的内容例如要把业务员的资料也一起录入进去。 其中业务员的实体类定义如下public class Salesman { public string Name { get; set; } public string Tel { get; set; } }Controller的接口需要修改成public ActionResult Submit(Customer customer, Salesman salesman) { }这时HTML表单又该怎么写呢刚好这二个类的(部分)属性名称一样显然前面表单中的Name,Tel就无法对应了。 此时我们可以将表单写成如下形式form action/Home/Submit methodpost p客户名称: input typetext namecustomer.Name stylewidth: 300px //p p客户电话: input typetext namecustomer.Tel stylewidth: 300px //p p销售员名称: input typetext namesalesman.Name stylewidth: 300px //p p销售员电话: input typetext namesalesman.Tel stylewidth: 300px //p pinput typesubmit value提交 //p /form注意Controller方法中的参数名与HTML表单中的name是有关系的。回到顶部F5刷新问题并不是WebForms的错刚才说到了MVC框架再来说说WebForms框架。以前时常听到有些人在抱怨用WebForms的表单有F5的刷新重复提交问题。 在此我想为WebForms说句公道话这个问题并不是WebForms本身的问题是浏览器的问题 只是如果您一直使用WebForms的较传统用法是容易产生这个现象的。那么什么叫做【传统用法】呢这里我就给个我自己的定义吧 所谓的WebForms的传统用法是说您的页面一直使用服务器控件的提交方式(postback)在事件处理后页面又进入再一次的重现过程 或者说当前页面一直在使用POST方式向当前页面提交。那么如何避开这个问题呢办法大致有2种1. PRG模式(Post-Redirect-Get)在事件处理后调用重定向的操作Response.Redirect() 而不要在事件处理的后期再去给一些服务器控件绑定数据项了建议按钮事件只做一些提交数据的处理将数据绑定的操作放在OnPreRender方法中处理而不是写在每个事件中遍地开花。 不过这种方式下可能伟大的ViewState就发挥不了太大的作用了如果您发现ViewState没用了在Web.config中全局关掉后 又发现很多服务器控件的高级事件又不能用了嗯杯具有啊。这个话题说下去又没完没了到此为止吧不过千万不要以为这种方法是在倒退哦。2. 以Ajax方式提交表单请继续阅读本文。