ASP.NET MVC 5与EF 6 - 使用文件

本教程探索如何在ASP.NET MVC应用程序中上传文件以及如何使用实体框架将其存储在数据库中。它建立在一个 12系列,其中包括虚构的Contoso大学 这教您如何使用实体框架来构建MVC 5应用程序进行数据访问。原始教程系列是在C#的C#中制作的 里克安德森 (  @rickandmsft.  )微软。本教程在C#中编码。与上一个教程一样,我制作了一个使用Visual Basic语言的版本。这是 可用.

下载代码

可用本节的代码 这里 。 拯救 。压缩 文件到方便的位置,然后提取内容。确保您安装了一个版本的Visual Studio 2013(Express for Web,Community,Professional,Premium或Ultimate),双击 .sln. 文件。项目在IDE中打开后,按 Shift + Ctrl + B 建立解决方案。这将确保从Nuget恢复所有包,并且可能需要一段时间根据您的互联网连接速度。

 

我应该将文件存储在数据库或文件系统中吗?

开发人员论坛中最常见的问题之一,这个难题的答案是"it depends"。两种方法都有利弊,本教程不会寻求增加现有辩论,但将涵盖如何接近两个选项。您将修改学生CRUD方法和视图以存储数据库中的化身图像。然后,您将修改教师CRUD方法和视图以将图像中的图像存储在数据库中的字段系统中及其文件名中。

为文件类型创建文件类型枚举

该应用程序将满足将用于各种目的的文件,其中一些目前尚未知道。您将从将作为学生的化身和指导员的照片,这两种类型的文件将是作为学生的头像。这可能是为每个文件添加到用户实体的新属性。但是,它在软件开发的性质中,有一天,利益相关方将要求修改申请,以满足个人报告,成绩,会议说明和各种其他文件。每次需要满足新文件类型时,您都必须更改用户类并添加新的迁移。相反,您将创建一个枚举以表示每个文件的目的,并将一个属性添加到用户实体以保存文件集合,无论其目的如何。

将新类文件添加到 楷模 文件夹并命名它 filetype.cs. 。用以下内容替换现有代码:

 名称 space ContosoUniversity.Models
{
    public enum FileType
    {
        Avatar = 1, Photo
    }
}

在数据库中存储文件

第一个示例将介绍将文件存储为数据库表中的二进制数据。如果您已经决定将文件存储在文件系统中并希望跳过此部分,请随时导航到教程的第二部分。

创建文件实体

Newer versions of SQL Server offer the FileStream data type for storing file data. Existing versions of Entity Framework (6.1.2 as of the publication date of this tutorial) do not support the FileStream data type via Code First. When you store file content in a database, the content itself is stored in binary format. This is represented as an array of bytes in .NET which Entity Framework (currently) maps to the SQL Server varbinary(max) data type.

  1. 右键点击 楷模 文件夹并选择 添加新项目。添加类文件,名称它 file.cs. 并用以下内容替换现有内容:

    using System.ComponentModel.DataAnnotations;
    
     名称 space ContosoUniversity.Models
    {
        public class File 
        {
            public int FileId { get; set; }
            [StringLength(255)]
            public string FileName { get; set; }
            [StringLength(100)]
            public string ContentType { get; set; }
            public byte[] Content { get; set; }
            public FileType FileType { get; set; }
            public int  人 Id { get; set; }
            public virtual  人  { get; set; }
        }
    }

    The File entity has an virtual property, which is one part of establishing a one-to-many relationship with the class.

    MVC5与EF6.

  2. Complete this association by adding a using directive for System.Collections.Generic and a collection of files as a property to the 班级(突出显示下方):

    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Collections.Generic;
    
     名称 space ContosoUniversity.Models
    {
        public abstract class {
            public int ID { get; set; }
    
            [Required]
            [StringLength(50)]
            [Display(Name = "Last Name")]
            public string LastName { get; set; }
            [Required]
            [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
            [Column("FirstName")]
            [Display(Name = "First Name")]
            public string FirstMidName { get; set; }
    
            [Display(Name = "Full Name")]
            public string FullName
            {
                get
                {
                    return LastName + ", " + FirstMidName;
                }
            }
            public virtual ICollection<File> Files { get; set; }
        }
    }
  3. Now add a DbSet to the SchoolContext class:

    public DbSet<File> Files { get; set; }
  4. 通过将以下键入包管理器控制台(PMC)来添加新迁移:

    add-migration Files
  5. 通过将以下键入PMC来运行迁移。

    update-database

更改学生创建表单和控制器

使突出显示的变化 create.vbhtml. 文件在 观点\学生 folder:

@using (Html.BeginForm("Create", "Student", null, FormMethod.Post, new {enctype = "multipart/form-data"})) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Student</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.FirstMidName, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.FirstMidName, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.FirstMidName, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.EnrollmentDate, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.EnrollmentDate, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.EnrollmentDate, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.Label("Avatar", new {@class = "control-label col-md-2"})
            <div class="col-md-10">
                <input type="file" id="Avatar"  名称 ="upload" />
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

您已添加文件上传到表单:

MVC5与EF6.

You did this by specifying the type as "file" on an input element. However, this is not enough to ensure that uploaded file data is accessible on the server. You also used one of the longer overloads of the Html.BeginForm helper to add an enctype attribute to the form and sets its value to multipart/form-data. This is an essential step to getting file uploading to work.

Now add the highlighted code below to the HttpPost Create method in the StudentsController:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult  创造 ([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student, HttpPostedFileBase upload)
{
    try
    {
        if (ModelState.IsValid)
        {
            if (upload != null && upload.ContentLength > 0)
            {
                var avatar = new File
                {
                    FileName = System.IO.Path.GetFileName(upload.FileName),
                    FileType = FileType.Avatar,
                    ContentType = upload.ContentType
                };
                using (var reader = new System.IO.BinaryReader(upload.InputStream))
                {
                    avatar.Content = reader.ReadBytes(upload.ContentLength);
                }
                student.Files = new List<File> { avatar };
            }
            db.Students.Add(student);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    }
    catch (RetryLimitExceededException /* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
    }
    return View(student);
}

The first change adds a new parameter to the 创造 method's signature - upload, which is an instance of the HttpPostedFileBase type. The parameter name matches the 名称 attribute's value of the input type="file" in the Create form so that the MVC model binder can bind the uploaded file to the parameter that you added to the 创造 method.

The highlighted code block that you added is responsible for extracting the binary data from the request and populating a new File entity instance ready to be added to the student's Files collection. If the user adds a new student without uploading a file, the upload parameter will equate to null, which is why that condition is checked before any attempt to access the HttpPostedFileBase ContentLength property. If you do not perform this check, and then reference the ContentLength property when no file has been uploaded, an exception will be raised. You then check the ContentLength because it is perfectly possible to upload an empty file. If the user does that, the ContentLength property will return 0, and there is no point in storing a file with no data in it. If the upload passes both tests, you create a new File object and assign the relevant properties with values taken from the upload parameter. The binary data is obtained from the InputStream property of the uploaded file, and a BinaryReader object is used to read that into the Content property of the File object. Finally, you add the new File object to the student object.

更改详细信息方法和视图

在数据库中存储了图像,您需要进行一些更改以获取图像并在详细信息视图中显示它。

  1. First, you will use the Include method in the LINQ query that fetches the student from the database to bring back releated files. 该包含方法不支持过滤,所以下面的突出显示的线 全部 无论类型如何,与学生关联的文件。

    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Student student = db.Students.Include(s => s.Files).SingleOrDefault(s => s.ID == id);
        if (student == null)
        {
            return HttpNotFound();
        }
        return View(student);
    }
  2. 将突出显示的行添加到 视图\ web.config. 文件。确保将此添加到 web.config. 在视图目录中的文件 - 而不是根文件夹中的文件。

    < 名称 spaces>
      <add  名称 space="System.Web.Mvc" />
      <add  名称 space="System.Web.Mvc.Ajax" />
      <add  名称 space="System.Web.Mvc.Html" />
      <add  名称 space="System.Web.Optimization"/>
      <add  名称 space="System.Web.Routing" />
      <add  名称 space="ContosoUniversity" />
      <add  名称 space="ContosoUniversity.Models" />
    </ 名称 spaces>
  3. 将突出显示的代码部分添加到 观点\ student \ details.chtml 文件。

    <h4>Student</h4>
    <hr />
    <dl class="dl-horizontal">
        @if(Model.Files.Any(f => f.FileType == FileType.Avatar)){
            <dt>
                Avatar
            </dt>
            <dd>
                <img src="~/File?id=@Model.Files.First(f => f.FileType == FileType.Avatar).FileId" alt="avatar" />
            </dd>
        }
        <dt>
            @Html.DisplayNameFor(model => model.LastName)
        </dt>

你所做的改变 Views\web.config file makes the ContosoUniversity.Model namespace available to your views. It results in you not having to use the fully qualified name to reference items in that namespace, such as the FileType enumeration. If you had not made that change, you would have to do a bit more typing in the view whenever you want to reference types in the Model namespace:

@if(Model.Files.Any(f => f.FileType == ContosoUniversity.Model.FileType.Avatar)){

您返回与学生关联的所有文件(如果有的话),因此必须检查是否有与化身文件类型匹配的任何文件。如果有的话,您呈现第一个文件。使用标准HTML IMG标记呈现图像。

按照惯例,图像映射的URL映射到名为file的控制器的索引方法,以文件的ID传递为查询字符串值。

添加文件控制器

  1. 右键单击控制器文件夹并选择 添加..控制器

  2. 选择 MVC 5控制器 - 空 from the selection.

    MVC5与EF6.

  3. 说出它 filecontroller..

    MVC5与EF6.

  4. 用以下内容替换模板合计代码:

    using ContosoUniversity.DAL;
    using System.Web.Mvc;
    
     名称 space ContosoUniversity.Controllers
    {
        public class filecontroller. : Controller
        {
            private SchoolContext db = new SchoolContext();
            //
            // GET: /File/
            public ActionResult Index(int id)
            {
                 var fileToRetrieve = db.Files.Find(id);
                 return File(fileToRetrieve.Content, fileToRetrieve.ContentType);
            }
        }
    }

    代码基于在查询字符串中传递的ID值获取正确的文件。然后它将图像返回给浏览器 FileResult.

  5. 创建一个新学生,确保选择要上传的图像文件,然后导航到其详细信息以检查结果。

    MVC5与EF6.

您已完成功能以将要存储在数据库中的图像添加到新学生,并将其显示为其详细信息的一部分。下一节介绍如何提供功能以允许用户编辑现有的学生的化身图像。

自定义编辑方法和视图

  1. 改变 code in the HttpGet Edit method in the StudentController to include the retrieval of files as in the highlighted code below.

    // GET: Student/Edit/5
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Student student = db.Students.Include(s => s.Files).SingleOrDefault(s => s.ID == id);
        if (student == null)
        {
            return HttpNotFound();
        }
        return View(student);
    }
  2. 改变 观点\ student \ edit.cshtml file to include the correct enctype attribute in the form, and to both display the existing image and to provide a file upload should the user wish to provide a replacement:

    @using (Html.BeginForm("Edit", "Student", null, FormMethod.Post, new {enctype = "multipart/form-data"}))
    {
        @Html.AntiForgeryToken()
        
        <div class="form-horizontal">
            <h4>Student</h4>
            <hr />
            @Html.ValidationSummary(true, "", new { @class = "text-danger" })
            @Html.HiddenFor(model => model.ID)
    
            <div class="form-group">
                @Html.LabelFor(model => model.LastName, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.LastName, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger" })
                </div>
            </div>
    
            <div class="form-group">
                @Html.LabelFor(model => model.FirstMidName, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.FirstMidName, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.FirstMidName, "", new { @class = "text-danger" })
                </div>
            </div>
    
            <div class="form-group">
                @Html.LabelFor(model => model.EnrollmentDate, htmlAttributes: new { @class = "control-label col-md-2" })
                <div class="col-md-10">
                    @Html.EditorFor(model => model.EnrollmentDate, new { htmlAttributes = new { @class = "form-control" } })
                    @Html.ValidationMessageFor(model => model.EnrollmentDate, "", new { @class = "text-danger" })
                </div>
            </div>
    
            @if(Model.Files.Any(f => f.FileType == FileType.Avatar)) {
                <div class="form-group">
                    <span class="control-label col-md-2"><strong>Current Avatar</strong></span>
                    <div class="col-md-10">
                        <img src="~/File?id=@Model.Files.First(f => f.FileType == FileType.Avatar).FileId" alt="avatar" />
                    </div>
                </div>
            }
    
            <div class="form-group">
                @Html.Label("Avatar", new {@class = "control-label col-md-2"})
                <div class="col-md-10">
                    <input type="file" id="Avatar"  名称 ="upload" />
                </div>
            </div>
    
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Save" class="btn btn-default" />
                </div>
            </div>
        </div>
    }
  3. 使突出显示的变化 HttpPost Edit method as shown below.

    [HttpPost, ActionName("Edit")]
    [ValidateAntiForgeryToken]
    public ActionResult EditPost(int? id, HttpPostedFileBase upload)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        var studentToUpdate = db.Students.Find(id);
        if (TryUpdateModel(studentToUpdate, "",
            new string[] { "LastName", "FirstMidName", "EnrollmentDate" }))
        {
            try
            {
                if (upload != null && upload.ContentLength > 0)
                {
                    if (studentToUpdate.Files.Any(f => f.FileType == FileType.Avatar))
                    {
                        db.Files.Remove(studentToUpdate.Files.First(f => f.FileType == FileType.Avatar));
                    }
                    var avatar = new File
                    {
                        FileName = System.IO.Path.GetFileName(upload.FileName),
                        FileType = FileType.Avatar,
                        ContentType = upload.ContentType
                    };
                    using (var reader = new System.IO.BinaryReader(upload.InputStream))
                    {
                        avatar.Content = reader.ReadBytes(upload.ContentLength);
                    }
                    studentToUpdate.Files = new List<File> { avatar };
                }
                db.Entry(studentToUpdate).State = EntityState.Modified;
                db.SaveChanges();
    
                return RedirectToAction("Index");
            }
            catch (RetryLimitExceededException /* dex */)
            {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
            }
        }
        return View(studentToUpdate);
    }

You altered the signature of the HttpPost Edit method to accept an instance of the HttpPostedFile. As in the 创造 method, this represents the uploaded file. The highlighted code block checks to see if a file was uploaded, and if one was, that it contains data. These are the same checks as you made in the 创造 方法。 If the file contains data, you find any existing files associated with the current student that are Avatar file types. If found, you mark the file for deletion. Then you add the uploaded file as a replacement.

使用文件系统

The next section covers how to store uploaded files in the file system instead of in the database. You will create a FilePath entity to represent a single file.

  1. 为...添加新课程 楷模 文件夹称为 filepath.cs. 并用以下内容替换模板化代码:

     名称 space ContosoUniversity.Models
    {
      using System.ComponentModel.DataAnnotations;  
        public class FilePath
        {
            public int FilePathId {get;set;}
            [StringLength(255)]
            public string FileName {get;set;}
            public FileType FileType {get;set;}
            public int  人 ID {get;set;}
            public virtual  人  {get;set;}
        }
    }
  2. Add a DbSet to the SchoolContext for the new entity:

    public DbSet<FilePath> FilePaths { get; set; }
  3. Add a new navigational property to the class to accommodate a collection of FilePath objects:

    public virtual ICollection<FilePath> FilePaths { get; set; }

    这在人和文件路之间创造了一对多关系:

    MVC5与EF6.

  4. ctrl + shift + b 为确保您的项目成功构建,然后通过将以下键入PMC来为项目添加新迁移:

    add-migration FilePaths
  5. Apply the changes to the database by typing update-database into the PMC.

修改教师创建和详细信息视图和方法

In this example, you will only alter the 创造 and Details methods and views. These alterations cover the core concepts that you need to understand when saving files to the file system.

  1. 将新文件夹添加到名为“图像”的根目录中。

  2. 改变 Html.BeginForm helper in 视图\ Constructor \ create.cshtml to add the correct enctype to the form:

    @using (Html.BeginForm("Create", "Instructor", null, FormMethod.Post, new {enctype = "multipart/form-data"})){
  3. 现在,将文件输入添加到表单本身,刚刚在办公室位置输入之后以及在DIV之前,该课程包含分配的课程复选框:

    <div class="form-group">
         @Html.Label("Photo", new {@class = "control-label col-md-2"})
         <div class="col-md-10">
             <input type="file" id="Photo"  名称 ="upload" />
         </div>
    </div>
  4. 使突出显示的变化 HttpPost Create method in ChinessuroController.cs.:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult  创造 ([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment")]Instructor instructor, string[] selectedCourses, HttpPostedFileBase upload)
    {
        if (selectedCourses != null)
        {
            instructor.Courses = new List<Course>();
            foreach (var course in selectedCourses)
            {
                var courseToAdd = db.Courses.Find(int.Parse(course));
                instructor.Courses.Add(courseToAdd);
            }
        }
        
        if (ModelState.IsValid)
        {
            if (upload != null && upload.ContentLength > 0)
            {
                var photo = new FilePath
                {
                    FileName = System.IO.Path.GetFileName(upload.FileName),
                    FileType = FileType.Photo
                };
                instructor.FilePaths = new List<FilePath>();
                instructor.FilePaths.Add(photo);
            }
            db.Instructors.Add(instructor);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        PopulateAssignedCourseData(instructor);
        return View(instructor);
    }

You added an additional parameter to the 创造 method's signature - upload of type HttpPostedFileBase. This is mapped to the file upload based on the parameter's name matching the 名称 attribute on the input type=file that you added in the Create view, and provides access to the properties of the uploaded file. The added code checks to ensure that the upload is not null and then checks its ContentLength property to ensure that an empty file as not uploaded. If both of those checks succeed, a new FilePath object is created and its FileName property is assigned the value of the uploaded file's FileName, which is obtained by the System.IO.Path.GetFileName 方法。

笔记: Some browsers provide more than just the file name when files are uploaded. Versions of Internet Explorer will provide the full client file path when you use it locally, i.e in development and test. Other browsers may or may not prepend the file name with values such as C:\FakePath\. In any event, you cannot rely on a browser providing just the file's name. Equally, you cannot rely on the browser to provide the original path of the uploaded file. Only some versions of Internet Explorer (currently) do this, and even then, only when your client and server are the same machine.

使用从上载作为文件名的值将文件本身保存到图像文件夹中。

笔记: it is not always a good idea to use the file's original name when you store uploads in the file system. Two files with the same name cannot exist in the same location so if a new file is uploaded and saved with the same name as an existing one, the original file will be overwritten when the new one is saved. A common resolution to this problem is to rename the file prior to saving it. Typically, a Guid is used as a file name on the basis that it can pretty much guarantee uniqueness. If you need to adopt this strategy, you can replace the existing line that assigns the FilePath object's FileName property with the following highlighted line of code:

var photo = new FilePath
{
    FileName = Guid.NewGuid().ToString() + System.IO.Path.GetExtension(upload.FileName),
    FileType = FileType.Photo
};

修改教练细节方法和视图

  1. Make the highlighted amendment to the Details method in ChinessuroController.cs.:

    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        Instructor instructor = db.Instructors.Include(i => i.FilePaths).SingleOrDefault(i => i.ID == id);
        if (instructor == null)
        {
            return HttpNotFound();
        }
        return View(instructor);
    }
  2. 最后,将以下代码添加到 观点\ constructor \ details.chtml file just before the closing </dl> tag at the bottom

    @if(Model.FilePaths.Any(f => f.FileType == FileType.Photo))
    {
        <dt>
            Photo
        </dt>
        <dd>
            <img src="~/images/@Model.FilePaths.First(f => f.FileType == FileType.Photo)).FileName" alt="" />
        </dd>
    }

The alteration that you made to the Details method in the controller ensures that any filepath objects belonging to the instructor are retrieved along with the rest of their details. The code that you added to the View checks to see if there are any filepath objects matching the FileType.Photo type, and if there are, the first one is displayed using an img element. The src attribute value is generated by concatenating the location where files are saved with the name that was given to this particular file. Notice that you didn't include the folder name as part of the value that you used for the FileName property. Doing so may make things more diffcult if you ever needed to change the file storage location.

概括

在本教程中,您了解如何使用实体框架在MVC应用程序中使用文件。您探讨了两种持续文件信息的方法:将文件数据存储在数据库中;并将文件存储在文件系统中。