前言:
本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。
本系列文章主要参考资料:
微软文档: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 方法才能对更改进行保存。
一、视图分页视图模型
首先创建一个视图模型用于确定每页的书籍数、页数
1 public class PagingInfo 2 { 3 public int TotalItems { get; set; } 4 public int ItemsPerPage { get; set; } 5 public int CurrentPage { get; set; } 6 7 public int TotalPages 8 { 9 get => (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); 10 } 11 }
然后创建另一个视图模型用于确定该分页信息以及各分页的书籍:
1 public class BookListViewModel 2 { 3 public IEnumerableBookDetails { get; set; } 4 public PagingInfo PagingInfo { get; set; } 5 }
创建一个自定义 HtmlHelper 用于在视图中使用 Razor 语法获取分页,在 ASP.NET Core 中 TagBuilder 继承自 IHtmlContent,不能直接对 TagBuilder 进行赋值,需调用 MergeAttribute 方法和 InnerHtml 属性的 AppendHtml 方法编写标签;并且 TagBuilder 没有实现 ToString 方法,只能通过其 WriteTo 方法将内容写到一个 TextWriter 对象中以取出其值:
1 public static class PagingHelper 2 { 3 public static HtmlString PageLinks(this IHtmlHelper html, PagingInfo pagingInfo, Func<int, string> pageUrl) 4 { 5 StringWriter writer=new StringWriter(); 6 for (int i = 1; i <= pagingInfo.TotalPages; i++) 7 { 8 TagBuilder tag=new TagBuilder("a"); 9 tag.MergeAttribute("href",pageUrl(i)); 10 tag.InnerHtml.AppendHtml(i.ToString()); 11 if (i==pagingInfo.CurrentPage) 12 { 13 tag.AddCssClass("selected"); 14 tag.AddCssClass("btn-primary"); 15 } 16 tag.AddCssClass("btn btn-default"); 17 tag.WriteTo(writer,HtmlEncoder.Default); 18 } 19 return new HtmlString(writer.ToString()); 20 } 21 }
二、编辑图书信息页面的首页
在此准备使用 Session 更快地获取图书信息,为了在使用时更直观,此处对 Session 类进行扩展:
1 public static class SessionExtensions 2 { 3 public static void Set(this ISession session, string key, T value) 4 { 5 session.SetString(key, JsonConvert.SerializeObject(value)); 6 } 7 8 public static T Get (this ISession session, string key) 9 { 10 var value = session.GetString(key); 11 return value == null ? default(T) : JsonConvert.DeserializeObject (value); 12 } 13 }
创建一个 BookInfo 控制器:
1 public class BookInfoController : Controller 2 { 3 private LendingInfoDbContext _lendingInfoDbContext; 4 5 public BookInfoController(LendingInfoDbContext context) 6 { 7 _lendingInfoDbContext = context; 8 } 9 }
创建学生浏览的首页:
在此使用 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 books = null; 14 if (HttpContext.Session != null) 15 { 16 books = HttpContext.Session.Get>("bookDetails"); 17 } 18 if (books == null) 19 { 20 books = _context.BooksDetail; 21 HttpContext.Session?.Set>("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 方法以获取图片:
1 @using LibraryDemo.HtmlHelpers 2 @model BookListViewModel 3 @{ 4 ViewData["Title"] = "Index"; 5 int i = 1; 6 Layout = "_LendingLayout"; 7 } 8 18 19
20
"width: 3%">@((@Model.PagingInfo.CurrentPage-1)*4+i++) | 26"text-align: center; width: 150px; height: 200px;"> 27 @if (book.ImageData == null) 28 { 29 30 } 31 else 32 { 33 class="img-thumbnail pull-left" src="@Url.Action("GetImage", "BookInfo", new {book.ISBN})" /> 34 } 35 | 36"text-align: left;">
37 "margin-left: 1em;" href="@Url.Action("Detail",new{[email protected]})">@book.Name
38 "margin-left: 2em;margin-top: 5px">
39 @book.Author
40
44 41 @book.Press 42 @book.FetchBookNumber 43"text-indent: 2em">
45
47 @book.Description 46 |
48
在此同样使用 Session 获取书籍列表:
利用 [Authorize] 特性指定 Role 属性确保只有 Admin 身份的人才能访问该页面:
1 [Authorize(Roles = "Admin")] 2 public IActionResult BookDetails(string isbn, int page = 1) 3 { 4 IEnumerablebooks = null; 5 BookListViewModel model; 6 if (HttpContext.Session != null) 7 { 8 books = HttpContext.Session.Get >("bookDetails"); 9 } 10 if (books == null) 11 { 12 books = _context.BooksDetail.AsNoTracking(); 13 HttpContext.Session?.Set >("books", books); 14 15 } 16 if (isbn != null) 17 { 18 model = new BookListViewModel() 19 { 20 BookDetails = new List () { books.FirstOrDefault(b => b.ISBN == isbn) }, 21 PagingInfo = new PagingInfo() 22 }; 23 return View(model); 24 } 25 model = new BookListViewModel() 26 { 27 28 PagingInfo = new PagingInfo() 29 { 30 ItemsPerPage = amout, 31 TotalItems = books.Count(), 32 CurrentPage = page, 33 }, 34 BookDetails = books.OrderBy(b => b.FetchBookNumber).Skip((page - 1) * amout).Take(amout) 35 }; 36 return View(model); 37 }
BookDetails 视图,confirmDelete 为删除按钮添加了确认的模态窗口;
53 行为 glyphicon 为 Bootstrap 提供的免费图标,只能通过 span 元素使用:
1 @using LibraryDemo.HtmlHelpers 2 @model BookListViewModel 3 @{ 4 ViewData["Title"] = "BookDetails"; 5 int i = 1; 6 } 7 8 26 27 36 37 3855
39 @if (TempData["message"] != null) 40 { 41@TempData["message"]
42
43
44 } 45
56
57 58 86 87
88
Index 页面:
BookDetails 页面:
三、添加书籍信息
在此为了接受图片需要使用 IFormFile 接口,为了使图片以原有的格式在浏览器中显示,需要用另一个字段 ImageType 保存文件的格式;
39 页使用 TempData 传递一次性信息告知书籍添加成功,在传递完成后 TempData 将被立即释放:
1 [Authorize(Roles = "Admin")] 2 public IActionResult AddBookDetails(BookDetails model) 3 { 4 if (model == null) 5 { 6 model = new BookDetails(); 7 } 8 return View(model); 9 } 10 11 [HttpPost] 12 [ValidateAntiForgeryToken] 13 [Authorize(Roles = "Admin")] 14 public async TaskAddBookDetails(BookDetails model, IFormFile image) 15 { 16 BookDetails bookDetails = new BookDetails(); 17 if (ModelState.IsValid) 18 { 19 if (image != null) 20 { 21 bookDetails.ImageMimeType = image.ContentType; 22 bookDetails.ImageData = new byte[image.Length]; 23 await image.OpenReadStream().ReadAsync(bookDetails.ImageData, 0, (int)image.Length); 24 } 25 26 bookDetails.ISBN = model.ISBN; 27 bookDetails.Name = model.Name; 28 bookDetails.Author = model.Author; 29 bookDetails.Description = model.Description; 30 bookDetails.FetchBookNumber = model.FetchBookNumber; 31 bookDetails.Press = model.Press; 32 bookDetails.PublishDateTime = model.PublishDateTime; 33 bookDetails.SoundCassettes = model.SoundCassettes; 34 bookDetails.Version = model.Version; 35 36 await _lendingInfoDbContext.BooksDetail.AddAsync(bookDetails); 37 38 _lendingInfoDbContext.SaveChanges(); 39 TempData["message"] = $"已添加书籍《{model.Name}》"; 40 return RedirectToAction("EditBookDetails"); 41 } 42 return View(model); 43 }
AddBookDetails 视图:
为了使表单可以上传文件,需要指定表单的 enctype 属性值为 multipart/form-data,66 行中使用一个 a 元素包含用来上传文件的 input ,指定其 class 为 btn 以生成一个按钮,指定 href="javascript:;" 使该元素不会返回任何值。
指定 input 的 name 属性为 image 以在上传表单时进行模型绑定,指定其 accept 属性令其打开文件选择框时只接收图片。
JS 代码为 input 添加 onchange 事件以预览上传的图片,并为关闭或刷新页面时添加模态窗口进行确认,同时为提交按钮添加事件以重置 window.onbeforeunload 事件从而不弹出确认窗口:
1 @model LibraryDemo.Models.DomainModels.BookDetails 2 @{ 3 ViewData["Title"] = "AddBookDetails"; 4 } 5 6 28 29添加书籍
30 31
结果:
四、删除书籍信息
删除书籍的动作方法:
此处通过在之前的 BookDetails 视图中指定 input 元素的 type 为 checkbox,指定 name 为 isbns 以实现多个字符串的模型绑定:
1 [Authorize(Roles = "Admin")] 2 [HttpPost] 3 [ValidateAntiForgeryToken] 4 public async TaskRemoveBooksAndBookDetails(IEnumerable<string> isbns) 5 { 6 StringBuilder sb = new StringBuilder(); 7 foreach (var isbn in isbns) 8 { 9 BookDetails bookDetails = _lendingInfoDbContext.BooksDetail.First(b => b.ISBN == isbn); 10 IQueryable books = _lendingInfoDbContext.Books.Where(b => b.ISBN == isbn); 11 _lendingInfoDbContext.BooksDetail.Remove(bookDetails); 12 _lendingInfoDbContext.Books.RemoveRange(books); 13 sb.Append("《" + bookDetails.Name + "》"); 14 await _lendingInfoDbContext.SaveChangesAsync(); 15 } 16 TempData["message"] = $"已移除书籍{sb.ToString()}"; 17 return RedirectToAction("BookDetails"); 18 }
结果:
五、编辑书籍信息
动作方法:
1 [Authorize(Roles = "Admin")] 2 public async TaskEditBookDetails(string isbn) 3 { 4 BookDetails book = await _lendingInfoDbContext.BooksDetail.FirstOrDefaultAsync(b => b.ISBN == isbn); 5 if (book != null) 6 { 7 return View(book); 8 } 9 else 10 { 11 return RedirectToAction("BookDetails"); 12 } 13 } 14 15 [HttpPost] 16 [ValidateAntiForgeryToken] 17 [Authorize(Roles = "Admin")] 18 public async Task EditBookDetails(BookDetails model, IFormFile image) 19 { 20 BookDetails bookDetails = _lendingInfoDbContext.BooksDetail.FirstOrDefault(b => b.ISBN == model.ISBN); 21 if (ModelState.IsValid) 22 { 23 if (bookDetails != null) 24 { 25 if (image != null) 26 { 27 bookDetails.ImageMimeType = image.ContentType; 28 bookDetails.ImageData = new byte[image.Length]; 29 await image.OpenReadStream().ReadAsync(bookDetails.ImageData, 0, (int)image.Length); 30 } 31 32 BookDetails newBookDetails = model; 33 34 bookDetails.Name = newBookDetails.Name; 35 bookDetails.Author = newBookDetails.Author; 36 bookDetails.Description = newBookDetails.Description; 37 bookDetails.FetchBookNumber = newBookDetails.FetchBookNumber; 38 bookDetails.Press = newBookDetails.Press; 39 bookDetails.PublishDateTime = newBookDetails.PublishDateTime; 40 bookDetails.SoundCassettes = newBookDetails.SoundCassettes; 41 bookDetails.Version = newBookDetails.Version; 42 43 await _lendingInfoDbContext.SaveChangesAsync(); 44 TempData["message"] = $"《{newBookDetails.Name}》修改成功"; 45 return RedirectToAction("EditBookDetails"); 46 } 47 } 48 return View(model); 49 }
此处视图与之前 AddBookDetails 大致相同,但在此对一些视图中的 ISBN 字段添加了 readonly 属性使它们不能被直接编辑:
1 @model LibraryDemo.Models.DomainModels.BookDetails 2 3 @{ 4 ViewData["Title"] = "EditBookDetails"; 5 } 6 7 34 35编辑书籍
36 37 @section Scripts 38 { 39 40 } 41 42
结果:
六、查询特定书籍
此处和之前的账号登录处一样使用 switch 对不同的关键词进行检索:
1 public async TaskSearch(string keyWord, string value) 2 { 3 BookDetails bookDetails = new BookDetails(); 4 switch (keyWord) 5 { 6 case "Name": 7 bookDetails =await _context.BooksDetail.FirstOrDefaultAsync(b => b.Name == value); 8 break; 9 case "ISBN": 10 bookDetails =await _context.BooksDetail.FirstOrDefaultAsync(b => b.ISBN == value); 11 break; 12 case "FetchBookNumber": 13 bookDetails =await _context.BooksDetail.FirstOrDefaultAsync(b => b.FetchBookNumber == value); 14 break; 15 } 16 17 if (bookDetails!=null) 18 { 19 return RedirectToAction("EditBookDetails", new {isbn = bookDetails.ISBN}); 20 } 21 22 TempData["message"] = "找不到该书籍"; 23 return RedirectToAction("BookDetails"); 24 }
结果: