ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

时间:2024-01-20 15:45:09

前言:

本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。

本系列文章主要参考资料:

微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows

《Pro ASP.NET MVC 5》、《锋利的 jQuery》

此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。

项目 github 地址:https://github.com/NanaseRuri/LibraryDemo

本章内容:分页、自定义 HtmlHelper、通过模态窗口确认是否提交表单、上传文件、获取文件、预览文件、select 元素、表单提交数组、checkbox、js 确认关闭页面

注:在对 EF 中的数据进行更改时,需要调用对应 Context 的 SaveChange 方法才能对更改进行保存。

一、视图分页视图模型

首先创建一个视图模型用于确定每页的书籍数、页数

     public class PagingInfo
{
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; } public int TotalPages
{
get => (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage);
}
}

然后创建另一个视图模型用于确定该分页信息以及各分页的书籍:

      public class BookListViewModel
{
public IEnumerable<BookDetails> BookDetails { get; set; }
public PagingInfo PagingInfo { get; set; }
}

创建一个自定义 HtmlHelper 用于在视图中使用 Razor 语法获取分页,在 ASP.NET Core 中 TagBuilder 继承自 IHtmlContent,不能直接对 TagBuilder 进行赋值,需调用 MergeAttribute 方法和 InnerHtml 属性的 AppendHtml 方法编写标签;并且 TagBuilder 没有实现 ToString 方法,只能通过其 WriteTo 方法将内容写到一个 TextWriter 对象中以取出其值:

     public static class PagingHelper
{
public static HtmlString PageLinks(this IHtmlHelper html, PagingInfo pagingInfo, Func<int, string> pageUrl)
{
StringWriter writer=new StringWriter();
for (int i = ; i <= pagingInfo.TotalPages; i++)
{
TagBuilder tag=new TagBuilder("a");
tag.MergeAttribute("href",pageUrl(i));
tag.InnerHtml.AppendHtml(i.ToString());
if (i==pagingInfo.CurrentPage)
{
tag.AddCssClass("selected");
tag.AddCssClass("btn-primary");
}
tag.AddCssClass("btn btn-default");
tag.WriteTo(writer,HtmlEncoder.Default);
}
return new HtmlString(writer.ToString());
}
}

二、编辑图书信息页面的首页

在此准备使用 Session 更快地获取图书信息,为了在使用时更直观,此处对 Session 类进行扩展:

     public static class SessionExtensions
{
public static void Set<T>(this ISession session, string key, T value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
} public static T Get<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
}
}

创建一个 BookInfo 控制器:

    public class BookInfoController : Controller
{
private LendingInfoDbContext _lendingInfoDbContext; public BookInfoController(LendingInfoDbContext context)
{
_lendingInfoDbContext = context;
}
}

创建学生浏览的首页:

在此使用 Session 获取书籍列表:

创建 BookInfo 控制器并确定其中每个分页的书籍数,使用 Session 获取更快地获取书籍信息:

 1     public class BookInfoController : Controller
2 {
3 private LendingInfoDbContext _context;
4 private static int amout = 4;
5
6 public BookInfoController(LendingInfoDbContext context)
7 {
8 _context = context;
9 }
10
11 public IActionResult Index(string category, int page = 1)
12 {
13 IEnumerable<BookDetails> books = null;
14 if (HttpContext.Session != null)
15 {
16 books = HttpContext.Session.Get<IEnumerable<BookDetails>>("bookDetails");
17 }
18 if (books == null)
19 {
20 books = _context.BooksDetail;
21 HttpContext.Session?.Set<IEnumerable<BookDetails>>("books", books);
22 }
23 BookListViewModel model = new BookListViewModel()
24 {
25 PagingInfo = new PagingInfo()
26 {
27 ItemsPerPage = amout,
28 TotalItems = books.Count(),
29 CurrentPage = page,
30 },
31 BookDetails = books.OrderBy(b => b.FetchBookNumber).Skip((page - 1) * amout).Take(amout)
32 };
33 return View(model);
34 }
35
36 public FileContentResult GetImage(string isbn)
37 {
38 BookDetails target = _context.BooksDetail.FirstOrDefault(b => b.ISBN == isbn);
39 if (target != null)
40 {
41 return File(target.ImageData, target.ImageMimeType);
42 }
43 return null;
44 }
45 }

视图页面:

33 行利用 BookListViewModel 中 PagingInfo 的 CurrentPage 获取各序号,32 行中使 img 元素的 src 指向 BookInfoController 的 GetImage 方法以获取图片:

 @using LibraryDemo.HtmlHelpers
@model BookListViewModel
@{
ViewData["Title"] = "Index";
int i = ;
Layout = "_LendingLayout";
}
<style type="text/css">
tr > td {
padding: 5px;
padding-left: 20px;
}
tr+tr {
border-top: thin solid black;
padding-top: 10px;
}
</style> <hr />
<table>
<tbody>
@foreach (var book in Model.BookDetails)
{
<tr>
<td style="width: 3%">@((@Model.PagingInfo.CurrentPage-)*+i++)</td>
<td style="text-align: center; width: 150px; height: 200px;">
@if (book.ImageData == null)
{
<label>No Image</label>
}
else
{
<img class="img-thumbnail pull-left" src="@Url.Action("GetImage", "BookInfo", new {book.ISBN})" />
}
</td>
<td style="text-align: left;">
<a style="margin-left: 1em;" href="@Url.Action("Detail",new{isbn=@book.ISBN})">@book.Name</a>
<div style="margin-left: 2em;margin-top: 5px">
<span>@book.Author</span>
<br />
<span>@book.Press</span>
<p>@book.FetchBookNumber</p>
</div>
<div style="text-indent: 2em">
<p>@book.Description</p>
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="btn-group pull-right">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("Index", new { page = x}))
</div>

在此同样使用 Session 获取书籍列表:

利用 [Authorize] 特性指定 Role 属性确保只有 Admin 身份的人才能访问该页面:

         [Authorize(Roles = "Admin")]
public IActionResult BookDetails(string isbn, int page = )
{
IEnumerable<BookDetails> books = null;
BookListViewModel model;
if (HttpContext.Session != null)
{
books = HttpContext.Session.Get<IEnumerable<BookDetails>>("bookDetails");
}
if (books == null)
{
books = _context.BooksDetail.AsNoTracking();
HttpContext.Session?.Set<IEnumerable<BookDetails>>("books", books); }
if (isbn != null)
{
model = new BookListViewModel()
{
BookDetails = new List<BookDetails>() { books.FirstOrDefault(b => b.ISBN == isbn) },
PagingInfo = new PagingInfo()
};
return View(model);
}
model = new BookListViewModel()
{ PagingInfo = new PagingInfo()
{
ItemsPerPage = amout,
TotalItems = books.Count(),
CurrentPage = page,
},
BookDetails = books.OrderBy(b => b.FetchBookNumber).Skip((page - ) * amout).Take(amout)
};
return View(model);
}

BookDetails 视图,confirmDelete 为删除按钮添加了确认的模态窗口;

53 行为 glyphicon 为 Bootstrap 提供的免费图标,只能通过 span 元素使用:

     @using LibraryDemo.HtmlHelpers
@model BookListViewModel
@{
ViewData["Title"] = "BookDetails";
int i = ;
} <script>
function confirmDelete() {
var isbns = document.getElementsByName("isbns");
var message="确认删除";
for (i in isbns) {
if (isbns[i].checked) {
var book = isbns[i].parentElement.nextElementSibling.nextElementSibling.firstElementChild.innerHTML;
message=message+"《"+book+"》";
}
}
message = message + "?";
if (confirm(message) == true) {
return true;
} else {
return false;
}
}
</script> <style type="text/css">
tr + tr {
border-top: thin solid gray;
} .container {
width: 1200px;
}
</style> <hr />
@if (TempData["message"] != null)
{
<p>@TempData["message"]</p>
<br />
<br />
}
<form class="pull-left" action="@Url.Action("Search")">
@Html.DropDownList("keyword",new List<SelectListItem>()
{
new SelectListItem("书名","Name"),
new SelectListItem("ISBN","ISBN"),
new SelectListItem("索书号","FetchBookNumber"),
})
<input type="text" name="value"/>
<button type="submit"><span class="glyphicon glyphicon-search"></span></button>
</form>
<br />
<br /> <form method="post" asp-action="RemoveBooksAndBookDetails">
<table width="">
<tbody>
<tr>
<th></th>
<th >序号</th>
<th>标题</th>
<th ></th>
<th style="text-align: right">ISBN</th>
</tr>
@foreach (var book in Model.BookDetails)
{
<tr>
<td><input type="checkbox" name="isbns" value="@book.ISBN" /></td>
<td style=" padding-left: 10px">@((Model.PagingInfo.CurrentPage-)*+i++)</td>
<td><a asp-action="EditBookDetails" asp-route-isbn="@book.ISBN">@book.Name</a></td>
<td></td>
<td style="text-align: right;">@book.ISBN</td>
</tr>
}
</tbody>
</table>
<br/>
<div>
<a class="btn btn-primary" href="@Url.Action("AddBookDetails")">添加书籍</a>
<button type="submit" class="btn btn-danger" onclick="return confirmDelete()"> 删除书籍</button>
</div>
</form> <br />
<div class="btn-group pull-right">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("BookDetails", new { page = x }))
</div>

Index 页面:

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

BookDetails 页面:

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

三、添加书籍信息

在此为了接受图片需要使用 IFormFile 接口,为了使图片以原有的格式在浏览器中显示,需要用另一个字段 ImageType 保存文件的格式;

39 页使用 TempData 传递一次性信息告知书籍添加成功,在传递完成后 TempData 将被立即释放:

         [Authorize(Roles = "Admin")]
public IActionResult AddBookDetails(BookDetails model)
{
if (model == null)
{
model = new BookDetails();
}
return View(model);
} [HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> AddBookDetails(BookDetails model, IFormFile image)
{
BookDetails bookDetails = new BookDetails();
if (ModelState.IsValid)
{
if (image != null)
{
bookDetails.ImageMimeType = image.ContentType;
bookDetails.ImageData = new byte[image.Length];
await image.OpenReadStream().ReadAsync(bookDetails.ImageData, , (int)image.Length);
} bookDetails.ISBN = model.ISBN;
bookDetails.Name = model.Name;
bookDetails.Author = model.Author;
bookDetails.Description = model.Description;
bookDetails.FetchBookNumber = model.FetchBookNumber;
bookDetails.Press = model.Press;
bookDetails.PublishDateTime = model.PublishDateTime;
bookDetails.SoundCassettes = model.SoundCassettes;
bookDetails.Version = model.Version; await _lendingInfoDbContext.BooksDetail.AddAsync(bookDetails); _lendingInfoDbContext.SaveChanges();
TempData["message"] = $"已添加书籍《{model.Name}》";
return RedirectToAction("EditBookDetails");
}
return View(model);
}
 

AddBookDetails 视图:

为了使表单可以上传文件,需要指定表单的 enctype 属性值为 multipart/form-data,66 行中使用一个 a 元素包含用来上传文件的 input ,指定其 class 为 btn 以生成一个按钮,指定 href="javascript:;" 使该元素不会返回任何值。

指定 input 的 name 属性为 image 以在上传表单时进行模型绑定,指定其 accept 属性令其打开文件选择框时只接收图片。

JS 代码为 input 添加 onchange 事件以预览上传的图片,并为关闭或刷新页面时添加模态窗口进行确认,同时为提交按钮添加事件以重置 window.onbeforeunload 事件从而不弹出确认窗口:

 @model LibraryDemo.Models.DomainModels.BookDetails
@{
ViewData["Title"] = "AddBookDetails";
} <script>
function preview(file) {
$(".image").addClass("hidden");
$('.preview').wrap("<div></div>");
$(".preview").removeClass("hidden");
if (file.files && file.files[]) {
var reader = new FileReader();
reader.onload = function (evt) {
$('.preview').attr('src', evt.target.result);
}
reader.readAsDataURL(file.files[]);
} else {
$('.preview').attr('src', file.value);
}
}
window.onbeforeunload = function () {
return "您的数据未保存,确定退出?";
}
function removeOnbeforeunload() {
window.onbeforeunload = "";
}
</script> <h2>添加书籍</h2> <form enctype="multipart/form-data" method="post">
<div class="panel-body">
<div class="form-group">
@Html.LabelFor(m => m.ISBN)
@Html.TextBoxFor(m => m.ISBN, new { @class = "form-control" })
</div>
<div class="form-group">
@Html.LabelFor(m => m.Name)
@Html.TextBoxFor(m => m.Name, new { @class = "form-control" })
</div>
<div class="form-group">
@Html.LabelFor(m => m.Author)
@Html.TextBoxFor(m => m.Author, new { @class = "form-control" })
</div>
<div class="form-group">
@Html.LabelFor(m => m.Press)
@Html.TextBoxFor(m => m.Press, new { @class = "form-control" })
</div>
<div class="form-group">
@Html.LabelFor(m => m.FetchBookNumber)
@Html.TextBoxFor(m => m.FetchBookNumber, new { @class = "form-control" })
</div>
<div class="form-group">
@Html.LabelFor(m => m.SoundCassettes)
@Html.TextBoxFor(m => m.SoundCassettes, new { @class = "form-control" })
</div>
<div class="form-group">
@Html.LabelFor(m => m.Description)
@Html.TextAreaFor(m => m.Description, new { @class = "form-control", rows = })
</div>
<div class="form-group">
@Html.LabelFor(m => m.PublishDateTime)
<div>@Html.EditorFor(m => m.PublishDateTime)</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Version)
<div>@Html.EditorFor(m => m.Version)</div>
</div>
<div class="form-group">
<div style="position: relative;">
<label>Image</label>
<a class="btn" href="javascript:;">
选择图片
<input type="file" name="Image" size="" accept="image/*"
style="position: absolute; z-index: 2; top: 0; left: 0; filter: alpha(opacity=0); opacity: 0; background-color: transparent; color: transparent"
onchange="preview(this)" />
</a>
<img style="width: 150px;" class="hidden preview img-thumbnail">
</div>
</div>
<input type="submit" onclick="removeOnbeforeunload()"/>
</div>
</form>

结果:

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

四、删除书籍信息

删除书籍的动作方法:

此处通过在之前的 BookDetails 视图中指定 input 元素的 type 为 checkbox,指定 name 为 isbns 以实现多个字符串的模型绑定:

         [Authorize(Roles = "Admin")]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveBooksAndBookDetails(IEnumerable<string> isbns)
{
StringBuilder sb = new StringBuilder();
foreach (var isbn in isbns)
{
BookDetails bookDetails = _lendingInfoDbContext.BooksDetail.First(b => b.ISBN == isbn);
IQueryable<Book> books = _lendingInfoDbContext.Books.Where(b => b.ISBN == isbn);
_lendingInfoDbContext.BooksDetail.Remove(bookDetails);
_lendingInfoDbContext.Books.RemoveRange(books);
sb.Append("《" + bookDetails.Name + "》");
await _lendingInfoDbContext.SaveChangesAsync();
}
TempData["message"] = $"已移除书籍{sb.ToString()}";
return RedirectToAction("BookDetails");
}

结果:

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

五、编辑书籍信息

动作方法:

         [Authorize(Roles = "Admin")]
public async Task<IActionResult> EditBookDetails(string isbn)
{
BookDetails book = await _lendingInfoDbContext.BooksDetail.FirstOrDefaultAsync(b => b.ISBN == isbn);
if (book != null)
{
return View(book);
}
else
{
return RedirectToAction("BookDetails");
}
} [HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public async Task<ActionResult> EditBookDetails(BookDetails model, IFormFile image)
{
BookDetails bookDetails = _lendingInfoDbContext.BooksDetail.FirstOrDefault(b => b.ISBN == model.ISBN);
if (ModelState.IsValid)
{
if (bookDetails != null)
{
if (image != null)
{
bookDetails.ImageMimeType = image.ContentType;
bookDetails.ImageData = new byte[image.Length];
await image.OpenReadStream().ReadAsync(bookDetails.ImageData, , (int)image.Length);
} BookDetails newBookDetails = model; bookDetails.Name = newBookDetails.Name;
bookDetails.Author = newBookDetails.Author;
bookDetails.Description = newBookDetails.Description;
bookDetails.FetchBookNumber = newBookDetails.FetchBookNumber;
bookDetails.Press = newBookDetails.Press;
bookDetails.PublishDateTime = newBookDetails.PublishDateTime;
bookDetails.SoundCassettes = newBookDetails.SoundCassettes;
bookDetails.Version = newBookDetails.Version; await _lendingInfoDbContext.SaveChangesAsync();
TempData["message"] = $"《{newBookDetails.Name}》修改成功";
return RedirectToAction("EditBookDetails");
}
}
return View(model);
}

此处视图与之前 AddBookDetails 大致相同,但在此对一些视图中的 ISBN 字段添加了 readonly 属性使它们不能被直接编辑:

 @model LibraryDemo.Models.DomainModels.BookDetails

 @{
ViewData["Title"] = "EditBookDetails";
} <script>
function preview(file) {
$(".image").addClass("hidden");
$('.preview').wrap("<div></div>");
$(".preview").removeClass("hidden");
if (file.files && file.files[]){
var reader = new FileReader();
reader.onload = function(evt){
$('.preview').attr('src' , evt.target.result);
}
reader.readAsDataURL(file.files[]);
}else{
$('.preview').attr('src' , file.value);
}
}
window.onload = function() {
$("div>input").addClass("form-control");
var isbn = document.getElementById("ISBN");
isbn.setAttribute("readonly","true");
}
window.onbeforeunload = function (event) {
return "您的数据未保存,确定退出?";
}
function removeOnbeforeunload() {
window.onbeforeunload = "";
}
</script> <h2>编辑书籍</h2> @section Scripts
{ } <form enctype="multipart/form-data" method="post">
<div class="panel-body">
<div class="form-group">
@Html.LabelFor(m => m.ISBN)
@Html.EditorFor(m => m.ISBN)
</div>
<div class="form-group">
@Html.LabelFor(m => m.Name)
@Html.TextBoxFor(m => m.Name, new {@class = "form-control"})
</div>
<div class="form-group">
@Html.LabelFor(m => m.Author)
@Html.TextBoxFor(m => m.Author, new {@class = "form-control"})
</div>
<div class="form-group">
@Html.LabelFor(m => m.Press)
@Html.TextBoxFor(m => m.Press, new {@class = "form-control"})
</div>
<div class="form-group">
@Html.LabelFor(m => m.FetchBookNumber)
@Html.TextBoxFor(m => m.FetchBookNumber, new {@class = "form-control"})
</div>
<div class="form-group">
@Html.LabelFor(m => m.SoundCassettes)
@Html.TextBoxFor(m => m.SoundCassettes, new {@class = "form-control"})
</div>
<div class="form-group">
@Html.LabelFor(m => m.Description)
@Html.TextAreaFor(m => m.Description, new {@class = "form-control", rows = })
</div>
<div class="form-group">
@Html.LabelFor(m => m.PublishDateTime)
<div>@Html.EditorFor(m => m.PublishDateTime)</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Version)
<div>@Html.EditorFor(m => m.Version)</div>
</div>
<div class="form-group">
<div style="position: relative;">
<label>Image</label>
<a class="btn" href="javascript:;">
选择图片
<input type="file" name="Image" size="" accept="image/*"
style="position: absolute; z-index: 2; top: 0; left: 0; filter: alpha(opacity=0); opacity: 0; background-color: transparent; color: transparent"
onchange="preview(this)" />
</a>
<img style="width: 150px;" class="hidden preview img-thumbnail">
</div>
@if (Model.ImageData == null)
{
<div class="form-control-static image">No Image</div>
}
else
{
<img class="img-thumbnail image" style="width: 150px;" src="@Url.Action("GetImage", "BookInfo", new {Model.ISBN})" />
}
</div>
<br />
<a class="btn btn-primary" asp-action="Books" asp-route-isbn="@Model.ISBN" onclick="return removeOnbeforeunload()">编辑外借书籍信息</a>
<br />
<br />
<input type="submit" class="btn btn-success" onclick="return removeOnbeforeunload()"/>
</div>
</form>

结果:

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

六、查询特定书籍

此处和之前的账号登录处一样使用 switch 对不同的关键词进行检索:

         public async Task<IActionResult> Search(string keyWord, string value)
{
BookDetails bookDetails = new BookDetails();
switch (keyWord)
{
case "Name":
bookDetails =await _context.BooksDetail.FirstOrDefaultAsync(b => b.Name == value);
break;
case "ISBN":
bookDetails =await _context.BooksDetail.FirstOrDefaultAsync(b => b.ISBN == value);
break;
case "FetchBookNumber":
bookDetails =await _context.BooksDetail.FirstOrDefaultAsync(b => b.FetchBookNumber == value);
break;
} if (bookDetails!=null)
{
return RedirectToAction("EditBookDetails", new {isbn = bookDetails.ISBN});
} TempData["message"] = "找不到该书籍";
return RedirectToAction("BookDetails");
}

结果:

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查

ASP.NET Core MVC 打造一个简单的图书馆管理系统 (修正版)(四)图书信息的增删改查