本部分演示使用 Razor 頁面上傳文件。
本教程中的 Razor 頁面 Movie 示例應用使用簡單的模型綁定上傳文件,非常適合上傳小型文件。 有關流式傳輸大文件的信息,請參閱通過流式傳輸上傳大文件。
在下列步驟中,向示例應用添加電影計划文件上傳功能。 每個電影計划由一個 Schedule 類表示。 該類包括兩個版本的計划。 其中一個版本 (PublicSchedule) 提供給客戶。 另一個版本 (PrivateSchedule) 用於公司員工。 每個版本作為單獨的文件進行上傳。 本教程演示如何通過單個 POST 將兩個文件上傳至服務器。
添加 FileUpload 類
創建 Razor 頁以處理一對文件上傳。 添加 FileUpload 類(此類與頁面綁定以獲取計划數據)。 右鍵單擊“Models”文件夾。 選擇“添加” > “類”。 將類命名為“FileUpload”,並添加以下屬性:
using Microsoft.AspNetCore.Http; using System.ComponentModel.DataAnnotations; namespace RazorPagesMovie.Models { public class FileUpload { [Required] [Display(Name="Title")] [StringLength(60, MinimumLength = 3)] public string Title { get; set; } [Required] [Display(Name="Public Schedule")] public IFormFile UploadPublicSchedule { get; set; } [Required] [Display(Name="Private Schedule")] public IFormFile UploadPrivateSchedule { get; set; } } }
此類有一個屬性對應計划標題,另各有一個屬性對應計划的兩個版本。 3 個屬性皆為必需屬性,標題長度必須為 3-60 個字符。
添加用於上傳文件的 helper 方法
為避免處理未上傳計划文件時出現代碼重復,請首先上傳一個靜態 helper 方法。 在此應用中創建一個“Utilities”文件夾,然后在“FileHelpers.cs”文件中添加以下內容。 helper 方法 ProcessFormFile 接受 IFormFile 和 ModelStateDictionary,並返回包含文件大小和內容的字符串。 檢查內容類型和長度。 如果文件未通過驗證檢查,將向 ModelState 添加一個錯誤。
using System; using System.ComponentModel.DataAnnotations; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using RazorPagesMovie.Models; namespace RazorPagesMovie.Utilities { public class FileHelpers { public static async Task<string> ProcessFormFile(IFormFile formFile, ModelStateDictionary modelState) { var fieldDisplayName = string.Empty; // Use reflection to obtain the display name for the model // property associated with this IFormFile. If a display // name isn't found, error messages simply won't show // a display name. MemberInfo property = typeof(FileUpload).GetProperty(formFile.Name.Substring(formFile.Name.IndexOf(".") + 1)); if (property != null) { var displayAttribute = property.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute; if (displayAttribute != null) { fieldDisplayName = $"{displayAttribute.Name} "; } } // Use Path.GetFileName to obtain the file name, which will // strip any path information passed as part of the // FileName property. HtmlEncode the result in case it must // be returned in an error message. var fileName = WebUtility.HtmlEncode(Path.GetFileName(formFile.FileName)); if (formFile.ContentType.ToLower() != "text/plain") { modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) must be a text file."); } // Check the file length and don't bother attempting to // read it if the file contains no content. This check // doesn't catch files that only have a BOM as their // content, so a content length check is made later after // reading the file's content to catch a file that only // contains a BOM. if (formFile.Length == 0) { modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) is empty."); } else if (formFile.Length > 1048576) { modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) exceeds 1 MB."); } else { try { string fileContents; // The StreamReader is created to read files that are UTF-8 encoded. // If uploads require some other encoding, provide the encoding in the // using statement. To change to 32-bit encoding, change // new UTF8Encoding(...) to new UTF32Encoding(). using ( var reader = new StreamReader( formFile.OpenReadStream(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), detectEncodingFromByteOrderMarks: true)) { fileContents = await reader.ReadToEndAsync(); // Check the content length in case the file's only // content was a BOM and the content is actually // empty after removing the BOM. if (fileContents.Length > 0) { return fileContents; } else { modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) is empty."); } } } catch (Exception ex) { modelState.AddModelError(formFile.Name, $"The {fieldDisplayName}file ({fileName}) upload failed. " + $"Please contact the Help Desk for support. Error: {ex.Message}"); // Log the exception } } return string.Empty; } } }
以上內容如果看不懂就不用看了,核心思想就是檢查上傳文件是否合乎要求。
將文件保存到磁盤(拓展內容,本案例未用到)
示例應用將文件內容保存到數據庫字段。 若要將文件內容保存到磁盤,請使用文件流:
using (var fileStream = new FileStream(filePath, FileMode.Create)) { await formFile.CopyToAsync(fileStream); }
將文件保存到 Azure Blob 存儲(拓展內容,本案例未用到)
若要將文件內容上傳到 Azure Blob 存儲,請參閱使用 .NET 的 Azure Blob 存儲入門。 本主題演示如何使用UploadFromStream 將文件流保存到 blob 存儲。
添加 Schedule 類
右鍵單擊“Models”文件夾。 選擇“添加” > “類”。 將類命名為“Schedule”,並添加以下屬性:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace RazorPagesMovie.Models { public class Schedule { public int ID { get; set; } public string Title { get; set; } public string PublicSchedule { get; set; } [Display(Name = "Public Schedule Size (bytes)")] [DisplayFormat(DataFormatString = "{0:N1}")] public long PublicScheduleSize { get; set; } public string PrivateSchedule { get; set; } [Display(Name = "Private Schedule Size (bytes)")] [DisplayFormat(DataFormatString = "{0:N1}")] public long PrivateScheduleSize { get; set; } [Display(Name = "Uploaded (UTC)")] [DisplayFormat(DataFormatString = "{0:F}")] public DateTime UploadDT { get; set; } } }
此類使用 Display 和 DisplayFormat 特性,呈現計划數據時,這些特性會生成友好型的標題和格式。
更新 MovieContext
在 MovieContext (Models/MovieContext.cs) 中為計划指定 DbSet:
using Microsoft.EntityFrameworkCore; namespace RazorPagesMovie.Models { public class MovieContext:DbContext { public MovieContext(DbContextOptions<MovieContext> options) : base(options) { } public DbSet<Movie> Movie { get; set; } public DbSet<Class> Class { get; set; } public DbSet<Schedule> Schedule { get; set; } } }
將 Schedule 表添加到數據庫
打開包管理器控制台 (PMC):“工具” > “NuGet 包管理器” > “包管理器控制台”。

在 PMC 中執行以下命令。 這些命令將向數據庫添加 Schedule 表:
Add-Migration AddScheduleTable
Update-Database
添加文件上傳 Razor 頁面
在“Pages”文件夾中創建“Schedules”文件夾。 在“Schedules”文件夾中,創建名為“Index.cshtml”的頁面,用於上傳具有如下內容的計划:
@page @model RazorPagesMovie.Pages.Schedule.IndexModel @{ ViewData["Title"] = "Schedules"; } <h2>Schedules</h2> <hr /> <h3>Upload Schedules</h3> <div class="row"> <div class="col-md-4"> <form method="post" enctype="multipart/form-data"> <div class="form-group"> <label asp-for="FileUpload.Title" class="control-label"></label> <input asp-for="FileUpload.Title" type="text" class="form-control" /> <span asp-validation-for="FileUpload.Title" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="FileUpload.UploadPublicSchedule" class="control-label"></label> <input asp-for="FileUpload.UploadPublicSchedule" type="file" class="form-control" style="height:auto" /> <span asp-validation-for="FileUpload.UploadPublicSchedule" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="FileUpload.UploadPrivateSchedule" class="control-label"></label> <input asp-for="FileUpload.UploadPrivateSchedule" type="file" class="form-control" style="height:auto" /> <span asp-validation-for="FileUpload.UploadPrivateSchedule" class="text-danger"></span> </div> <input type="submit" value="Upload" class="btn btn-default" /> </form> </div> </div> <h3>Loaded Schedules</h3> <table class="table"> <thead> <tr> <th></th> <th> @Html.DisplayNameFor(model => model.Schedule[0].Title) </th> <th> @Html.DisplayNameFor(model => model.Schedule[0].UploadDT) </th> <th class="text-center"> @Html.DisplayNameFor(model => model.Schedule[0].PublicScheduleSize) </th> <th class="text-center"> @Html.DisplayNameFor(model => model.Schedule[0].PrivateScheduleSize) </th> </tr> </thead> <tbody> @foreach (var item in Model.Schedule) { <tr> <td> <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a> </td> <td> @Html.DisplayFor(modelItem => item.Title) </td> <td> @Html.DisplayFor(modelItem => item.UploadDT) </td> <td class="text-center"> @Html.DisplayFor(modelItem => item.PublicScheduleSize) </td> <td class="text-center"> @Html.DisplayFor(modelItem => item.PrivateScheduleSize) </td> </tr> } </tbody> </table> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
每個窗體組包含一個 <label>,它顯示每個類屬性的名稱。 FileUpload 模型中的 Display 特性提供這些標簽的顯示值。例如,UploadPublicSchedule 特性的顯示名稱通過 [Display(Name="Public Schedule")] 進行設置,因此呈現窗體時會在此標簽中顯示“Public Schedule”。
每個窗體組包含一個驗證 <span>。 如果用戶輸入未能滿足 FileUpload 類中設置的屬性特性,或者任何 ProcessFormFile方法文件檢查失敗,則模型驗證會失敗。 模型驗證失敗時,會向用戶呈現有用的驗證消息。 例如,Title 屬性帶有 [Required] 和 [StringLength(60, MinimumLength = 3)] 注釋。 用戶若未提供標題,會接收到一條指示需要提供值的消息。如果用戶輸入的值少於 3 個字符或多於 60 個字符,則會接收到一條指示值長度不正確的消息。 如果提供不含內容的文件,則會顯示一條指示文件為空的消息。
添加頁面模型
將頁面模型 (Index.cshtml.cs) 添加到“Schedules”文件夾中:
using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using RazorPagesMovie.Models; using RazorPagesMovie.Utilities; namespace RazorPagesMovie.Pages.Schedule { public class IndexModel : PageModel { private readonly RazorPagesMovie.Models.MovieContext _context; public IndexModel(RazorPagesMovie.Models.MovieContext context) { _context = context; } [BindProperty] public FileUpload FileUpload { get; set; } public IList<Models.Schedule> Schedule { get; private set; } public async Task OnGetAsync() { Schedule = await _context.Schedule.AsNoTracking().ToListAsync(); } public async Task<IActionResult> OnPostAsync() { // Perform an initial check to catch FileUpload class // attribute violations. if (!ModelState.IsValid) { Schedule = await _context.Schedule.AsNoTracking().ToListAsync(); return Page(); } var publicScheduleData = await FileHelpers.ProcessFormFile(FileUpload.UploadPublicSchedule, ModelState); var privateScheduleData = await FileHelpers.ProcessFormFile(FileUpload.UploadPrivateSchedule, ModelState); // Perform a second check to catch ProcessFormFile method // violations. if (!ModelState.IsValid) { Schedule = await _context.Schedule.AsNoTracking().ToListAsync(); return Page(); } var schedule = new Models.Schedule() { PublicSchedule = publicScheduleData, PublicScheduleSize = FileUpload.UploadPublicSchedule.Length, PrivateSchedule = privateScheduleData, PrivateScheduleSize = FileUpload.UploadPrivateSchedule.Length, Title = FileUpload.Title, UploadDT = DateTime.UtcNow }; _context.Schedule.Add(schedule); await _context.SaveChangesAsync(); return RedirectToPage("./Index"); } } }
頁面模型(Index.cshtml.cs 中的 IndexModel)綁定 FileUpload 類:
[BindProperty] public FileUpload FileUpload { get; set; }
此模型還使用計划列表 (IList<Schedule>) 在頁面上顯示數據庫中存儲的計划:
public IList<Models.Schedule> Schedule { get; private set; }
頁面加載 OnGetAsync 時,會從數據庫填充 Schedules,用於生成已加載計划的 HTML 表:
public async Task OnGetAsync() { Schedule = await _context.Schedule.AsNoTracking().ToListAsync(); }
將窗體發布到服務器時,會檢查 ModelState。 如果無效,會重新生成 Schedule,且頁面會呈現一個或多個驗證消息,陳述頁面驗證失敗的原因。 如果有效,FileUpload 屬性將用於“OnPostAsync”中,以完成兩個計划版本的文件上傳,並創建一個用於存儲數據的新 Schedule 對象。 然后會將此計划保存到數據庫:
public async Task<IActionResult> OnPostAsync() { // Perform an initial check to catch FileUpload class // attribute violations. if (!ModelState.IsValid) { Schedule = await _context.Schedule.AsNoTracking().ToListAsync(); return Page(); } var publicScheduleData = await FileHelpers.ProcessFormFile(FileUpload.UploadPublicSchedule, ModelState); var privateScheduleData = await FileHelpers.ProcessFormFile(FileUpload.UploadPrivateSchedule, ModelState); // Perform a second check to catch ProcessFormFile method // violations. if (!ModelState.IsValid) { Schedule = await _context.Schedule.AsNoTracking().ToListAsync(); return Page(); } var schedule = new Models.Schedule() { PublicSchedule = publicScheduleData, PublicScheduleSize = FileUpload.UploadPublicSchedule.Length, PrivateSchedule = privateScheduleData, PrivateScheduleSize = FileUpload.UploadPrivateSchedule.Length, Title = FileUpload.Title, UploadDT = DateTime.UtcNow }; _context.Schedule.Add(schedule); await _context.SaveChangesAsync(); return RedirectToPage("./Index"); }
鏈接文件上傳 Razor 頁面
打開“_Layout.cshtml”,然后向導航欄添加一個鏈接以訪問文件上傳頁面:
<div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li><a asp-page="/Index">Home</a></li> <li><a asp-page="/About">About</a></li> <li><a asp-page="/Contact">Contact</a></li> <li><a asp-page="/Product">產品展示</a></li> <li><a asp-page="/Movies/Index">RpMovie</a></li> <li><a asp-page="/Schedule/Index">文件上傳</a></li> </ul> </div>
現在可以運行看一下效果了

這里只能上傳文本文件,還要注意.txt文件必須為Unicode文件類型,否則文件中帶有漢字的話,上傳后會提示錯誤消息
Error: Unable to translate bytes [D7] at index 0 from specified code page to Unicode.
解決方法:FileHelpers.cs文件中修改讀取字符編碼的格式為
using ( var reader = new StreamReader( formFile.OpenReadStream(),
new UTF32Encoding(false, false),//使用此字符編碼 //new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true),//原字符編碼 detectEncodingFromByteOrderMarks: true))

修改后上傳文件中包含中文字符就沒問題了。
添加計划刪除確認頁面
用戶單擊刪除計划時,為其提供取消此操作的機會。 向“Schedules”文件夾添加刪除確認頁面 (Delete.cshtml):
@page "{id:int}" @model RazorPagesMovie.Pages.Schedule.DeleteModel @{ ViewData["Title"] = "Delete Schedule"; } <h2>Delete Schedule</h2> <h3>Are you sure you want to delete this?</h3> <div> <h4>Schedule</h4> <hr /> <dl class="dl-horizontal"> <dt> @Html.DisplayNameFor(model => model.Schedule.Title) </dt> <dd> @Html.DisplayFor(model => model.Schedule.Title) </dd> <dt> @Html.DisplayNameFor(model => model.Schedule.PublicScheduleSize) </dt> <dd> @Html.DisplayFor(model => model.Schedule.PublicScheduleSize) </dd> <dt> @Html.DisplayNameFor(model => model.Schedule.PrivateScheduleSize) </dt> <dd> @Html.DisplayFor(model => model.Schedule.PrivateScheduleSize) </dd> <dt> @Html.DisplayNameFor(model => model.Schedule.UploadDT) </dt> <dd> @Html.DisplayFor(model => model.Schedule.UploadDT) </dd> </dl> <form method="post"> <input type="hidden" asp-for="Schedule.ID" /> <input type="submit" value="Delete" class="btn btn-default" /> | <a asp-page="./Index">Back to List</a> </form> </div>
頁面模型 (Delete.cshtml.cs) 在請求的路由數據中加載由 id 標識的單個計划。 將“Delete.cshtml.cs”文件添加到“Schedules”文件夾:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; namespace RazorPagesMovie.Pages.Schedule { public class DeleteModel : PageModel { private readonly RazorPagesMovie.Models.MovieContext _context; public DeleteModel(RazorPagesMovie.Models.MovieContext context) { _context = context; } [BindProperty] public Models.Schedule Schedule { get; set; } public async Task<IActionResult> OnGetAsync(int? id) { if (id == null) { return NotFound(); } Schedule = await _context.Schedule.SingleOrDefaultAsync(m => m.ID == id); if (Schedule == null) { return NotFound(); } return Page(); } public async Task<IActionResult> OnPostAsync(int? id) { if (id == null) { return NotFound(); } Schedule = await _context.Schedule.FindAsync(id); if (Schedule != null) { _context.Schedule.Remove(Schedule); await _context.SaveChangesAsync(); } return RedirectToPage("./Index"); } } }
OnPostAsync 方法按 id 處理計划刪除:
public async Task<IActionResult> OnPostAsync(int? id) { if (id == null) { return NotFound(); } Schedule = await _context.Schedule.FindAsync(id); if (Schedule != null) { _context.Schedule.Remove(Schedule); await _context.SaveChangesAsync(); } return RedirectToPage("./Index"); }
成功刪除計划后,RedirectToPage 將返回到計划的“Index.cshtml”頁面。
有效的 Schedules Razor 頁面
頁面加載時,計划標題、公用計划和專用計划的標簽和輸入將呈現提交按鈕:

在不填充任何字段的情況下選擇“上傳”按鈕會違反此模型上的 [Required] 特性。 ModelState 無效。 會向用戶顯示驗證錯誤消息:

上傳一個或多個計划時,“已加載計划”部分會顯示已加載計划:

用戶可單擊該表中的“刪除”鏈接以訪問刪除確認視圖,並在其中選擇確認或取消刪除操作。
ASP.NET CORE RAZOR 到此結束
源代碼
https://pan.baidu.com/s/1c4k5au8
密碼:rtr7
