大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: ASP.NET 修改讲义

文件上传

复习:表单元素-文件 以及 文件上传下载

MVC中没有上传文件的HtmlHelper,只能使用原生的input元素:
    <input type="file" name="icon" multiple accept="image/*" />

注意:可上传文件的表单元素,enctype值必须为:multipart/form-data。

所以,在View中写form,可以直接用HTML原生form标签,或者:

@using(Html.BeginForm("Index", "Register", FormMethod.Post, new { enctype = "multipart/form-data" })) { }

然后,可以通过两种方式上传文件内容:

  1. Request对象
    HttpPostedFileBase icon = Request.Files["icon"];
  2. Action中通过HttpPostedFileBase参数可直接获取:
    [HttpPost]
    public ActionResult Index(HttpPostedFileBase icon)

注意:参数名必须是icon,和input中的name相对应。

HttpPostedFileBase对象

包含有属性:

  • FileName:上传文件的原文件名,可通过文件后缀名判断其类型
    if (Path.GetExtension(icon.FileName) == ".jpg")
  • ContentType:文件类型,通过检测文件中的MIME信息获得
    if (icon.ContentType != "image/png")
  • ContentLength:文件大小(单位bytes)
    if (icon.ContentLength > 500*300)

以及SaveAs()方法:可将上传文件存放到指定位置。(演示)

PS:绝大多数情况,我们都不会(虽然可以)把文件直接存放到数据库中。

icon.SaveAs(Server.MapPath("/Images/qq.jpg"));
注意这里的Server.MapPath()方法,它可以将url格式的path自动“映射”到当前项目所在文件夹位置,非常好用!

web.config

此外,ASP.NET默认限制文件上传最大为5M,如果要允许更大的文件上传,需要在web.config中更改:

<system.webServer>
  <!--IIS 7(包含)以上-->
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="1000" />
    </requestFiltering>
  </security>
</system.webServer>

超过该大小的文件上传,会被IIS拦截,会进入ASP.NET中进行处理。(演示)


动态文件输出

服务器上的静态文件,可以直接通过a标签、img标签等显示/下载(复习:HTML - 链接和图片,但动态生成的文件(比如图形验证码)呢?

return File()

MVC的Action可以通过File()方法返回FileResult的三种子类:

  • FilePathResult(string filename, ...):通过文件路径生成,一般用于权限验证,某些文件只有某种权限的用户才能访问
  • FileContentResult(byte[] content, ...):通过字节流生成
  • FileStreamResult(Stream stream, ...):通过流生成

返回类型都是ActionResult的子类,所以可以:

public ActionResult _Captcha(){

注意他们都要要求一个参数:

contentType

文件类型由此决定,常用的有:

  • 图片:image/*,*是通配符,具体可包括image/png、image/gif、image/jpeg等
  • 文字:text/*,比如text/plain、text/html等

更多类型参考

示例:Captcha

C#进阶:IO和文件操作复制captcha图片的生成代码,

不同的是最后需要将bitmap的放置到MemoryStream中:

MemoryStream stream = new MemoryStream();
image.Save(stream, ImageFormat.Png);

注意:image.Save()到stream之后,其Position被放置到最末尾端,所以如果这时候直接使用:

return File(stream, "image/png");
(依赖于Position的)File()方法就读取不到任何内容。有两种方法可以选择:
  1. 将stream转换成byte[],让Position不起作用:
    return File(stream.ToArray(), "image/png");
  2. 重置stream的Position:
    stream.Seek(0, SeekOrigin.Begin);

这样,/Captcha/Get就可以作为一个url存在,输出的就是一个png图片:

<img src="/Register/_Captcha" />


Cookie

复习:cookie和session

Response生成

ASP.NET Framework为我们准备了HttpCookie对象,使用还是很方便的:

PS:有些版本,(因为包含隐私的立法)需要更改默认的cookie policy等

  • 直接new HttpCookie()生成对象,cookie名为user
    HttpCookie cookie = new HttpCookie("user");
  • 在cookie中添加若干(2个)键值对
    cookie.Values.Add("id", "98");
    cookie.Values.Add("pwd", "1234");
  • 将cookie发送给前端。
    Response.Cookies.Add(cookie);

Response是对HTTP请求响应的封装。

演示:生成的cookie名为user,值为id=98&pwd=1234

接下来,通过

Request.Cookies

就可以获得前端传来的cookies,然后一步一步的获取各个cookie,以及他们的value和values:

HttpCookie cookie = Request.Cookies.Get("user");
string rawValue = cookie.Value;    //id=98&pwd=1234
string id = cookie.Values["id"];   //98
string pwd = cookie.Values["pwd"]; //1234

但是,这里面有一个陷阱:对Request.Cookies的操作不会影响到客户端,比如:

Request.Cookies.Remove("user");

会删除掉user cookie。@想一想@:为什么?

另外,通过Request获得的cookie对象有“不太靠谱”,比如Expires(演示:总是0001/01/01 00:00:00)

cookie.Expires = DateTime.Now.AddDays(14);
要想真正改变cookie,需要把同名(且同domain同path)的cookie发送到客户端。这需要Response.Cookies中处理:
Response.Cookies.Add(cookie);

这个cookie可以从Request.Cookies获取,但更多的时候是直接new一个,再进行各种设置。比如

删除

一个cookie:

HttpCookie cookie = new HttpCookie("user");
//cookie.Path = "/Register";    如果待删除cookie有path的话
cookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(cookie);


Session

ASP.NET Framework的Session也是开箱即用的:

PS:有些版本,需要显式的enable(默认是disabled)

  1. MVC的Controller中自带Session属性
  2. 使用索引器直接取值/赋值,可以直接存放对象。
if (Session[KEY_OF_STUDENT] == null)
{
    Session[KEY_OF_STUDENT] = new Student
    {
        Id = new Random().Next(100)
    };
}
else
{
    Student student = Session[KEY_OF_STUDENT] as Student;
}

演示:

  1. 一旦session被赋值,就会生成一个名为ASP.NET_SessionId的cookie
  2. 同一个浏览器,多次请求,上述cookie值不变,Controller中通过Session获取的Student不变
  3. 不同的浏览器,有不同的cookie值,不同的Session内容

说明:Session依赖于框架自动生成的ASP.NET_SessionId来辨别用户

且Session[KEY_OF_STUDENT]中存放的“是一个而是多个”Student:

配置

演示:Session中Timeout和Mode属性

在web.config的system.web节点下,添加sessionState节点,可以更改上述默认设置:

<sessionState timeout="10" mode="StateServer"></sessionState>

Timeout是session保留的时长,单位分钟,默认是20分钟。

Mode是session存放的位置,默认是InProc。还可以:


存放位置


InProc
IIS进程中内存
最快
不稳定,因为IIS重启/Application pool回收等丢失
StateServer
独立的windows服务
比较快
比较稳定,一般来说只有服务器重启才会丢失
SQLServer
数据库
不快
稳定,在指定的过期时间内不丢失;不担心size,大量session时使用
Off  关闭session功能


mode设为

StateServer

需要:

  1. 任务管理器 - 服务 - 开启aspnet_state服务
  2. Student标记能被序列化:
    [Serializable]
    public class Student

演示:

  • 打开任务管理器查看服务:
  • IIS重启后session仍然存在

数据库(选)

首先需要打开VS的Developer Command Prompt

执行如下命令生成数据库:

aspnet_regsql.exe -S (localdb)\MSSQLLocalDB  -E -ssadd -sstype p
其中:
  • aspnet_regsql.exe:要执行的exe文件
  • -S (localdb)\MSSQLLocalDB:指定要连接的SQL server实例
  • -E -ssadd -sstype p:使用当前用户登录,创建session相关数据库等,详见文档

运行成功,就会生成如下数据库:


最后,在web.config中配置:

    <sessionState timeout="10" mode="SQLServer" 
                  sqlConnectionString="Data Source=(localdb)\MSSQLLocalDB;Integrated Security=True;" >
    </sessionState>

注意:连接字符串中删除掉 Initial Catalog,由ASP.NET自己匹配数据库

演示:生成的session保存在上述数据库表中



#常见面试题:你知道哪些页面传值方式?#

页面之间其实不能传值(复习:HTTP无状态)。

所谓页面传值,指的是把在一个页面(后台)生成的值,传递给另外一个页面(后台)。

所有的“页面传值”都是通过“迂回”的方法:

  • cookie:数据传回客户端,再传到另一个页面
  • session:传送方式同cookie,但数据存放在服务器端
  • query string:数据通过url参数传递
  • hidden field:WebForm的ViewState实现机制,只有当被传值页面向服务器提交时有用
  • TempData:RazorPage和MVC内置,可由cookie(默认)和session实现
  • HttpContext.Items
  • Cache
  • ……



Filter

Filter是ASP.NET面向切口的实现复习:AOP,以供开发人员在HTTP生命周期中特定节点插入自定义的逻辑。

MVC常用的时间节点有这些:


利用Filter的方式有两种:

FilterAttribute

MVC为我们内置了一个FilterAttribute(继承自Attribute,所以可以当做特性使用),下面再派生出若干子类,常用的有:

  • AuthorizeAttribute,还实现IAuthorizationFilter
  • ActionFilterAttribute,还实现IActionFilter和IResultFilter
  • HandleErrorAttribute,实现IExceptionFilter

通常我们只需要继承这些子类就可以了:

public class ModelValidationAttribute : ActionFilterAttribute{}
public class ErrorLogAttribute : HandleErrorAttribute{}
public class NeedLogOnAttribute : AuthorizeAttribute{}

通过override父类的方法,就可以实现我们自己的逻辑。

这些Filter既可以放在Action上,也可以放在Controller上(源代码:AttributeUsage)

  • Controller:作用于该Controller下所有的Action
  • Action:作用于该Action(包括ChildAction
[HttpGet]
[NeedLogOn]
[ModelValidation]
[ErrorLog]
public ActionResult Index()

执行顺序

演示:Filter的执行顺序

  1. AuthorizeAttribute.OnAuthorization()
  2. ActionFilterAttribute.OnActionExecuting()
  3. 运行Action()
  4. ActionFilterAttribute.OnActionExecuted()
  5. ActionFilterAttribute.OnResultExecuting()
  6. 运行View()
  7. ActionFilterAttribute.OnResultExecuted()
  8. HandleErrorAttribute

同一种类的Attribute,可以使用Order属性来设置执行顺序

[ModelValidation(Order=100)]

@想一想@:当出现ChildAction的嵌套时,执行顺序是怎么样的?(依次类推)

参数filterContext

所有的“Filter(override)方法”都提供了这个参数(ControllerContext的不同子类),通过它大量的“上下文信息”(context)供我们编程使用:

  • HttpContext:可以.出Request、Response、Session等
  • Controller:类型为ControllerBase,所以可以直接获取ViewData/ViewBag、TempData等,但如果要获取ModelState等,需要:
    (filterContext.Controller as Controller).ModelState
  • ActionDescriptor:ActionName
  • RouteData:
还可以使用属性Result返回指定的页面:
filterContext.Result = new RedirectResult("/Log/On");

另外,注意几个有用的方法/属性:

  • filterContext.HttpContext.Request.IsAjaxRequest():需要对Ajax请求特殊处理时非常有用
  • filterContext.ParentActionViewContext:ChildAction需要父Action信息时有用

全局注册

首先要声明一个类,实现Filter接口(见上文):

public class ContextPerRequest : IActionFilter

然后在App_Start文件夹FilterConfig.RegisterGlobalFilters()中注册:

filters.Add(new ContextPerRequest());

注意

  1. Add()方法参数类型为object,但是如果参数没有实现Filter接口,运行时会报错
  2. 这个方法是被Global.asax中的Application_Start()调用的,所以只会在项目运行时执行一次(同RouteConfig

Filter接口的执行一样是有顺序的

利用Filter实现PRG

有没有感觉之前的PRG实现“怪怪的”?使用TempData/Merge()在POST和GET方法里搞一堆,性价比也不高嘛。

有没有更简单的解决办法?当然有:

  • 为POST建一个ModelValidationAttribute,如果验证失败的话,往TempData中传入ModelState,并重定向到相应Action
    //ModelStateDictionary state = ((Controller)filterContext.Controller).ModelState;  或者
    ModelStateDictionary state = filterContext.Controller.ViewData.ModelState;
    
    if (!state.IsValid)
    {
        filterContext.Controller.TempData[Const.Other.MODEL_VALIDATE_ERROR] = state;
        filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.Url.PathAndQuery);
    }//else nothing
  • 为GET建一个ImportValidationErrorAttribut,如果TempData有相应的值,导入TempData中的ModelState,并进行Merge
    ModelStateDictionary state = filterContext.Controller.TempData[Const.Other.MODEL_VALIDATE_ERROR] 
        as ModelStateDictionary;
    if (state != null)
    {
        filterContext.Controller.ViewData.ModelState.Merge(state);
    }

@想一想@:能不能把两个Filter合成一个?(可以的)但哪一种方式更好呢?


作业

  1. 利用PRG模式,完成个人资料页面功能,包括分别用UserId和/年/月/日/GUID做文件名,保存用户上传头像,并将其路径录入数据库(页面刷新能看到其头像)
  2. 完成验证码以下功能(JavaScript):
    1. 点击输入框才生成验证码
    2. 可以手动刷新验证码
  3. 能自动检测用户是否阅读网站最新通知:未读,显示;已读,啥都不做。通过cookie实现,比如系统最新通知id为9,当用户进入网站首页:
    • 系统检查是否存在cookie:hasRead={n}
    • 如果没有该cookie,或者n值不等于9,显示该条通知,并将生成/修改该条cookie为hasRead=9
  4. 在第3题的基础上,利用filter,使用户访问任何一个页面都有相同的检查
  5. 用session而不是cookie实现用户登录

学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ASP.NET 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码