Web应用程序开发教程 - 第二章: 图书列表页面

关于本教程

在本系列教程中, 你将构建一个名为 Acme.BookStore 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的:

  • Entity Framework Core 做为ORM提供程序.
  • MVC / Razor Pages 做为UI框架.

本教程分为以下部分:

下载源码

本教程根据你的UI数据库偏好有多个版本,我们准备了几种可供下载的源码组合:

如果你在Windows中遇到 “文件名太长” or “解压错误”, 很可能与Windows最大文件路径限制有关. Windows文件路径的最大长度为250字符. 为了解决这个问题,参阅 在Windows 10中启用长路径.

如果你遇到与Git相关的长路径错误, 尝试使用下面的命令在Windows中启用长路径. 参阅 https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path git config --system core.longpaths true

视频教程

本章也被录制为视频教程 发布在YouTube.

动态JavaScript代理

JavaScript 端通过AJAX调用HTTP API端点是常见的做法. 你可以使用 $.ajax 或其他工具来调用端点. 但是ABP提供了更好的方法.

ABP动态为所有API端点创建 JavaScript代理. 所以你可以像调用Javascript本地方法一样使用任何端点.

在开发者控制台中进行测试

你可以在自己喜欢的浏览器的开发者控制台轻松的测试JavaScript代理. 运行应用程序,打开浏览器的开发者人员工具(快捷键通常是F12),切换到控制台选项卡,输入以下代码然后按回车:

  1. acme.bookStore.books.book.getList({}).done(function (result) { console.log(result); });
  • acme.bookStore.booksBookAppService 的命令空间转换成小驼峰形式.
  • bookBookAppService 的约定名称(删除AppService后缀并且转换为小驼峰).
  • getListCrudAppService 基类定义的 GetListAsync 方法的约定名称(删除Async后缀并且转换为小驼峰).
  • {} 参数将空对象发送到 GetListAsync 方法,该方法通常需要一个类型为 PagedAndSortedResultRequestDto 的对象,该对象用于将分页和排序选项发送到服务器(所有属性都是可选的,具有默认值. 因此你可以发送一个空对象).
  • getList 函数返回一个 promise. 你可以传递一个回调到 then(或done)函数来获取从服务器返回的结果.

运行该代码会产生以下输出:

bookstore-javascript-proxy-console

你可以看到服务端返回的 图书列表. 你也可以在开发者人员工具的 网络 选项卡查看客户端到服务端的通信:

bookstore-getlist-result-network

让我们使用 create 函数创建一本书:

  1. acme.bookStore.books.book.create({
  2. name: 'Foundation',
  3. type: 7,
  4. publishDate: '1951-05-24',
  5. price: 21.5
  6. }).then(function (result) {
  7. console.log('successfully created the book with id: ' + result.id);
  8. });

如果你下载了本教程的源代码并按照示例中的步骤操作,你需要传递authorId参数给创建方法以创建一本新书.

您应该在控制台中看到类似以下的消息:

  1. successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246

检查数据库中的 Books 表你会看到新的一行. 你可以自己尝试使用 get, updatedelete 函数.

在接下来的章节,我们将利用这些动态代理函数与服务器通信.

本地化

开始的UI开发之前,我们首先要准备本地化的文本(这是你通常在开发应用程序时需要做的).

本地化文本位于 Acme.BookStore.Domain.Shared 项目的 Localization/BookStore 文件夹下:

bookstore-localization-files

打开 en.json (英文翻译)文件并更改内容,如下所示:

  1. {
  2. "Culture": "en",
  3. "Texts": {
  4. "Menu:Home": "Home",
  5. "Welcome": "Welcome",
  6. "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.",
  7. "Menu:BookStore": "Book Store",
  8. "Menu:Books": "Books",
  9. "Actions": "Actions",
  10. "Close": "Close",
  11. "Delete": "Delete",
  12. "Edit": "Edit",
  13. "PublishDate": "Publish date",
  14. "NewBook": "New book",
  15. "Name": "Name",
  16. "Type": "Type",
  17. "Price": "Price",
  18. "CreationTime": "Creation time",
  19. "AreYouSure": "Are you sure?",
  20. "AreYouSureToDelete": "Are you sure you want to delete this item?",
  21. "Enum:BookType:0": "Undefined",
  22. "Enum:BookType:1": "Adventure",
  23. "Enum:BookType:2": "Biography",
  24. "Enum:BookType:3": "Dystopia",
  25. "Enum:BookType:4": "Fantastic",
  26. "Enum:BookType:5": "Horror",
  27. "Enum:BookType:6": "Science",
  28. "Enum:BookType:7": "Science fiction",
  29. "Enum:BookType:8": "Poetry"
  30. }
  31. }

简体中文翻译请打开zh-Hans.json文件 ,并将”Texts”对象中对应的值替换为中文.

  • 本地化关键字名称是任意的. 你可以设置任何名称. 对于特定的文本类型,我们更喜欢遵循一些约定:
    • 为按钮项添加 Menu: 前缀.
    • 使用 Enum:<enum-type>:<enum-value> 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化.

如果未在本地化文件中定义文本,则文本将回退到本地化键(ASP.NET Core的标准行为).

ABP本地化系统建立在ASP.NET Core标准本地化系统之上,并以多种方式进行了扩展. 有关详细信息请参见本地化文档.

创建图书页面

是时候创建可见的和可用的东西了! 我们将使用微软推荐的Razor Pages UI,而不是经典的MVC.

Acme.BookStore.Web 项目的 Pages 文件夹下创建一个名为新的 Books 的文件夹. 然后在文件夹右键选择 添加 > Razor Page 菜单. 输入名称 Index:

bookstore-add-index-page

打开 Index.cshtml 并把内容修改成下面这样:

  1. @page
  2. @using Acme.BookStore.Web.Pages.Books
  3. @model IndexModel
  4. <h2>Books</h2>

Index.cshtml.cs 内容应该是:

  1. using Microsoft.AspNetCore.Mvc.RazorPages;
  2. namespace Acme.BookStore.Web.Pages.Books
  3. {
  4. public class IndexModel : PageModel
  5. {
  6. public void OnGet()
  7. {
  8. }
  9. }
  10. }

将图书页面添加到主菜单

打开 Menus 文件夹中的 BookStoreMenuContributor 类,在 ConfigureMainMenuAsync 方法的底部添加如下代码:

  1. context.Menu.AddItem(
  2. new ApplicationMenuItem(
  3. "BooksStore",
  4. l["Menu:BookStore"],
  5. icon: "fa fa-book"
  6. ).AddItem(
  7. new ApplicationMenuItem(
  8. "BooksStore.Books",
  9. l["Menu:Books"],
  10. url: "/Books"
  11. )
  12. )
  13. );

运行项目,使用用户名 admin 和密码 1q2w3E* 登录到应用程序. 看到新菜单项已添加到顶部栏:

bookstore-menu-items

点击BookStore下的Books子菜单项就会跳转到空的图书页面.

图书列表

我们将使用Datatables.netJQuery插件来显示图书列表. Datatables可以完全通过AJAX工作,速度快,并提供良好的用户体验.

Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,无需在页面中引用样式和脚本文件.

Index.cshtml

Pages/Book/Index.cshtml 改成下面的样子:

  1. @page
  2. @using Acme.BookStore.Localization
  3. @using Acme.BookStore.Web.Pages.Books
  4. @using Microsoft.Extensions.Localization
  5. @model IndexModel
  6. @inject IStringLocalizer<BookStoreResource> L
  7. @section scripts
  8. {
  9. <abp-script src="/Pages/Books/Index.js" />
  10. }
  11. <abp-card>
  12. <abp-card-header>
  13. <h2>@L["Books"]</h2>
  14. </abp-card-header>
  15. <abp-card-body>
  16. <abp-table striped-rows="true" id="BooksTable"></abp-table>
  17. </abp-card-body>
  18. </abp-card>
  • abp-script tag helper用于将外部的 脚本 添加到页面中.它比标准的script标签多了很多额外的功能.它可以处理 最小化版本.查看捆绑 & 压缩文档获取更多信息.
  • abp-cardabp-table 是为Twitter Bootstrap的card component封装的 tag helpers.ABP中有很多tag helpers,可以很方便的使用大多数bootstrap组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看tag helpers 文档.

Index.js

Pages/Books/ 文件夹中创建 index.js文件

bookstore-index-js-file

index.js 的内容如下:

  1. $(function () {
  2. var l = abp.localization.getResource('BookStore');
  3. var dataTable = $('#BooksTable').DataTable(
  4. abp.libs.datatables.normalizeConfiguration({
  5. serverSide: true,
  6. paging: true,
  7. order: [[1, "asc"]],
  8. searching: false,
  9. scrollX: true,
  10. ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
  11. columnDefs: [
  12. {
  13. title: l('Name'),
  14. data: "name"
  15. },
  16. {
  17. title: l('Type'),
  18. data: "type",
  19. render: function (data) {
  20. return l('Enum:BookType:' + data);
  21. }
  22. },
  23. {
  24. title: l('PublishDate'),
  25. data: "publishDate",
  26. render: function (data) {
  27. return luxon
  28. .DateTime
  29. .fromISO(data, {
  30. locale: abp.localization.currentCulture.name
  31. }).toLocaleString();
  32. }
  33. },
  34. {
  35. title: l('Price'),
  36. data: "price"
  37. },
  38. {
  39. title: l('CreationTime'), data: "creationTime",
  40. render: function (data) {
  41. return luxon
  42. .DateTime
  43. .fromISO(data, {
  44. locale: abp.localization.currentCulture.name
  45. }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
  46. }
  47. }
  48. ]
  49. })
  50. );
  51. });
  • abp.localization.getResource 获取一个函数,该函数用于使用服务器端定义的相同JSON文件对文本进行本地化. 通过这种方式你可以与客户端共享本地化值.
  • abp.libs.datatables.normalizeConfiguration是一个辅助方法.不是必须的, 但是它通过为缺省的选项提供约定的值来简化Datatables配置.
  • abp.libs.datatables.createAjax是另一个辅助方法,用来适配ABP的动态JavaScript API代理和Datatable期望的参数格式.
  • acme.bookStore.books.book.getList 是动态JavaScript代理函数(上面已经介绍过了)
  • luxon 库也是该解决方案中预先配置的标准库,你可以轻松地执行日期/时间操作.

查看 Datatable文档 了解更多配置项.

运行最终应用程序

你可以运行应用程序!该部分的最终用户界面如下所示:

Book list

这是一个可以正常工作的,服务端分页,排序和本地化的图书列表.

下一章

查看本教程的下一章.