本文对form表单做了深入而透彻的讲解与分析,感兴趣的朋友请参考学习之。
Form(表单)对于每个WEB开发人员来说,应该是再熟悉不过的东西了,可它却是页面与WEB服务器交互过程中最重要的信息来源。 虽然Asp.net WebForms框架为了帮助我们简化开发工作,做了很完美的封装,让我们只需要简单地使用服务端控件就可以直接操作那些 HTML表单元素了。但我认为了解一些基础的东西,可以使我们不必束缚在WebForms框架上,以及遇到一些奇怪问题时, 可以更从容地解决它们。
今天,我将和大家来聊聊表单,这个简单又基础的东西。我将站在HTML和单纯的Asp.net框架的角度来解释它们的工作方式, 因此,本文不演示WebForms服务器控件的相关内容。
简单的表单,简单的处理方式
好了,让我们进入今天的主题,看看下面这个简单的HTML表单。
在这个HTML表单中,我定义了二个文本输入框,一个提交按钮,表单将提交到Handler1.ashx中处理,且以POST的方式。
注意哦,如果我们想让纯静态页面也能向服务器提交数据,就可以采用这样方式来处理:将action属性指向一个服务器能处理的地址。
说明:当我们使用WebForms的服务器表单控件时,一般都会提交到页面自身来处理(action属性指向当前页面), 这样可以方便地使用按钮事件以及从服务器控件访问从浏览器提交的控件输入结果。
如果在URL重写时,希望能在页面回传时保持URL不变,即:action为重写后的URL,那么可以Page类中执行以下调用:
好了,我们再回到前面那个HTML表单,看一下如果用户点击了“提交”按钮,浏览器是如何把表单的内容发出的。 在此,我们需要Fiddler工具的协助,请在提交表单前启动好Fiddler。我将这个表单的提交请求过程做了如下截图。
上图是将要提交的表单的输入情况,下图是用Fiddler看到的浏览器发出的请求内容。
在这张图片中,我们可以看到浏览器确实将请求发给了我前面在action中指定的地址,且以POST形式发出的。 表单的二个控件的输入值放在请求体中,且做了【编码】处理,编码的方式用请求头【Content-Type】说明, 这样,当服务端收到请求后,就知道该如何读取请求的内容了。 注意:表单的数据是以name1=value1&name2=value2 的形式提交的,其中name,value分别对应了表单控件的相应属性。
我们还可以在Fiddler中,将视图切换到WebForms选项卡,这样能更清楚地只查看浏览器提交的数据,如下图。
看了客户端的页面和请求的内容,我们再来看看在服务端如何获取浏览器提交的表单的输入吧,代码如下:
代码很简单,直接根据表单控件的name属性访问Request.Form就可以了。
回到顶部
表单提交,成功控件
我们再来看一下浏览器是如何提交表单的,或者说,浏览器在提交表单时,要做哪些事情。
浏览器并不是将所有的表单控件全部发送到服务器的,而是会查找所有的【成功控件】,只将这些成功控件的数据发送到服务端, 什么是成功控件呢?
简单地来说,成功控件就是:每个表单中的控件都应该有一个name属性和”当前值“, 在提交时,它们将以 name=value 的形式做为提交数据的一部分。
对于一些特殊情况,成功控件还有以下规定:
1. 控件不能是【禁用】状态,即指定【disabled="disabled"】。即:禁用的控件将不是成功控件。
2. 如果一个表单包含了多个提交按键,那么仅当用户点击的那个提交按钮才算是成功控件。
3. 对于checkbox控件来说,只有被用户勾选的才算是成功控件。
4. 对于radio button来说,只有被用户勾选的才算是成功控件。
5. 对于select控件来说,所有被选择的选项都做为成功控件,name由select控件提供。
6. 对于file上传文件控件来说,如果它包含了选择的文件,那么它将是一个成功控件。
此外,浏览器不会考虑Reset按钮以及OBJECT元素。
注意:
1. 对于checkbox, radio button来说,如果它们被确认为成功控件,但没有为控件指定value属性, 那么在表单提交时,将会以"on"做为它们的value
2. 如果在服务端读不到某个表单控件的值,请检查它是否满足以上规则。
提交方式:在前面的示例代码中,我为form指定了method="post",这个提交方法就决定了浏览器在提交数据时,通过什么方式来传递它们。
如果是【post】,那么表单数据将放在请求体中被发送出去。
如果是【get】,那么表单数据将会追加到查询字符串中,以查询字符串的形式提交到服务端。
建议:表单通常还是以post方式提交比较好,这样可以不破坏URL,况且URL还有长度限制。
数据的编码:前面我将浏览器的请求细节用Fiddler做了个截图,从这个图中我们可以看到:控件输入的内容并不是直接发送的, 而是经过一种编码规则来处理的。目前基本上只会只使用二种编码规则:application/x-www-form-urlencoded 和 multipart/form-data , 这二个规则的使用场景简单地说就是:后者在上传文件时使用,其它情形则使用前者(默认)。
这个规则是在哪里指定的呢? 其实form还有个enctype属性,用它就可以指定编码规则,当我在VS2008写代码时,会有以下提示:
按照我前面说过的编码规则选择逻辑,application/x-www-form-urlencoded做为默认值,所以,一般情况下我们并不用显式指定。 除非我们要上传文件了,那么此时必须设置enctype="multipart/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代码
服务端处理代码
方法2:我将二个按钮的name设置为相同的值(根据前面的成功控件规则,只有被点击的按钮才会提交),在服务端判断value,示例代码如下:
string action = context.Request.Form["submit"];
if( action == "保存" ) {
// 保存的处理逻辑
}
if( action == "查询" ) {
// 查询的处理逻辑
}
当然了,解决这个问题的方法很多,我们还可以在提交前修改form.action属性。 对于MVC来说,可能有些人会选择使用Filter的方式来处理。最终选择哪种方法,可根据各自喜好来选择。
我可能更喜欢直接使用Ajax提交到一个具体的URL,这样也很直观,在服务端也就不用这些判断了。接着往下看吧。
回到顶部
上传文件的表单
前面我说到“数据的编码"提到了form.enctype,这个属性正是上传表单与普通表单的区别,请看以下示例代码:
我将上传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的方法中直接以传入参数的形式指定, 此时框架会自动根据方法的参数名查找对应的输入数据(当然也不止表单数据了)。下面举个简单的例子:
Conntroller中的方法的签名:
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就无法对应了。 此时我们可以将表单写成如下形式:
注意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方式提交表单,请继续阅读本文。