在 ASP.NET Core MVC 上传文件

Author Image
admin Sunday, May 10, 2020 阅读数: 227

本文作者:梁桐铭- 微软最有价值专家(Microsoft MVP)
本文出自《从零开始学 ASP.NET Core 与 EntityFramework Core》目录
视频课程效果更佳:跨平台开发实战掌握 ASP.NET Core 与 EntityFramework Core

在 ASP.NET Core MVC 上传文件

在本章节中,我们将做一个案例如何使用 ASP.NET Core MVC 上传文件。我们会完善我们的创建学生的表单信息。

我们会实现下面的功能,在创建学生信息的时候,可以使用表单中的图像字段来上传学生的照片信息。

images

提交信息后,我们能够在数据库Students表中,存储 Student 类的数据,包含Name,Email,Major,PhotoPath信息。

下面是数据库 Students 表的信息,它是通过 ASP.NET Core 中的迁移功能创建的 。

Id 名字 电子邮件 主修科目 图片路径
1 52ABP管理员 2 info@ddxc.org info.png
2 梁桐铭 3 ltm@ddxc.org ltm.png

学生的照片信息,会上传到 Web 服务器上的wwwroot/images文件夹中。 为了实现这个上传功能,我们需要添加修改和添加以下几个类文件。

学生模型类

 public class Student
    {
         public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public MajorEnum? Major { get; set; }
        public string PhotoPath { get; set; }
    }

Student 类文件中的各种验证属性已经移除掉了,因为从现在开始它的职责更多的就是负责同步数据库的架构。而基于添加学生信息所需要进行的业务规则的验证,我们需要添加一个新的类文件,叫做StudentCreateViewModel.cs,在我们之前的课程中提到过,很多时候领域模型是不满足我们的业务要求的,所以需要使用视图模型也被称作 DTO,来帮助我们完善业务。

StudentCreateViewModel 文件

 public class StudentCreateViewModel
    {

        [Required(ErrorMessage = "请输入名字"), MaxLength(50, ErrorMessage = "名字的长度不能超过50个字符")]
        [Display(Name = "名字")]
        public string Name { get; set; }
        [Required]
        [Display(Name = "主修科目")]
        public MajorEnum? Major { get; set; }

        [Display(Name = "电子邮件")]
        [RegularExpression(@"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
        ErrorMessage = "邮箱的格式不正确")]
        [Required(ErrorMessage = "请输入邮箱地址")]
        public string Email { get; set; }

        [Display(Name = "图像")]
        public IFormFile Photo { get; set; }
    }

在我们的StudentCreateViewModel文件中添加了一个类型IFormFile的 Photo 的字段。

  • IFormFile 位于Microsoft.AspNetCore.Http命名空间中。
  • 上传至服务器的文件可通过 IFormFile 接口通过模型绑定的形式进行访问。
  • Photos 属性通过模型绑定接收上传的文件
  • 如果要支持多个文件上传,我们将 Photos 属性的数据类型设置为 List即可
  • 接口 IFormFile 具有以下属性和方法
  public interface IFormFile
  {
    string ContentType { get; }
    string ContentDisposition { get; }
    IHeaderDictionary Headers { get; }
    long Length { get; }
    string Name { get; }
    string FileName { get; }
    Stream OpenReadStream();
    void CopyTo(Stream target);
    Task CopyToAsync(Stream target, CancellationToken cancellationToken = null);
  }

名称 内容
ContentType 获取上传文件的原始Content-Type标头。
ContentDisposition 获取上传文件的原始Content-Disposition标头。
Length 获取文件长度,以字节为单位。
FileName 从Content-Disposition标头中获取文件名。
Name 从Content-Disposition标头中获取的字段名称。
Headers 获取上传文件的HTTP消息头的字典信息。
OpenReadStream 打开请求流以读取上载的文件。
CopyTo 将上传文件的内容复制到流
CopyToAsync 异步地将上传文件的内容复制到流

以上就是这些属性以及方法的说明。

更新 Create 视图的代码

@model StudentCreateViewModel
@{ ViewBag.Title = "创建学生信息"; }

要支持文件上传,请设置表单元素为enctype="multipart/form-data"

  • enctype 属性规定在发送到服务器之前应该如何对表单数据进行编码。
  • application/x-www-form-urlencoded 在发送前编码所有字符(默认)
  • multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
  • text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 以上都是 Hhtml 的基础。
<form
  enctype="multipart/form-data"
  asp-controller="home"
  asp-action="create"
  method="post"
  class="mt-3"
>
  <div class="form-group row">
    <label asp-for="Name" class="col-sm-2 col-form-label"></label>
    <div class="col-sm-10">
      <input asp-for="Name" class="form-control" placeholder="请输入名字" />
      <span asp-validation-for="Name" class="text-danger"></span>
    </div>
  </div>

  <div class="form-group row">
    <label asp-for="Email" class="col-sm-2 col-form-label"></label>
    <div class="col-sm-10">
      <input
        asp-for="Email"
        class="form-control"
        placeholder="请输入邮箱地址"
      />
      <span asp-validation-for="Email" class="text-danger"></span>
    </div>
  </div>

  <div class="form-group row">
    <label asp-for="Major" class="col-sm-2 col-form-label"></label>
    <div class="col-sm-10">
      <select
        asp-for="Major"
        class="custom-select mr-sm-2"
        asp-items="Html.GetEnumSelectList<MajorEnum>()"
      >
        <option value=""> 请选择</option>
      </select>
      <span asp-validation-for="Major" class="text-danger"></span>
    </div>
  </div>

  @* 我们使用了asp-for的taghelper设置input的属性为"Photo"。
  "Photo"属性类型是IFormFile, 所以在运行的时候ASP.NET 
  Core会将该标签生成上传控件(input type=file) *@

  <div class="form-group row">
    <label asp-for="Photo" class="col-sm-2 col-form-label"></label>
    <div class="col-sm-10">
      <div class="custom-file">
        <input asp-for="Photo" class="form-control custom-file-input" />
        <label class="custom-file-label">请选择照片...</label>
      </div>
    </div>
  </div>

  <div asp-validation-summary="All" class="text-danger"></div>
  <div class="form-group row">
    <div class="col-sm-10">
      <button type="submit" class="btn btn-primary">创建</button>
    </div>
  </div>
  @*以下JavaScript代码的作用是,可以在上传标签中显示选定的上传文件名称。*@
  @section Scripts {
  <script>
    $(document).ready(function() {
      $(".custom-file-input").on("change", function() {
        var fileName = $(this)
          .val()
          .split("\\")
          .pop();
        $(this)
          .next(".custom-file-label")
          .html(fileName);
      });
    });
  </script>
  }
</form>

我们使用了 asp-for 的 taghelper 标签,将设置 input 的属性为"Photo"。而"Photo"属性类型是 IFormFile, 所以在 ASP.NET Core 运行的时候会自动将该标签生成为上传控件(input type=file),参考以下代码:

<input
  class="form-control custom-file-input"
  type="file"
  id="Photo"
  name="Photo"
/>
  • 因为原有的Student领域模型已经不满足我们的业务呈现了,所以当前页面视图模型改用StudentCreateViewModel

  • 在结束form标签之前的 JavaScript 代码的作用是,可以增强上传控件,可以显示选定的上传文件名称。

修改 Create 操作方法

我们回到HomeController文件中,因为要让 Create()方法支持文件上传,所以我们要对这个方法进行修改。修改规则如下:

  • 判断用户是否上传了图片,如果没有上传图片路径信息为空。
    • 所以我们要判断StudentCreateViewModel中的Photo属性是否为空
  • 如果有上传图片,则要进行规则验证。
  • 所有的图片都必须上传到 wwwroot 中的 images 文件夹中。
    • 而要获取wwwroot文件夹的路径,我们需要通过 ASP.NET Core 中的依赖注入注册 HostingEnvironment服务
  • 为了确保文件名是唯一的,文件名的生成规则为 GUID 值加一个下划线。

完整的上传文件代码如下:

[HttpPost]
        public IActionResult Create(StudentCreateViewModel model)
        {
            if (ModelState.IsValid)
            {
                string uniqueFileName = null;

  //如果传入模型对象中的Photo属性不为null,则表示该用户选择了要上传的图片信息。
                if (model.Photo != null)
                {
//必须将图像上传到wwwroot中的images文件夹
//而要获取wwwroot文件夹的路径,我们需要注入 ASP.NET  Core提供的HostingEnvironment服务
       string uploadsFolder = Path.Combine(hostingEnvironment.WebRootPath, "images");
  //为了确保文件名是唯一的,我们在文件名后附加一个新的GUID值和一个下划线
      uniqueFileName = Guid.NewGuid().ToString() + "_" + model.Photo.FileName;
     string filePath = Path.Combine(uploadsFolder, uniqueFileName);
  //使用IFormFile接口提供的CopyTo()方法将文件复制到wwwroot/images文件夹
                    model.Photo.CopyTo(new FileStream(filePath, FileMode.Create));
                }

                Student newStudent = new Student
                {
                    Name = model.Name,
                    Email = model.Email,
                    Major = model.Major,
// 将文件名保存在student对象的PhotoPath属性中。
//它将保存到数据库 Students的 表中
                    PhotoPath = uniqueFileName
                };

                _studentRepository.Add(newStudent);
                return RedirectToAction("details", new { id = newStudent.Id });
            }

            return View();
        }

HomeController文件中的其余代码不需要上传文件。涉及到图片显示的视图页面为学生详情页面学生列表页面,所以去修改对应的代码地方即可。

学生详情视图页面代码

@model HomeDetailsViewModel
@{

    ViewBag.Title = "学生详情页";
 var photoPath = "~/images/" + (Model.Student.PhotoPath ?? "noimage.jpg");
}
    <div class="row justify-content-center m-3">
        <div class="col-sm-8">
            <div class="card">
                <div class="card-header">
                    <h1>@Model.Student.Name</h1>
                </div>

                <div class="card-body text-center">
                   <img class="card-img-top" src="@photoPath" asp-append-version="true"/>

                    <h4>学生ID : @Model.Student.Id</h4>
                    <h4>邮箱 : @Model.Student.Email</h4>
                    <h4>主修科目名称 : @Model.Student.Major</h4>
                </div>
                <div class="card-footer text-center">
                    <a  asp-action="Index" asp-controller="home" class="btn btn-primary">返回</a>
                    <a href="#" class="btn btn-primary">编辑</a>
                    <a href="#" class="btn btn-danger">删除</a>
                </div>
            </div>
        </div>
    </div>
@section Scripts{
    <script src="~/js/CustomScript.js"></script>
}

视图详情页面需要显示我们的学生图片信息,所以我们声明一个photoPath属性来拼接我们的图片路径完整地址。 将原本的<img class="card-img-top" src="~/images/timg.jpg"/>修改为

<img class="card-img-top" src="@photoPath" asp-append-version="true" />

同时给 img 标签加上 asp-append-version="true"为我们的图片提供缓存服务。

##学生列表视图页面代码

同样的学生列表视图页面,我们也只需要加上一个photoPath属性即可。和详情页面不同的是,因为列表页面存在多个不同的内容,所以photoPath不能设置为全局的,我们需要在 foreach 方法中添加以下代码。

var photoPath = "~/images/" + (student.PhotoPath ?? "noimage.jpg");

同样的将原来的纯静态网页

<img class="card-img-top" src="~/images/timg.jpg" asp-append-version="true" />

替换为

<img
  class="card-img-top imageThumbnail"
  src="@photoPath"
  asp-append-version="true"
/>

同时为 img 标签的 class 属性添加了一个imageThumbnail自定义样式

最终完整的学生列表页面代码如下:

@model IEnumerable<Student>
@{

    ViewBag.Title = "学生列表页面";
}

<div class="card-deck">
    @foreach (var student in Model)
    {
        var photoPath = "~/images/" + (student.PhotoPath ?? "noimage.jpg");

    <div class="card m-3">
        <div class="card-header">
            <h3>@student.Name</h3>
        </div>
        <img class="card-img-top imageThumbnail" src="@photoPath"
             asp-append-version="true"/>

        <div class="card-footer text-center">

            <a asp-controller="home" asp-action="details" asp-route-id="@student.Id" class="btn btn-primary m-1">查看</a>


            <a href="#" class="btn btn-primary m-1">编辑</a>
            <a href="#" class="btn btn-danger m-1">删除</a>
        </div>
    </div>
    }
</div>

最后我们到 wwwroot 文件夹中的 css 文件夹中的 site.css添加以下样式内容:

.imageThumbnail {
  height: 200px;
  width: auto;
}

文章说明

如果您觉得我的文章质量还不错,欢迎打赏,也可以订阅我的视频哦
未得到授权不得擅自转载本文内容,52abp.com 保留版权
感谢您对我的支持

关注微信公众号:角落的白板报

公众号:角落的白板报