前言:
本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。
本系列文章主要参考资料:
微软文档: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 页面:
BookDetails 页面:
三、添加书籍信息
在此为了接受图片需要使用 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>
结果:
四、删除书籍信息
删除书籍的动作方法:
此处通过在之前的 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");
}
结果:
五、编辑书籍信息
动作方法:
[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>
结果:
六、查询特定书籍
此处和之前的账号登录处一样使用 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");
}
结果: